Posted on 10. December 2023

.NET 8 Networking Improvements

Read this article in your language IT | EN | DE | ES

Покращення роботи з мережею в .NET 8

Вже стало традицією публікувати в блозі повідомлення про нові цікаві зміни в мережевому просторі з новим випуском .NET . Цього року ми хотіли б запровадити зміни в просторі HTTP , нові додані метрики, нові API HttpClientFactory, тощо.


HTTP

Метрики

.NET 8 додає вбудовані HTTP-метрики як до ASP.NET Core, так і до HttpClient, за допомогою System.Diagnostics.Metrics API, який був представлений у .NET 6. І API-інтерфейси Metrics, і семантика нових вбудованих показників були розроблені у тісній співпраці з OpenTelemetry, переконуючись, що нові показники відповідають стандарту та добре працюють із такими популярними інструментами, як Prometheus і Grafana. .

API System.Diagnostics.Metrics представляє багато нових функцій, яких не було в EventCounters. Ці функції широко використовуються новими вбудованими метриками, що призводить до ширшої функціональності, досягнутої простішим і елегантнішим набором інструментів. Наведу кілька прикладів:

Гістограми дозволяють нам повідомляти про тривалості, напр. тривалість запиту ( http.client.request.duration) або тривалість з’єднання (http.client.connection.duration). Це нові показники без відповідників EventCounter.

Багатовимірність дозволяє нам додавати теги (атрибути або мітки) до вимірювань, що означає, що ми можемо повідомляти таку інформацію, як server.address (ідентифікує джерело URI) або error.type (описує причину помилки, якщо запит не вдається) разом із вимірюваннями. Багатовимірність також забезпечує спрощення: для звітування про кількість відкритих HTTP-з’єднань SocketsHttpHandler використовує 3 EventCounters: http11-connections-current-total, http20-connections-current-totalіhttp30-connections-current-total , тоді як еквівалент цих лічильників Metrics є одним інструментом, http.client.open_connections, де версія HTTP повідомляється за допомогою тегу network.protocol.version.

Щоб допомогти використати випадки, коли вбудованих тегів недостатньо для класифікації вихідних HTTP-запитів, метрика http.client.request.duration підтримує впровадження тегів, визначених користувачем. Це називається збагаченням.

Інтеграція IMeterFactory дозволяє ізолювати екземпляри Meter, які використовуються для випромінювання метрик HTTP, що полегшує написання тестів, які перевіряють вбудовані вимірювання, і дозволяє паралельне виконання таких тестів.

– Хоча це не стосується вбудованих мережевих метрик, варто зазначити, що API колекції System.Diagnostics.Metrics також є більш досконалими: вони суворо типізовані та більш продуктивні та відкривають кільком слухачам одночасно доступ до неагрегованих вимірювань.

Ці переваги разом призводять до кращих, багатших показників, які можна ефективніше збирати сторонніми інструментами, такими як Prometheus. Завдяки гнучкості PromQL (Prometheus Query Language) , яка дозволяє створювати складні запити на основі багатовимірних показників, зібраних із мережевого стеку .NET, користувачі тепер можуть отримувати статистичні дані про стан і працездатність екземплярів HttpClient та SocketsHttpHandler на рівні, який не був раніше можливим.

З іншого боку, слід зазначити, що лише компоненти System.Net.Http та System.Net.NameResolution інструментуються за допомогою System.Diagnostics.Metrics у .NET 8, а це означає, що вам все одно потрібно використовувати EventCounters для отримання лічильників із нижчих рівнів стеку, таких як System.Net.Sockets. Хоча всі вбудовані лічильники подій, які існували в попередніх версіях, все ще підтримуються, команда .NET не очікує значних нових інвестицій у лічильники подій, і нові вбудовані інструменти будуть додані, використовуючи System.Diagnostics.Metrics, в майбутніх версіях.

Щоб отримати додаткові відомості про використання вбудованих метрик HTTP, прочитайте наш підручник щодо мережевих метрик у .NET . Він містить приклади збирання та звітування за допомогою Prometheus і Grafana, а також демонструє, як збагачувати та тестувати вбудовані HTTP-метрики. Щоб отримати вичерпний список вбудованих інструментів, перегляньте документацію для метрик System.Net . Якщо вас більше цікавить серверна сторона, будь ласка, прочитайте документацію про показники ASP.NET Core.

Розширена телеметрія

