Не оборачивайте HttpClient в using ── практика HTTP-взаимодействия в бизнес-приложениях на C# (паттерны создания, тайм-ауты, повторные попытки)
· Го Комура · CSharp, .NET, HttpClient, IHttpClientFactory, Сети, async/await, Разработка Windows, Техническая консультация
«После обеда соединения с внешним API начинают падать с SocketException» — «Сменили адрес сервера, а приложение всё равно продолжает стучаться на старый» — вызвать GetAsync у HttpClient в C# просто, но если ошибиться в том, как создавать и хранить экземпляр, легко заложить именно такую проблему: код работает во время разработки, а ломается уже в эксплуатации.
В этой статье, ориентируясь на сценарий, когда Windows-бизнес-приложение обращается к внешним веб-API или внутренним сервисам, мы разберём правильные паттерны создания HttpClient, проектирование тайм-аутов, повторные попытки, обработку ошибок и специфичные для Windows подводные камни — в том порядке, в котором обычно возникают затруднения на практике.
1. Сначала вывод (таблица решений)
То, как правильно держать HttpClient, зависит от архитектуры приложения. Сначала сведём это в таблицу решений.
| Тип приложения | Рекомендуемый паттерн | Причина |
|---|---|---|
| Консольная утилита (.NET), работающая секунды–минуты | Один static/singleton-экземпляр HttpClient |
В короткоживущем процессе проблема смены DNS практически не имеет значения |
| Долгоживущее приложение / служба Windows (.NET, без DI) | static HttpClient + настройка SocketsHttpHandler.PooledConnectionLifetime |
Решает и исчерпание сокетов, и проблему смены DNS |
| Приложение на Generic Host / с DI (.NET) | IHttpClientFactory (AddHttpClient) |
Пул и ротацию обработчиков можно доверить фабрике. Именованные/типизированные клиенты позволяют разделить настройки для разных адресатов |
| Приложение на .NET Framework | Внедрить IHttpClientFactory через пакет Microsoft.Extensions.Http |
В .NET Framework самостоятельное создание клиента легко приводит к исчерпанию портов, и Microsoft официально рекомендует использовать фабрику1 |
| Для разных адресатов различаются настройки прокси, cookie, сертификатов | Заводить отдельный HttpClient под каждую конфигурацию (не переиспользовать) |
Настройки соединения в обработчике нельзя изменить после отправки первого запроса2 |
После этого сформулируем выводы заранее.
- Нельзя вызывать
new HttpClient()при каждом запросе и уничтожать его черезusing.HttpClientсодержит внутренний пул соединений и спроектирован для повторного использования. Если создавать и уничтожать его каждый раз, при высокой нагрузке доступные сокеты закончатся и возникнетSocketException2. - Но и просто сделать его
static— не решение.HttpClientразрешает DNS только при создании соединения, поэтому даже если IP-адрес адресата изменится, он продолжит использовать старое соединение. Официально рекомендуемое решение — ограничивать время жизни соединения черезSocketsHttpHandler.PooledConnectionLifetime1. - Если вы используете Generic Host или DI, доверьте это
IHttpClientFactory. Фабрика сама пулит обработчики и по умолчанию заменяет их каждые 2 минуты, решая одновременно и проблему исчерпания сокетов, и проблему смены DNS3. - Тайм-аут по умолчанию составляет 100 секунд. С точки зрения ощущений пользователя бизнес-приложения это почти неотличимо от «зависания навечно», поэтому задавайте явное значение для каждого адресата исходя из требований.
- Повторные попытки не пишите сами — начните со стандартного обработчика
Microsoft.Extensions.Http.Resilience. Он позволяет внедрить повторные попытки, автоматический выключатель (circuit breaker) и тайм-ауты с проверенными значениями по умолчанию и на уровне архитектуры избежать типичной для самописных циклов повторов проблемы — безусловной повторной отправки неудавшегося POST-запроса с двойной регистрацией в результате4.
2. Почему плохо «создавать через using при каждом запросе» ── исчерпание сокетов
Поскольку HttpClient реализует IDisposable, следующий код на первый взгляд кажется правильным.
// Антипаттерн: создание и уничтожение при каждом запросе
public async Task<string> GetDataAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
Проблема в том, что даже после Dispose сокет на уровне ОС не освобождается немедленно. По спецификации TCP сокет закрывающей стороны некоторое время остаётся в состоянии TIME_WAIT. Пока запросы редки, ничего не происходит, но при росте нагрузки неосвобождённые сокеты накапливаются, и в какой-то момент соединение внезапно перестаёт устанавливаться с SocketException2.
Коварность этой проблемы в том, что она почти никогда не воспроизводится во время разработки и тестирования. Она проявляется в виде отказов только в часы пиковой нагрузки в продакшене или только во время пакетной обработки в конце месяца. При расследовании стоит в момент проявления проблемы проверить количество сокетов в состоянии TIME_WAIT следующей командой — это поможет определить причину.
# Подсчитать количество сокетов в состоянии TIME_WAIT для каждого адресата
netstat -ano | Select-String "TIME_WAIT" | Measure-Object
Отметим, что HttpClient, полученный через IHttpClientFactory, к этой проблеме не относится. У клиента, созданного фабрикой, вызов Dispose не уничтожает обработчик (собственно пул соединений), поэтому его можно спокойно оборачивать в using3.
3. static — это ещё не решение ── проблема смены DNS
Сделать HttpClient static в качестве меры против исчерпания сокетов — правильное направление, но этим одна проблема не решается. HttpClient разрешает DNS только при создании соединения и не учитывает TTL записи DNS2. Пока соединение живёт в пуле, оно продолжает подключаться по старому IP-адресу, даже если адрес назначения изменился.
Именно этот механизм становится причиной сбоев вида «DNS переключили при отказоустойчивом переходе, а приложение продолжало обращаться к старому серверу вплоть до перезапуска». Решение, рекомендуемое официальными руководствами, — ограничивать время жизни соединения через SocketsHttpHandler.PooledConnectionLifetime1.
// Рекомендуемый паттерн для .NET (Core) / .NET 5+:
// Пересоздавая соединение через определённый интервал, отслеживаем изменения DNS
private static readonly HttpClient SharedClient = new(new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
});
Соединение, срок жизни которого истёк, пересоздаётся при следующем запросе, и в этот момент DNS разрешается заново. Значение выбирается исходя из того, насколько быстро вы хотите отслеживать изменения DNS. В примере из официальной документации используется 2 минуты, но для внутренних систем, где адрес назначения меняется редко, вполне допустимо значение и побольше1.
Отметим, что SocketsHttpHandler реализован начиная с .NET Core 2.1, и в .NET Framework недоступен. Для .NET Framework используйте IHttpClientFactory, о котором пойдёт речь в следующей главе1.
4. Если используете DI — IHttpClientFactory
В приложениях, использующих Generic Host или DI-контейнер, IHttpClientFactory (AddHttpClient) — первый кандидат. Описание самого Generic Host см. в статье «Что такое Generic Host», а о внедрении в десктопное приложение — в статье «Использование Generic Host + BackgroundService в десктопном приложении».
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Именованный клиент: разделяем настройки для разных адресатов
builder.Services.AddHttpClient("OrderApi", client =>
{
client.BaseAddress = new Uri("https://order.example.co.jp/");
client.Timeout = TimeSpan.FromSeconds(10);
});
Есть три момента в поведении фабрики, которые важно понимать.
- Обработчики пулятся и по умолчанию заменяются каждые 2 минуты. При каждом вызове
CreateClientвозвращается новыйHttpClient, но обработчик (пул соединений) под капотом общий, поэтому исчерпания сокетов не происходит, а периодическая замена обеспечивает отслеживание изменений DNS3. - Предполагается, что
HttpClient, созданный фабрикой, используется недолго. Если сохранить полученный экземпляр в поле singleton-объекта, он перестанет участвовать в замене обработчиков и не будет отслеживать изменения DNS. По той же причине избегайте внедрения типизированного клиента в singleton-сервис3. - В приложениях, зависящих от cookie, нужна осторожность. Из-за пулинга обработчиков
CookieContainerнепреднамеренно оказывается общим. Если вы используете cookie, официальная рекомендация — либо не использовать фабрику, либо отключить обработку cookie и добавлять заголовки самостоятельно1.
Вопросы получения токена при обращении к защищённым API (например, защищённым Microsoft Entra ID) рассмотрены в статье «Внедрение аутентификации Entra ID в приложения WinForms/WPF» — обратитесь к ней.
5. Проектирование тайм-аутов ── стандартные 100 секунд слишком долго для бизнес-приложения
Значение HttpClient.Timeout по умолчанию составляет 100 секунд5. Для бизнес-приложения, где вызов API происходит в продолжение действий пользователя на экране, ожидание в 100 секунд равносильно «зависанию», поэтому задавайте значение явно для каждого адресата.
// Тайм-аут по умолчанию для всего клиента
client.Timeout = TimeSpan.FromSeconds(10);
// Если нужно сделать короче/длиннее тайм-аут только для конкретного запроса, используйте CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
HttpResponseMessage response = await client.GetAsync(url, cts.Token);
Вот на что стоит обратить внимание при проектировании.
- Исключение при тайм-ауте — это
TaskCanceledException. Начиная с .NET 5, при тайм-ауте, вызванномHttpClient.Timeout, во внутреннее исключение помещаетсяTimeoutException5. Однако при тайм-ауте через собственныйCancellationTokenSource, как в примере выше, внутреннего исключения не будет. Надёжнее различать тайм-аут и отмену пользователем не по внутреннему исключению, а по тому, отменён ли токен, переданный вызывающей стороной (пример кода в главе 7). Если в catch-блоке обрабатывается толькоHttpRequestException, тайм-аут будет упущен — учитывайте это. Timeoutограничивает «весь запрос целиком». Если нужно жёстко ограничить только время установления соединения, дополнительно используйтеSocketsHttpHandler.ConnectTimeout. Требование вида «если сервер недоступен, сдаться через 3 секунды, но в штатном режиме ждать большой ответ до 60 секунд» можно выразить сочетанием этих двух параметров2.- Для загрузки больших файлов избегайте буферизации по умолчанию. По умолчанию
HttpClientзагружает весь ответ в память, поэтому при скачивании файлов размером в десятки мегабайт и больше указывайтеHttpCompletionOption.ResponseHeadersReadи обрабатывайте данные потоково2.
Для конфигурации, при которой значения тайм-аутов выносятся в appsettings.json и меняются в зависимости от окружения, можно напрямую воспользоваться таблицей решений из статьи «Практика управления конфигурацией Windows-бизнес-приложений».
Отметим также, что блокировка через .Result или .Wait() при вызове HTTP-обмена из WinForms/WPF приводит к взаимной блокировке (deadlock) UI-потока. Эта классическая ловушка подробно разобрана в статье «Практическая таблица решений по C# async/await» — рекомендуем прочитать её перед тем, как писать код обмена данными.
6. Повторные попытки ── используйте стандартный resilience-обработчик, а не самописный цикл
Сеть по своей природе иногда даёт временные сбои, поэтому в коде вызова внешних API нужны повторные попытки. Но самописный повтор через цикл for и Task.Delay требует правильно реализовать самостоятельно всё нижеперечисленное, а это того не стоит.
- Различение ошибок, при которых повтор допустим (тайм-аут, HTTP 408/429/5xx), и ошибок, при которых повтор бесполезен (HTTP 400/401/404)
- Исключение HTTP-методов, повтор которых опасен (например, двойное выполнение регистрации через POST)
- Экспоненциальная задержка и джиттер (случайный разброс, предотвращающий одновременную повторную отправку запросов всеми клиентами и повторную перегрузку сервера)
- Автоматический выключатель (circuit breaker), останавливающий сами вызовы при продолжающемся сбое
Стандартный обработчик из пакета Microsoft.Extensions.Http.Resilience предоставляет весь этот набор с проверенными значениями по умолчанию4.
builder.Services.AddHttpClient("OrderApi", client =>
{
client.BaseAddress = new Uri("https://order.example.co.jp/");
})
.AddStandardResilienceHandler(); // Стандартный набор: повторные попытки + автоматический выключатель (circuit breaker) + тайм-аут
Значения по умолчанию для стандартного обработчика: тайм-аут всего запроса — 30 секунд, до 3 повторных попыток с экспоненциальной задержкой (начальная задержка 2 секунды, с джиттером), тайм-аут на каждую попытку — 10 секунд, а HTTP 408/429/5xx и HttpRequestException трактуются как временные ошибки4.
Единственный момент, на который стоит обратить внимание в значениях по умолчанию: по умолчанию стандартный обработчик повторяет запросы для всех HTTP-методов. Для API, где двойное выполнение регистрирующего POST-запроса нежелательно, отключите повторные попытки для небезопасных методов4.
httpClientBuilder.AddStandardResilienceHandler(options =>
{
// Отключаем повтор для POST/PUT/DELETE и т.п.
options.Retry.DisableForUnsafeHttpMethods();
});
Отметим, что повторные попытки решают лишь временные сбои. Симптомы вроде «соединение установлено, но данные не идут» или «ответ приходит крайне медленно» часто связаны с проблемами на уровне TCP, и для их диагностики может пригодиться подход, описанный в статье «TCP-ретрансмиссии и RFC1323 ── расследование остановки связи с промышленной камерой».
7. Обработка ошибок ── как обрабатывать коды состояния
HttpClient не выбрасывает исключение при таких «неудачах, для которых HTTP-ответ всё же получен», как HTTP 404 или 500. Исключение возникает только тогда, когда сам ответ не был получен: сбой соединения, тайм-аут, отмена. Код нужно писать, осознанно разделяя эти два случая.
try
{
using HttpResponseMessage response = await client.GetAsync(url, ct);
if (!response.IsSuccessStatusCode)
{
// Ответ получен, но неуспешен: можно ветвиться по коду состояния
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null; // Пример, когда «не существует» обрабатывается как штатный случай
}
response.EnsureSuccessStatusCode(); // Остальное преобразуется в HttpRequestException
}
return await response.Content.ReadFromJsonAsync<Order>(ct);
}
catch (HttpRequestException ex)
{
// Сбой соединения либо неуспешный статус от EnsureSuccessStatusCode.
// Начиная с .NET 5 код состояния при ошибке доступен через ex.StatusCode
logger.LogError(ex, "Не удалось вызвать API заказов. StatusCode={StatusCode}", ex.StatusCode);
throw;
}
catch (TaskCanceledException) when (ct.IsCancellationRequested)
{
// Отмена по токену, переданному вызывающей стороной (например, закрытие окна).
// Это не ошибка, поэтому не засоряем лог и просто пробрасываем исключение дальше
throw;
}
catch (TaskCanceledException ex)
{
// Истечение HttpClient.Timeout либо собственного CTS для тайм-аута
logger.LogError(ex, "Истёк тайм-аут при вызове API заказов");
throw;
}
Решение вида «трактовать 404 как исключение или как null» зависит от семантики конкретного API. Если превращать всё подряд в исключение одним вызовом EnsureSuccessStatusCode, catch-блоки на стороне вызывающего кода разрастаются. Подход к тому, что делать исключением, а что выражать через возвращаемое значение, подробно описан в статье «Практика catch исключений, логирования и обработки ошибок» — его можно применить как есть.
Для отправки и получения JSON использование GetFromJsonAsync / PostAsJsonAsync / ReadFromJsonAsync из System.Net.Http.Json избавляет от необходимости писать сериализацию через строки вручную.
8. Особенности и подводные камни, специфичные для Windows-бизнес-приложений
Напоследок соберём подводные камни, в которые часто попадают на практике в среде Windows.
- Из-за автоопределения прокси первый запрос выполняется медленно. По умолчанию в Windows
HttpClientиспользует настройки прокси ОС (включая автоопределение). Если известно, что прокси не нужен, отключение черезHttpClientHandler.UseProxy = falseубирает ожидание обнаружения2. И наоборот, в средах, где корпоративный прокси обязателен, явное указание черезWebProxyпомогает избежать ситуации «работает на машине разработчика, но не работает на сервере». - Настройки прокси нужно завершить до первого запроса. Изменения настроек соединения в обработчике после отправки первого запроса не применяются2.
- Значения по умолчанию для количества одновременных соединений в .NET и .NET Framework противоположны. В .NET (
SocketsHttpHandler) количество одновременных соединений HTTP/1.1 по умолчанию не ограничено, поэтому при большом количестве параллельных запросов число соединений продолжает расти и может упереться в ограничения межсетевого экрана или сервера. Для операций с высокой степенью параллелизма задавайте предел черезMaxConnectionsPerServer2. В .NET Framework, напротив, значениеServicePointManager.DefaultConnectionLimitпо умолчанию мало — 2 (вне ASP.NET-окружения), из-за чего параллельные запросы внутренне ожидают друг друга и возникает противоположная проблема — тайм-аут. Если в .NET Framework нужно повысить степень параллелизма, явно увеличьте этот предел6. - При вызове из службы Windows контекст прокси и TLS отличается от пользовательского. У учётной записи, от имени которой выполняется служба, нет пользовательских настроек прокси и учётных данных, что становится типичной причиной сбоев связи вида «работает у интерактивного пользователя, но не работает в службе». О специфике контекста выполнения службы см. статью «Создание и эксплуатация служб Windows».
- Не встраивайте адреса и API-ключи в код. Переключение адресатов выносите в конфигурационные файлы (см. «Практика управления конфигурацией»), а хранение секретов организуйте способом из статьи «Избегание хранения секретов в открытом виде с помощью DPAPI».
Итог
На практике качество работы с HttpClient определяется не столько тем, «как вызывать», сколько тем, «как его держать». Создание при каждом запросе приводит к исчерпанию сокетов, а наивное превращение в static — к тому, что клиент перестаёт отслеживать смену DNS, и оба этих эффекта незаметны во время разработки. Ответ для .NET — общий экземпляр с PooledConnectionLifetime либо IHttpClientFactory, а для .NET Framework — внедрение IHttpClientFactory. После этого нужно явно задать тайм-ауты для каждого адресата и доверить повторные попытки стандартному resilience-обработчику — только тогда получится бизнес-приложение, изначально рассчитанное на то, что сеть иногда даёт сбои.
Пересмотр коммуникационной части существующего приложения (связь, которая падает только в часы пик, упорядочивание проектирования тайм-аутов, разработка новой интеграции с внешним API) часто требует решения с учётом конкретного кода и условий эксплуатации, поэтому, если сомневаетесь, обращайтесь за консультацией.
Связанные статьи
- Практическая таблица решений по C# async/await
- Что такое Generic Host
- Использование Generic Host + BackgroundService в десктопном приложении
- Не только appsettings.json ── практика управления конфигурацией Windows-бизнес-приложений
- Внедрение аутентификации Entra ID в приложения WinForms/WPF
- TCP-ретрансмиссии и RFC1323 ── расследование остановки связи с промышленной камерой
- Заблуждение о том, что TCP позволяет принимать данные теми же порциями, какими они были отправлены
- Практика catch исключений, логирования и обработки ошибок
- Хранение секретов в Windows-приложениях - избегаем открытых настроек с помощью DPAPI
Похожие направления консультаций
Komura Soft LLC занимается разработкой Windows-бизнес-приложений, включая интеграцию с внешними API, а также технической консультацией по расследованию проблем связи в существующих приложениях (исчерпание сокетов, тайм-ауты, периодические сбои соединения) и выработке плана их устранения.
Источники
-
Microsoft Learn, Guidelines for using HttpClient. О том, что в .NET Core/.NET 5+ следует использовать либо долгоживущий клиент с настроенным
PooledConnectionLifetime, либо короткоживущий клиент, полученный черезIHttpClientFactory; что в .NET Framework рекомендуется использоватьIHttpClientFactory; и что в приложениях, использующих cookie, следует избегатьIHttpClientFactoryиз-за общегоCookieContainer. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn, HttpClient Class. О том, что создание клиента при каждом запросе приводит к исчерпанию сокетов и
SocketException; что DNS разрешается только при создании соединения без учёта TTL; что настройки соединения в обработчике нельзя изменить после первого запроса; что количество одновременных соединений HTTP/1.1 по умолчанию не ограничено; о рекомендации использовать потоковую обработку для больших загрузок; а также о поведении прокси по умолчанию и его отключении черезUseProxy. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Microsoft Learn, IHttpClientFactory with .NET. О том, что срок жизни обработчика по умолчанию составляет 2 минуты; что клиент, созданный фабрикой, рассчитан на короткоживущее использование; что вызов
Disposeу клиента фабрики не уничтожает обработчик; и что внедрение типизированного клиента в singleton-сервис приводит к тому, что он перестаёт отслеживать изменения DNS. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Build resilient HTTP apps: Key development patterns. О пяти стратегиях, настраиваемых
AddStandardResilienceHandler(ограничитель скорости / общий тайм-аут 30 секунд / до 3 повторных попыток с экспоненциальной задержкой / автоматический выключатель / тайм-аут попытки 10 секунд), об обрабатываемых кодах состояния (408/429/5xx) и исключениях, а также об отключении повторных попыток для POST и подобных методов черезDisableForUnsafeHttpMethods. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, HttpClient.Timeout Property. О том, что значение по умолчанию составляет 100 секунд, и что начиная с .NET 5 при тайм-ауте выбрасывается
TaskCanceledExceptionс вложеннымTimeoutException. ↩ ↩2 -
Microsoft Learn, ServicePointManager.DefaultConnectionLimit Property. О том, что значение по умолчанию для количества одновременных соединений составляет 10 для приложений, размещённых в ASP.NET, и 2 — для остальных (например, десктопных приложений). ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Интернационализация приложений WinForms/WPF — resx, сателлитные сборки и переключение культуры на практике
Разбираем интернационализацию десктопных приложений Windows на практике: различие между CurrentCulture и CurrentUICulture, устройство рес...
Не только appsettings.json — практическое руководство по управлению конфигурацией Windows-бизнес-приложений (настройки окружений, секреты и место записи)
Практический разбор управления конфигурацией Windows-бизнес-приложений: слои appsettings.json, выбор между IConfiguration и семейством IO...
Печать и вывод PDF в Windows-бизнес-приложениях — выбор между System.Drawing.Printing, WPF и библиотеками отчётов
В виде таблицы выбора по требованиям собраны печать WinForms через PrintDocument, печать WPF через FlowDocument/FixedDocument и варианты ...
CSV — не «просто текст»: практика работы с CSV в бизнес-приложениях на C# (кодировки, совместимость с Excel, защита от инъекций)
Разбираем типичные ошибки при вводе-выводе CSV в бизнес-приложениях — самописный парсинг через Split(','), искажение текста в Excel из-за...
Как точно найти причину «медленной работы» с PerfView и dotnet-trace — практическое введение в анализ производительности .NET
Когда бизнес-приложение «работает медленно», «загружает ЦП под завязку» или «иногда зависает», какой инструмент выбрать и на что смотреть...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Поток UI и таймеры
Поток UI WPF / WinForms, асинхронные операции, Dispatcher и проектирование таймеров.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Потому что проектирование и реализация интеграции с внешними API и HTTP-взаимодействия входят в область практических консультаций по разработке Windows-приложений.
Технические консультации и ревью дизайна
Потому что пересмотр проблем связи в существующих приложениях (исчерпание сокетов, проектирование тайм-аутов) относится к технической консультации, включающей ревью архитектуры.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- HttpClient реализует IDisposable — значит, его нельзя оборачивать в using?
- Проблема не в самом using, а в использовании по схеме «создать и уничтожить при каждом запросе». HttpClient содержит внутри пул соединений и спроектирован так, чтобы повторно использоваться на протяжении всего времени жизни приложения. Если создавать и уничтожать его при каждом запросе, сокеты некоторое время остаются в состоянии TIME_WAIT даже после Dispose, и при высокой нагрузке сокеты заканчиваются, что приводит к SocketException. Уничтожайте клиент один раз при завершении приложения либо получайте его через IHttpClientFactory. HttpClient, полученный через фабрику, можно спокойно оборачивать в using — вызов Dispose не уничтожает сам обработчик (handler).
- Стоит ли переходить с WebClient и HttpWebRequest из .NET Framework?
- Для нового кода рекомендуется использовать только HttpClient. WebClient и HttpWebRequest — устаревшие API, оставленные ради обратной совместимости, и Microsoft тоже рекомендует использовать HttpClient при новой разработке. Полностью переписывать существующий код не обязательно, но при каждой доработке коммуникационной части имеет смысл постепенно переходить на HttpClient — это облегчит контроль тайм-аутов, поддержку async и тестируемость.
- Сколько раз и с каким интервалом делать повторные попытки?
- Безопаснее не изобретать значения самостоятельно, а взять за отправную точку значения по умолчанию стандартного обработчика Microsoft.Extensions.Http.Resilience (максимум 3 попытки, экспоненциальная задержка с джиттером, начальная задержка 2 секунды). Важнее не количество попыток, а то, можно ли вообще повторять данный запрос: операции вроде POST, повтор которых может привести к двойной регистрации, по умолчанию следует отключать от повторных попыток либо включать их только после того, как на стороне сервера обеспечена идемпотентность (свойство, при котором повторный приём одного и того же запроса не меняет результат).
- Почему в корпоративной прокси-среде только первый запрос выполняется крайне медленно?
- По умолчанию в Windows HttpClient пытается автоматически определить прокси, поэтому при первом подключении может уходить время на обнаружение. В средах, где прокси заведомо не нужен (например, взаимодействие внутри сервера), стоит отключить автоопределение, установив HttpClientHandler.UseProxy в false — это улучшает ситуацию. И наоборот, в корпоративных средах, где прокси обязателен, работа стабильнее, если задавать его явно через WebProxy, не полагаясь на автоопределение.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки