Можно ли использовать один экземпляр HttpClient для каждого хоста, с которым должно общаться мое приложение? - PullRequest
2 голосов
/ 01 ноября 2019

Я знаю, что при использовании контейнера внедрения зависимостей Microsoft для обработки экземпляров HttpClient рекомендуется использовать интерфейс IHttpClientFactory , предоставляемый пакетом nuget Microsoft.Extensions.Http .

К сожалению, классы, реализующие интерфейс IHttpClientFactory , не являются общедоступными (, как вы можете проверить здесь ), поэтому единственный способ использовать этот шаблон - использовать внедрение зависимостей Microsoftконтейнер (по крайней мере, это единственный, который я знаю). Иногда мне нужно поддерживать старые приложения, используя другой контейнер, поэтому мне нужно найти наилучшую практику, даже если подход IHttpClientFactory не может быть использован.

Как объяснено в этой знаменитой статье и подтверждено также в документах Microsoft класс HttpClient предназначен для создания экземпляра один раз за время существования приложения и повторного использования в нескольких вызовах HTTP. Это можно безопасно сделать, потому что открытые методы, используемые для выполнения HTTP-вызовов , задокументированы как поточно-ориентированные , поэтому одноэлементный экземпляр можно безопасно использовать. В этом случае важно следовать советам, данным в этой статье , чтобы избежать проблем, связанных с изменениями DNS.

Пока все хорошо.

Иногда этоудобно использовать такие свойства, как BaseAddress или DefaultRequestHeaders , которые не являются поточно-ориентированными (по крайней мере, они не документированы как поточно-ориентированные, поэтому я предполагаю, что они не безопасны) для настройкиЭкземпляр HttpClient.

Это открывает вопрос: что произойдет, если у меня есть одноэлементный экземпляр HttpClient и где-то в моем коде я использую свойство DefaultRequestHeaders , чтобы установить некоторые общие заголовки HTTP-запроса, полезные для вызова одного из хостовмое приложение должно общаться? Это потенциально опасно, потому что разные хосты могут требовать разные значения для одного и того же заголовка запроса (пример аутентификации можно представить в качестве примера). Кроме того, изменение DefaultRequestHeaders одновременно из двух потоков может потенциально испортить внутреннее состояние экземпляра HttpClient из-за отсутствия гарантий безопасности потоков.

По всем этим причинам я думаю, чтонаилучший подход к использованию HttpClient (когда IServiceCollection недоступен) заключается в следующем:

  • создать по одному экземпляру HttpClient для каждого хоста, с которым приложение должно взаимодействовать с . Каждый вызов одного конкретного хоста будет использовать один и тот же экземпляр HttpClient . Параллельные вызовы к одному и тому же хосту безопасны из-за задокументированной безопасности потоков, используемых для выполнения вызовов.

  • создать по одной службе для каждого хоста, с которым приложение должно взаимодействовать . Экземпляр HttpClient внедряется внутри этого сервиса, а сам сервис используется как приложение в приложении. Этот сервис используется для отвлечения доступа к хосту, с которым он связан. Подобные классы полностью тестируемы , как показано здесь .

  • единственная точка, где создаются и настраиваются экземпляры HttpClient, - это корень композиции приложения. Код в корне композиции является однопоточным, поэтому можно безопасно использовать такие свойства, как DefaultRequestHeaders для настройки экземпляров HttpClient.

Вы видите какие-либо проблемы при созданииодин экземпляр HttpClient на хост должен быть вызван?

Я знаю, что создание одного HttpClient на запрос может привести к исчерпанию сокета , и его следует избегать, но я предполагаю, что наличие одного экземпляра на хост безопасно в отношенииэта проблема (потому что один и тот же экземпляр используется для всех запросов к одному и тому же хосту, и я не ожидаю, что одно приложение должно общаться с большим количеством разных хостов).

Согласны ли вы? Я что-то упустил?

1 Ответ

3 голосов
/ 01 ноября 2019

Я знаю, что при использовании контейнера внедрения зависимостей Microsoft для обработки экземпляров HttpClient рекомендуется использовать интерфейс IHttpClientFactory, предоставляемый пакетом nuget Microsoft.Extensions.Http.

Правильно.

К сожалению, классы, реализующие интерфейс IHttpClientFactory, не являются общедоступными (как вы можете проверить здесь), поэтому единственный способ использовать этот шаблон - использовать контейнер внедрения зависимостей Microsoft (по крайней мере, это единственныйтот, который я знаю). Иногда мне нужно поддерживать старые приложения, используя другой контейнер, поэтому мне нужно найти лучшие практики, даже если подход IHttpClientFactory не может быть использован.