Окрім нових показників, наявні телеметричні події EventSource, представлені в .NET 5, були доповнені додатковою інформацією про HTTP-з’єднання ( dotnet/runtime#88853 ):

Тепер, коли встановлюється нове з’єднання, подія логує connectionId разом із схемою, портом та IP-адресою однорангового пристрою. Це дає змогу співвідносити запити та відповіді зі з’єднаннями через подію RequestHeadersStart, яка виникає, коли запит пов’язується з об’єднаним з’єднанням і починає оброблятися, яка також реєструє пов’язані connectionId. Це особливо цінно в діагностичних сценаріях, коли користувачі хочуть бачити IP-адреси серверів, які обслуговують їхні HTTP-запити, що було основною мотивацією додавання ( dotnet/runtime#63159 ).

 

Події можна використовувати багатьма способами, див. Мережева телеметрія в .NET – Події . Але для покращеного журналювання під час процесу EventListener можна використовувати спеціальний параметр, щоб співвіднести пару запит/відповідь із даними підключення:

Крім того, подію Redirect було розширено, щоб включити URI перенаправлення:

-void Redirect();

 

+void Redirect(string redirectUri);

Коди помилок HTTP

Одна з проблем діагностики HttpClient полягала в тому, що у випадку винятку було непросто програмно визначити точну причину помилки. Єдиним способом відрізнити багато з них було розібрати повідомлення про винятки з HttpRequestException. Крім того, інші реалізації HTTP, такі як WinHTTP із кодами помилок ERROR_WINHTTP_*, пропонують такі функції у формі числових кодів або перерахувань. Отже, .NET 8 представляє подібний перелік і надає його у винятках, створених обробкою HTTP, які є:

HttpRequestException для обробки запиту до отримання заголовків відповіді.

HttpIOException для читання змісту відповіді.

Дизайн HttpRequestError enum і те, як він підключається до винятків HTTP, описано в пропозиції API dotnet/runtime#76644 .


Тепер споживач методів HttpClient може обробляти конкретні внутрішні помилки набагато легше та надійніше:

Підтримка проксі HTTPS

Однією з особливо затребуваних функцій, яку було реалізовано в цьому випуску, є підтримка HTTPS-проксі ( dotnet/runtime#31113). Тепер можна використовувати проксі, які обслуговують запити через HTTPS, тобто з’єднання з проксі є безпечним. Це нічого не говорить про сам запит від проксі, який може бути як HTTP, так і HTTPS. У випадку звичайного текстового HTTP-запиту з’єднання з проксі-сервером HTTPS є безпечним (через HTTPS), а потім іде простий текстовий запит від проксі-сервера до пункту призначення. У разі запиту HTTPS (тунель проксі) початковий запит CONNECT на відкриття тунелю буде надіслано через захищений канал (HTTPS) до проксі, а потім запит HTTPS від проксі до пункту призначення через тунель.

 

Щоб скористатися цією функцією, все, що потрібно, це використовувати схему HTTPS під час налаштування проксі:

HttpClientFactory

.NET 8 розширює можливості налаштування HttpClientFactory, включаючи параметри клієнта за замовчуванням, спеціальне логування та спрощену конфігурацію SocketsHttpHandler. API реалізовано в пакеті Microsoft.Extensions.Http, який доступний на NuGet і включає підтримку .NET Standard 2.0. Таким чином, цю функцію можна використовувати клієнтам не лише в .NET 8, але й у всіх версіях .NET, включаючи .NET Framework (єдиним винятком є ​​відповідні API SocketsHttpHandler, які доступні лише для .NET 5+).

Налаштуйте параметри за замовчуванням для всіх клієнтів

.NET 8 додає можливість установити конфігурацію за замовчуванням, яка використовуватиметься для всіх HttpClient-ів створених HttpClientFactory( dotnet/runtime#87914). Це корисно, коли всі або більшість зареєстрованих клієнтів містять однакову підмножину конфігурації.

 

Розглянемо приклад, де визначено два клієнти з іменами, і обидва вони потребують MyAuthHandler для свого ланцюжка обробників повідомлень.

 

Щоб витягти загальну частину, тепер ви можете використовувати метод ConfigureHttpClientDefaults:

Усі методи розширення IHttpClientBuilder, які використовуються з AddHttpClient також можна використовувати всередині .ConfigureHttpClientDefaults.

Конфігурація за замовчуванням (ConfigureHttpClientDefaults) застосовується до всіх клієнтів перед конфігураціями для конкретного клієнта (AddHttpClient); їх відносне положення в реєстрації не має значення. ConfigureHttpClientDefaults можна зареєструвати кілька разів, у цьому випадку конфігурації будуть застосовані одна за одною в порядку реєстрації. Будь-яка частина конфігурації може бути перевизначена або змінена в конфігураціях для конкретного клієнта, наприклад, ви можете встановити додаткові параметри для об’єкта HttpClient або основного обробника, видалити раніше доданий додатковий обробник тощо.

 

Зауважте, що з версії 8.0 метод ConfigureHttpMessageHandlerBuilder застарів . Натомість вам слід використовувати методи ConfigurePrimaryHttpMessageHandler(Action<HttpMessageHandler,IServiceProvider>))) або ConfigureAdditionalHttpMessageHandlers, щоб змінити попередньо налаштований основний обробник або список додаткових обробників відповідно.

Змінити логування HttpClient

Налаштування (або навіть просто вимкнення) логування  HttpClientFactory було однією з давно запитуваних функцій (dotnet/runtime#77312).

Огляд старого логування

Логування за замовчуванням («старе»), додане HttpClientFactory досить багатослівне і видає 8 повідомлень журналу на запит:

1. Почати сповіщення з URI запиту — перед розповсюдженням через конвеєр обробника делегування;

2. Заголовки запитів — перед пайплайном обробника;

3. Почати сповіщення з URI запиту — після пайплайну обробника;

4. Заголовки запитів — після пайплайну обробника;

5. Зупинити сповіщення з вичерпаним часом — перед розповсюдженням відповіді через конвеєр обробника делегування;

6. Заголовки відповіді — перед тим, як передавати відповідь назад;

7. Зупинити сповіщення з вичерпаним часом — після повернення відповіді;

8. Заголовки відповіді — після передачі відповіді назад.

 

Це можна проілюструвати схемою нижче. На цій і наступних діаграмах *і [...]позначає подію логування (у реалізації за замовчуванням повідомлення журналу записується в ILogger), і символізує потік даних через прикладний і транспортний рівні.

Консольний вивід логу за замовчуванням HttpClientFactory виглядає так:

Зауважте, що для перегляду повідомлень рівня Trace вам потрібно ввімкнути це у файлі конфігурації глобального логування або за допомогою SetMinimumLevel(LogLevel.Trace). Але навіть враховуючи лише Informational повідомлення, «старе» логування все одно має 4 повідомлення на запит.

 

Щоб видалити стандартне (або раніше додане) логування, ви можете використати новий метод розширення RemoveAllLoggers(). Він особливо потужний у поєднанні з API ConfigureHttpClientDefaults, описаним у розділі «Налаштування параметрів за замовчуванням для всіх клієнтів» вище. Таким чином ви можете видалити «старе» журналювання для всіх клієнтів одним рядком:

Якщо вам колись знадобиться повернути «старе» логування, наприклад, для певного клієнта, ви можете зробити це за допомогою AddDefaultLogger().


Додати спеціальне логування

Окрім можливості видаляти «старе» логування, нові API HttpClientFactory також дозволяють повністю налаштувати логування. Ви можете вказати, що та як реєструватиметься, коли HttpClient починає запит, отримує відповідь або створює виняток.

Ви можете додати кілька власних реєстраторів разом, якщо ви вирішите зробити це, наприклад, консольні та ETW-реєстратори або обидва «пакетні» та «непакетовані» журнали. Через його адитивну природу вам може знадобитися явно видалити «старе» логування за замовчуванням заздалегідь.


Щоб додати настроюване ведення журналу, вам потрібно реалізувати інтерфейс IHttpClientLogger, а потім додати настроюваний реєстратор до клієнта за допомогою AddLogger. Зауважте, що реалізація логування не повинна створювати жодних винятків, інакше це може порушити виконання запиту.

 

Реєстрація:

Приклад імплементації логера:

Зразок результату:

Об’єкт контексту запиту

Контекстний об’єкт можна використовувати для зіставлення виклику LogRequestStart з відповідним викликом LogRequestStop для передачі даних від одного до іншого. Контекстний об’єкт створюється, LogRequestStart, а потім повертається до LogRequestStop. Це може бути сумка майна або будь-який інший об’єкт, який містить необхідні дані.

Якщо об’єкт контексту не потрібен, імплементація може повернути null з LogRequestStart.

 

У наступному прикладі показано, як об’єкт контексту можна використовувати для передачі ідентифікатора спеціального запиту.

Уникайте читання з потоків вмісту

Якщо ви збираєтеся читати та логувати,  наприклад, вміст запиту та відповіді, майте на увазі, що це потенційно може мати несприятливий побічний ефект для роботи кінцевого користувача та викликати помилки. Наприклад, вміст запиту може бути використано до того, як його буде надіслано, або вміст відповіді величезного розміру може бути буферизовано в пам’яті. Крім того, до .NET 7 доступ до заголовків не був потоково безпечним і міг призвести до помилок і неочікуваної поведінки.

Використовуйте асинхронне логування з обережністю

Ми очікуємо, що синхронний інтерфейс IHttpClientLogger підійде для переважної більшості випадків використання спеціального логування. Рекомендується утримуватися від використання асинхронного протоколу з міркувань продуктивності. Однак, якщо суворо потрібен асинхронний доступ до логування, ви можете застосувати асинхронну версію IHttpClientAsyncLogger. Він походить від IHttpClientLogger, тому може використовувати той самий API AddLogger для реєстрації.

Зауважте, що в такому випадку слід також реалізувати аналоги синхронізації методів логування, особливо якщо реалізація є частиною бібліотеки, орієнтованої на .NET Standard або .NET 5+. Відповідники синхронізації викликаються з методів синхронізації HttpClient.Send; навіть якщо поверхня .NET Standard їх не містить, бібліотеку .NET Standard можна використовувати в програмі .NET 5+, щоб кінцеві користувачі мали доступ до методів синхронізації HttpClient.Send.

Обгортання та не обгортання логерів

Коли ви додаєте логер, ви можете явно встановити параметр wrapHandlersPipeline, щоб вказати, чи буде логер

 

– обгортати пайплайн обробників (додано до верхньої частини пайплайну, що відповідає повідомленням № 1, 2, 7 і 8 у розділі «Огляд старого логування» вище)