Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Риваль Абдрахманов «HttpClient: основные ошибки и способы как их избежать»

DotNetRu
October 03, 2019

Риваль Абдрахманов «HttpClient: основные ошибки и способы как их избежать»

HttpClient является неотъемлимой частью большинаства современных приложений, поэтому правильность его использования напрямую влияет на производительность. В докладе рассмотрим самые роспространённые ошибки, связанные с HttpClient, способы эти ошибки избежать, а также инструменты, которые были добавлены в новые версии платформы, облегчающие жизнь разработчикам.

DotNetRu

October 03, 2019
Tweet

More Decks by DotNetRu

Other Decks in Programming

Transcript

  1. HttpClient основные ошибки и способы их избежать Риваль Абдрахманов Positive

    Technologies SpbDotNet, 2019 Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 1 / 90
  2. Содержание 1 HttpClient - базовая информация 2 Неочевидные проблемы 3

    Интерфейс IHttpClientFactory 4 Дополнительные улучшения в .NET Core 2.1 5 HttpRequestMessage и HttpResponseMessage 6 Новое в .NET Core 3.0 Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 2 / 90
  3. HttpClient Class Базовый класс для отправки HTTP-запросов и получения HTTP-ответов;

    GetAsync(. . .), PostAsync(. . .), SendAsync(. . .) и др.; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 4 / 90
  4. HttpClient Class Базовый класс для отправки HTTP-запросов и получения HTTP-ответов;

    GetAsync(. . .), PostAsync(. . .), SendAsync(. . .) и др.; HttpClient реализует IDisposable. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 4 / 90
  5. IDisposable Interface Предоставляет механизм для освобождения неуправляемых ресурсов; public void

    Dispose(); Конструкция using(. . .); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 5 / 90
  6. IDisposable Interface Предоставляет механизм для освобождения неуправляемых ресурсов; public void

    Dispose(); Конструкция using(. . .); Диспозится → диспозь Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 5 / 90
  7. IDisposable Interface Предоставляет механизм для освобождения неуправляемых ресурсов; public void

    Dispose(); Конструкция using(. . .); Диспозится → диспозь Диспозится → будь внимательней Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 6 / 90
  8. Disposable HttpClient Мотивация: using(var client = new HttpClient ()) {

    var response = await client.GetStringAsync (...); } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 7 / 90
  9. Проблема socket exhaustion for(int i = 0; i < 10;

    i++) { using (var client = new HttpClient ()) { await client .GetStringAsync("https :// api.github.com"); } } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 10 / 90
  10. Проблема socket exhaustion 10 сокетов в состоянии TIME WAIT; Приводит

    к SocketException; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 13 / 90
  11. Проблема socket exhaustion 10 сокетов в состоянии TIME WAIT; Приводит

    к SocketException; Актуально не только для Windows и .NET Framework. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 13 / 90
  12. Проблема socket exhaustion “HttpClient предназначен для однократного создания экземпляра и

    повторного использования в течение всего жизненного цикла приложения.” https://docs.microsoft.com/en- us/dotnet/api/system.net.http.httpclient Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 14 / 90
  13. Проблема socket exhaustion Решение проблемы - переиспользование клиента: public static

    HttpClient Client = new HttpClient (); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 15 / 90
  14. Проблема кеширования DNS Не учитываются изменения DNS; Соединение не закрывается

    длительное время. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 18 / 90
  15. Решение при балансировщике Решение для .NET Framework: Класс ServicePointManager; ServicePointManager.DnsRefreshTimeout

    (2 мин); ServicePoint.ConnectionLeaseTimeout (не ограничено); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 19 / 90
  16. Решение при балансировщике Решение для .NET Framework: Класс ServicePointManager; ServicePointManager.DnsRefreshTimeout

    (2 мин); ServicePoint.ConnectionLeaseTimeout (не ограничено); ServicePoint.MaxIdleTime (100 сек). Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 19 / 90
  17. Проблема кеширования DNS Пример: ServicePointManager.DnsRefreshTimeout = 60000; var sp =

    ServicePointManager .FindServicePoint( new Uri("https :// api.github.com")); sp.ConnectionLeaseTimeout = 60000; sp.MaxIdleTime = 60000; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 20 / 90
  18. Quiz var client = new HttpClient (); var tasks =

    new List <Task >(); for (var i = 0; i < 20; i++) { tasks.Add(client .GetStringAsync("https :// api.github.com")); } await Task.WhenAll(tasks); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 21 / 90
  19. Лимит одновременных соединений Лимит по умолчанию равен 2; ServicePointManager.DefaultConnectionLimit; Для

    localhost по умолчанию равен int.MaxValue; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 24 / 90
  20. Лимит одновременных соединений Лимит по умолчанию равен 2; ServicePointManager.DefaultConnectionLimit; Для

    localhost по умолчанию равен int.MaxValue; Только для .NET Framework. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 24 / 90
  21. Выводы Нельзя на каждый запрос переоткрывать соединение; Хочется переиспользовать соединение;

    Нельзя постоянно держать соединение открытым; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 25 / 90
  22. Выводы Нельзя на каждый запрос переоткрывать соединение; Хочется переиспользовать соединение;

    Нельзя постоянно держать соединение открытым; Вручную управлять сложно. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 25 / 90
  23. IHttpClientFactory Позволяет создавать и конфигурировать HttpClient; Был добавлен в ASP.NET

    Core 2.1; Для консольного приложения необходимо добавить Microsoft.Extensions.Hosting и Microsoft.Extensions.Http Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 27 / 90
  24. IHttpClientFactory Добавление в конструктор с помощью DI: public GitHubService(IHttpClientFactory clientFactory)

    { _clientFactory = clientFactory; } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 29 / 90
  25. IHttpClientFactory Создание клиента: var client = _clientFactory.CreateClient (); var response

    = await client.SendAsync(request); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 30 / 90
  26. Named clients Регистрация через метод расширения IServiceCollection: services.AddHttpClient("github", c =>

    { c.BaseAddress = new Uri("https :// api.github.com"); c.DefaultRequestHeaders .Add("Accept", "application/json"); }); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 31 / 90
  27. Named clients Добавление в конструктор с помощью DI: public SomeService(IHttpClientFactory

    clientFactory) { _clientFactory = clientFactory; } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 32 / 90
  28. Typed clients Класс типизированного клиента: public class GitHubClient { ...

    } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 34 / 90
  29. Typed clients private readonly HttpClient _client; public GitHubClient(HttpClient client) {

    client.BaseAddress = new Uri("https :// api.github.com"); client.DefaultRequestHeaders.Add("Accept", "application/json"); _client = client; } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 35 / 90
  30. Typed clients public async Task <Repo > GetRepo(string repo) {

    var response = await _client .GetAsync($"/repos/octocat /{repo}"); ... return repository; } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 36 / 90
  31. Typed clients Добавление в конструктор через DI: public GitHubService(GitHubClient gitHubClient)

    { _gitHubClient = gitHubClient; } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 38 / 90
  32. Refit public interface IGitHubClient { [Get("/repos/octocat /{repo}")] Task <Repo >

    GetRepo(string repo); } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 40 / 90
  33. Refit Регистрация клиента: services .AddRefitClient <IGitHubClient >() .ConfigureHttpClient(c => c.BaseAddress

    = new Uri("https :// api.github.com")); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 41 / 90
  34. Refit Добавление в конструктор через DI: public GitHubService(IGitHubClient gitHubClient) {

    _gitHubClient = gitHubClient; } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 42 / 90
  35. Создание HttpClient public class HttpClient : HttpMessageInvoker public class HttpMessageInvoker

    : IDisposable private HttpMessageHandler _handler; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 44 / 90
  36. Конструкторы HttpMessageInvoker public HttpMessageInvoker(HttpMessageHandler handler , bool disposeHandler) public HttpMessageInvoker(HttpMessageHandler

    handler) : this(handler , true) Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 45 / 90
  37. Конструкторы HttpClient public HttpClient(HttpMessageHandler handler , bool disposeHandler) : base(handler

    , disposeHandler) public HttpClient(HttpMessageHandler handler) : this(handler , true) public HttpClient () : this(( HttpMessageHandler) new HttpClientHandler ()) Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 46 / 90
  38. Конструкторы HttpClient 3 конструктора; Есть возможность передать HttpMessageHandler; В стандартном

    конструкторе disposeHandler равен true. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 47 / 90
  39. Dispose HttpClient protected override void Dispose(bool disposing) { if (disposing

    && !this._disposed) { this._disposed = true; this._pendingRequestsCts.Cancel (); this._pendingRequestsCts.Dispose (); } base.Dispose(disposing); } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 48 / 90
  40. Dispose HttpMessageInvoker protected virtual void Dispose(bool disposing) { if (!

    disposing || this._disposed) return; this._disposed = true; if (!this._disposeHandler) return; this._handler.Dispose (); } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 49 / 90
  41. Dispose HttpClient Отменяются все запросы ⇒ могут отменяться чужие запросы;

    Не стоит шарить HttpClient; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 50 / 90
  42. Dispose HttpClient Отменяются все запросы ⇒ могут отменяться чужие запросы;

    Не стоит шарить HttpClient; После Dispose использовать нельзя; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 50 / 90
  43. Dispose HttpClient Отменяются все запросы ⇒ могут отменяться чужие запросы;

    Не стоит шарить HttpClient; После Dispose использовать нельзя; Dispose вызывается у HttpMessageHandler только в случае disposeHandler = true. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 50 / 90
  44. IHttpClientFactory public static HttpClient CreateClient( this IHttpClientFactory factory) { return

    factory .CreateClient(DefaultName); } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 51 / 90
  45. DefaultHttpClientFactory public HttpClient CreateClient(string name) { HttpClient httpClient = new

    HttpClient(this.CreateHandler(name), false); return httpClient; } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 52 / 90
  46. DefaultHttpClientFactory public HttpMessageHandler CreateHandler( string name) { ActiveHandlerTrackingEntry entry =

    this._activeHandlers.GetOrAdd(name , this._entryFactory).Value; this.StartHandlerEntryTimer(entry); return (HttpMessageHandler) entry.Handler; } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 53 / 90
  47. Создание с помощью HttpClientFactory Dispose не трогает HttpMessageHandler; HttpClientFactory переиспользует

    Handler; HttpClientFactory выставляет таймер для Handler; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 54 / 90
  48. Создание с помощью HttpClientFactory Dispose не трогает HttpMessageHandler; HttpClientFactory переиспользует

    Handler; HttpClientFactory выставляет таймер для Handler; Получаем переиспользование соединений. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 54 / 90
  49. Выводы HttpClientFactory, Named clients, Typed clients, Refit; Переиспользуют HttpMessageHandler; Закрывают

    HttpMessageHandler спустя время. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 55 / 90
  50. DelegatingHandler DelegatingHandler позволяют создать цепочку обработки исходящих запросов; Схоже с

    middleware в ASP.NET Core; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 57 / 90
  51. DelegatingHandler DelegatingHandler позволяют создать цепочку обработки исходящих запросов; Схоже с

    middleware в ASP.NET Core; Функциональность была, но с IHttpClientFactory стало проще использовать. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 57 / 90
  52. Создание DelegatingHandler public class SomeHandler : DelegatingHandler { override SendAsync

    (...) { ... var response = await base.SendAsync( request , cancellationToken); ... } } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 59 / 90
  53. Пример override SendAsync (...) { var sw = Stopwatch.StartNew ();

    var resp = await base.SendAsync(request , ct); sw.Stop(); } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 60 / 90
  54. Пример override SendAsync (...) { if (! request.Headers.Contains("Key")) { return

    new HttpResponseMessage (); } return await base.SendAsync(request , ct); } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 61 / 90
  55. Пример override SendAsync (...) { return new HttpResponseMessage (); }

    Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 62 / 90
  56. Регистрация DelegatingHandler services.AddHttpClient("github") // first .AddHttpMessageHandler <OutsideHandler >() // second

    .AddHttpMessageHandler <InsideHandler >() Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 63 / 90
  57. Polly Подходит не только для HttpClient; Содержит различные политики: Retry,

    Circuit Breaker, Timeout, . . . Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 65 / 90
  58. Polly Подходит не только для HttpClient; Содержит различные политики: Retry,

    Circuit Breaker, Timeout, . . . Необходимо установить Microsoft.Extensions.Http.Polly. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 65 / 90
  59. Добавление политик Обработка всех ответов со статус кодами 5xx и

    408 services.AddHttpClient("github") .AddTransientHttpErrorPolicy(p => p.RetryAsync (3)) .AddTransientHttpErrorPolicy( p => p.CircuitBreakerAsync (5, TimeSpan.FromSeconds (30))); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 66 / 90
  60. Настройка внутреннего HttpMessageHandler services.AddHttpClient("github") .ConfigurePrimaryHttpMessageHandler (() => { return new

    SocketsHttpHandler () { AutomaticDecompression = DecompressionMethods.GZip }; }); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 67 / 90
  61. HttpRequestMessage Представляет сообщение HTTP-запроса. HttpMethod method; Uri requestUri; HttpRequestHeaders headers;

    Version version, значение по умолчанию Version(2, 0); HttpContent content, который является IDisposable; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 72 / 90
  62. Диспозить или нет? 1 Не диспозить var req = new

    HttpRequestMessage (); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 73 / 90
  63. Диспозить или нет? 1 Не диспозить var req = new

    HttpRequestMessage (); 2 Диспозить using(var req = new HttpRequestMessage ()) { ... } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 73 / 90
  64. Диспозить или нет? Зависит от HttpContent; Чаще всего это StringContent

    ⇒ можно не диспозить∗. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 74 / 90
  65. HttpResponseMessage Представляет ответное сообщение HTTP. HttpStatusCode statusCode (есть проверка value

    > 0 и value < 999); HttpResponseHeaders headers; string reasonPhrase HttpRequestMessage requestMessage Version version, значение по умолчанию Version(1, 1); HttpContent content, который является IDisposable; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 75 / 90
  66. Диспозить или нет? 1 Не диспозить var resp = await

    client.GetAsync (...); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 76 / 90
  67. Диспозить или нет? 1 Не диспозить var resp = await

    client.GetAsync (...); 2 Диспозить using(var resp = await client.GetAsync (...)) { ... } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 76 / 90
  68. Диспозить или нет? Для GetAsync и SendAsync; HttpCompletionOption: ResponseContentRead, ResponseHeadersRead;

    Если ResponseContentRead, то данные сохраняются в MemoryStream ⇒ можно без диспоза∗; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 77 / 90
  69. Диспозить или нет? Для GetAsync и SendAsync; HttpCompletionOption: ResponseContentRead, ResponseHeadersRead;

    Если ResponseContentRead, то данные сохраняются в MemoryStream ⇒ можно без диспоза∗; Иначе стоит диспозить. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 77 / 90
  70. ∗Всё-таки диспозить или нет? Это внутрення реализация, которая может измениться;

    Лучше всегда диспозить; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 78 / 90
  71. ∗Всё-таки диспозить или нет? Это внутрення реализация, которая может измениться;

    Лучше всегда диспозить; using declarations. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 78 / 90
  72. Using declarations using var req = new HttpRequestMessage (); using

    var resp = await client.GetAsync (...) Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 79 / 90
  73. Антипаттерн чтения в строку var response = await client.GetAsync("/repos/octocat/Hello -World");

    var str = await response.Content.ReadAsStringAsync (); var repository = JsonConvert.DeserializeObject <Repo >(str); return repository; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 80 / 90
  74. Десериализуем из stream var srz = new JsonSerializer (); var

    response = await client.GetAsync("/repos/octocat/Hello -World"); var stream = await response.Content.ReadAsStreamAsync (); using (var sr = new StreamReader(stream)) using (var jsonReader = new JsonTextReader(sr)) { return srz.Deserialize <Repo >( jsonReader); } Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 81 / 90
  75. Десериализуем из stream в .net core 3.0 var response =

    await client.GetAsync("/repos/octocat/Hello -World"); var stream = await response.Content.ReadAsStreamAsync (); var repository = await JsonSerializer.DeserializeAsync <Repo >( stream); return repository; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 82 / 90
  76. Выводы HttpRequestMessage и HttpResponseMessage лучше объявлять, используя using declarations; Лучше

    не использовать промежуточную строку при десериализации. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 83 / 90
  77. Поддержка HTTP/2 using (var request = new HttpRequestMessage(HttpMethod.Get , "/")

    { Version = new Version(2, 0) }) Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 85 / 90
  78. Поддержка HTTP/2 var client = new HttpClient () { BaseAddress

    = new Uri("https :// localhost :80"), DefaultRequestVersion = new Version(2, 0) }; Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 86 / 90
  79. Регистрация gRPC Client Схожий шаблон с HttpClient: services.AddGrpcClient <GreeterClient >(

    options => { options.BaseAddress = new Uri("https :// localhost :5001"); }); Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 87 / 90
  80. Общий вывод В .NET Framework используйте ServicePointManager; В .NET Core

    используйте IHttpClientFactory. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 88 / 90
  81. Ссылки You’re using HttpClient wrong and it is destabilizing your

    software; Singleton HttpClient? Beware of this serious behaviour and how to fix it; Beware of the .NET HttpClient; Подводные камни HttpClient в .NET; Make HTTP requests using IHttpClientFactory in ASP.NET Core; Steve Gordon https://www.stevejgordon.co.uk. Риваль Абдрахманов (PT) HttpClient SpbDotNet, 2019 89 / 90