Microsoft.Extensions.DependencyInjection ("MEDI") следует подумать(упрощенной) абстракции над несколькими системами DI - так уж получилось, что он поставляется с собственным базовым контейнером DI. Вы можете использовать MEDI в качестве интерфейса для Unity, SimpleInject, Ninject и др.

Как объяснено в этой известной статье и подтверждено также в документах Microsoft, класс HttpClient предназначен для одного экземпляра. за время жизни приложения и повторно используется в нескольких вызовах HTTP.

Не совсем.

  • Вам не нужно, чтобы singleton HttpClient использовался всемипотребители HttpClient в вашем приложении, потому что разные потребители могут иметь разные предположения относительно (как вы позже укажете) DefaultRequestHeaders и других HttpClient состояний. Некоторый код может также предполагать, что HttpClient также не использует экземпляры DelegatingHandler.
  • Вам также не нужны экземпляры HttpClient (созданные с использованием собственного конструктора без параметров) с неограниченным временем жизни, потому чтоо том, как его внутренняя HttpClientHandler по умолчанию обрабатывает (или, скорее, не обрабатывает) изменения DNS. Следовательно, по умолчанию IHttpClientFactory накладывает ограничение продолжительности жизни 2 минуты для каждого экземпляра HttpClientHandler.

Это открывает вопрос: что произойдет, если у меня будет одноэлементный экземпляр HttpClient и где-нибудь вмой код Я использую свойство DefaultRequestHeaders, чтобы установить некоторые распространенные заголовки HTTP-запроса, полезные для вызова одного из хостов, с которыми должно взаимодействовать мое приложение?

Что происходит? То, что происходит, - это то, что вы можете ожидать: разные потребители одного и того же экземпляра HttpClient действуют на неверную информацию - например, отправляют неправильный заголовок Authorization на неправильный BaseAddress. Вот почему HttpClient экземпляры не должны быть общими.

Это потенциально опасно, потому что разные хосты могут требовать разные значения для одного и того же заголовка запроса (например, аутентификация может быть примером). Кроме того, изменение DefaultRequestHeaders одновременно из двух потоков может потенциально испортить внутреннее состояние экземпляра HttpClient из-за отсутствия гарантий безопасности потоков.

Это не обязательно проблема «безопасности потоков» -у вас может быть однопоточное приложение, которое таким образом злоупотребляет HttpClient и все еще имеет ту же проблему. Реальная проблема заключается в том, что различные объекты (потребители HttpClient) предполагают, что они являются владельцем HttpClient, когда они не являются.

К сожалению, C # и .NETу нас нет встроенного способа объявить и утвердить владение или время жизни объекта (следовательно, почему IDisposable сегодня немного беспорядок), поэтому нам нужно прибегнуть к различным альтернативам.

createодно хранилище HttpClient для каждого хоста, с которым приложение должно взаимодействовать. Каждый вызов одного конкретного хоста будет использовать один и тот же экземпляр HttpClient. Параллельные вызовы к одному и тому же хосту безопасны из-за задокументированной безопасности потоков, используемых для выполнения вызовов.

(Под «хостом» я предполагаю, что вы имеете в виду HTTP «происхождение»). Это наивно и не будет работать, если вы отправляете разные запросы одному и тому же сервису с разными токенами доступа (если токены доступа хранятся в DefaultRequestHeaders).

создать одну службу для каждого хоста, с которым приложение должно взаимодействовать. Экземпляр HttpClient внедряется внутри этого сервиса, а сам сервис используется как приложение в приложении. Этот сервис используется для отвлечения доступа к хосту, с которым он связан. Подобные классы полностью тестируемы, как показано здесь.

Опять же, не думайте о сервисах HTTP в терминах "хостов" - в противном случае это имеет ту же проблему, что и выше.

единственной точкой, где создаются и настраиваются экземпляры HttpClient, является корень композиции приложения. Код в корне композиции является однопоточным, поэтому для настройки экземпляров HttpClient безопасно использовать такие свойства, как DefaultRequestHeaders.

Я тоже не уверен, как это поможет. Ваши потребители могут быть в состоянии.

В любом случае, реальным решением, imo, является реализация вашего собственного IHttpClientFactory (это также может быть ваш собственный интерфейс!). Чтобы упростить задачу, конструкторы ваших потребителей не примут экземпляр HttpClient, а вместо этого примут IHttpClientFactory и вызовут его метод CreateClient, чтобы получить собственный частный и сохраняющий состояние экземпляр HttpClient, который затем использует пул общих и не сохраняющих состояние экземпляров HttpClientHandler.

Используя этот подход:

  • Каждый потребитель получает свой собственный частный экземпляриз HttpClient, которые они могут изменять по своему усмотрению - не стоит беспокоиться о том, что объекты изменяют экземпляры, которыми они не владеют.
  • HttpClient экземпляр каждого потребителя не нуждается в утилизации - вы можете спокойно игнорировать тот факт, что они реализуют IDisposable.

    • Без обработчиков из пула каждый экземпляр HttpClient имеет свой собственный обработчик, который должен быть утилизирован.
    • Нос помощью обработчиков пула, как и при таком подходе, пул управляет временем жизни и очисткой обработчика, а не экземплярами HttpClient.
    • Ваш код может вызывать HttpClient.Dispose(), если он действительно хочет (или тыя хочу отключить FxCop), но он ничего не сделает: базовый HttpMessageHandler (PooledHttpClientHandler) имеет метод удаления NOOP.
  • Управление временем жизни HttpClient не имеет значения, поскольку каждый HttpClient имеет только свое собственное изменяемое состояние, такое как DefaultRequestHeaders и BaseAddress - так что вы можете иметь переходные процессы,экземпляры с длительным сроком действия или единичные HttpClient, и это нормально, потому что все они погружаются в пул HttpClientHandler экземпляров только тогда, когда они действительно отправляют запрос.

Вот так:

/// <summary>This service should be registered as a singleton, or otherwise have an unbounded lifetime.</summary>
public QuickAndDirtyHttpClientFactory : IHttpClientFactory // `IHttpClientFactory ` can be your own interface. You do NOT need to use `Microsoft.Extensions.Http`.
{
    private readonly HttpClientHandlerPool pool = new HttpClientHandlerPool();

    public HttpClient CreateClient( String name )
    {
        PooledHttpClientHandler pooledHandler = new PooledHttpClientHandler( name, this.pool );
        return new HttpClient( pooledHandler );
    }

    // Alternative, which allows consumers to set up their own DelegatingHandler chains without needing to configure them during DI setup.
    public HttpClient CreateClient( String name, Func<HttpMessageHandler, DelegatingHandler> createHandlerChain )
    {
        PooledHttpClientHandler pooledHandler = new PooledHttpClientHandler( name, this.pool );
        DelegatingHandler chain = createHandlerChain( pooledHandler );
        return new HttpClient( chain );
    }
}

internal class HttpClientHandlerPool
{
    public HttpClientHandler BorrowHandler( String name )
    {
        // Implementing this is an exercise for the reader.
        // Alternatively, I'm available as a consultant for a very high hourly rate :D
    }

    public void ReleaseHandler( String name, HttpClientHandler handler )
    {
        // Implementing this is an exercise for the reader.
    }
}

internal class PooledHttpClientHandler : HttpMessageHandler
{
    private readonly String name;
    private readonly HttpClientHandlerPool pool;

    public PooledHttpClientHandler( String name, HttpClientHandlerPool pool )
    {
        this.name = name;
        this.pool = pool ?? throw new ArgumentNullException(nameof(pool));
    }

    protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken )
    {
        HttpClientHandler handler = this.pool.BorrowHandler( this.name );
        try
        {
            return await handler.SendAsync( request, cancellationToken ).ConfigureAwait(false);
        }
        finally
        {
            this.pool.ReleaseHandler( this.name, handler );
        }
    }

    // Don't override `Dispose(Bool)` - don't need to.
}

Тогда каждый потребитель может использовать его так:

public class Turboencabulator : IEncabulator
{
    private readonly HttpClient httpClient;

    public Turboencabulator( IHttpClientFactory hcf )
    {
        this.httpClient = hcf.CreateClient();
        this.httpClient.DefaultRequestHeaders.Add( "Authorization", "my-secret-bearer-token" );
        this.httpClient.BaseAddress = "https://api1.example.com";
    }

    public async InverseReactiveCurrent( UnilateralPhaseDetractor upd )
    {
        await this.httpClient.GetAsync( etc )
    }
}

public class SecretelyDivertDataToTheNsaEncabulator : IEncabulator
{
    private readonly HttpClient httpClientReal;
    private readonly HttpClient httpClientNsa;

    public SecretNsaClientService( IHttpClientFactory hcf )
    {
        this.httpClientReal = hcf.CreateClient();
        this.httpClientReal.DefaultRequestHeaders.Add( "Authorization", "a-different-secret-bearer-token" );
        this.httpClientReal.BaseAddress = "https://api1.example.com";

        this.httpClientNsa = hcf.CreateClient();
        this.httpClientNsa.DefaultRequestHeaders.Add( "Authorization", "TODO: it's on a postit note on my desk viewable from outside the building" );
        this.httpClientNsa.BaseAddress = "https://totallylegit.nsa.gov";
    }

    public async InverseReactiveCurrent( UnilateralPhaseDetractor upd )
    {
        await this.httpClientNsa.GetAsync( etc )
        await this.httpClientReal.GetAsync( etc )
    }
}
...