为我的应用程序需要与之通信的每个主机使用一个 HttpClient 实例是否可以?

Is it fine to use one HttpClient instance for each host my application needs to talk to?

我知道,在使用 Microsoft 依赖项注入容器时,处理 HttpClient 实例的最佳做法是使用 IHttpClientFactory interface provided by the Microsoft.Extensions.Http nuget package.

不幸的是 类 实现了 IHttpClientFactory interface are not public (as you can verify here),因此利用此模式的唯一方法是使用 Microsoft 依赖项注入容器(至少这是我所知道的唯一一个)。有时我需要使用不同的容器维护旧应用程序,因此即使无法使用 IHttpClientFactory 方法,我也需要找出最佳实践。

this famous article and confirmed in the Microsoft docs too the HttpClient class is designed to be instantiated once per application lifetime and reused across multiple HTTP calls. This can safely be done because the public methods used to issue HTTP calls are documented to be thread safe, so a singleton instance can be safely used. In this case, it is important to follow the tips given in this article 中所述,以避免与 DNS 更改相关的问题。

到目前为止一切顺利。

有时使用像 BaseAddress or DefaultRequestHeaders 这样的非线程安全的属性(至少,它们没有被记录为线程安全的,所以我假设它们不是)来配置 HttpClient 实例很方便。

这引发了一个问题:如果我有一个单独的 HttpClient 实例,并且在我的代码中的某处我同时使用来自两个线程的 属性 DefaultRequestHeaders to set some common HTTP request headers useful to call one of the host my application needs to communicate with ? This is potentially dangerous, because different hosts could require different values for the same request header (think of authentication as an example of that). Furthermore, modifying DefaultRequestHeaders 会发生什么情况可能会弄乱HttpClient 实例,因为缺乏线程安全保证。

出于所有这些原因,我认为使用 HttpClient 的最佳方法(当 IServiceCollection 不可用时)如下:

您在为每个要调用的主机创建一个 HttpClient 实例时是否发现任何问题?

我知道每个请求实例化一个 HttpClient 会导致 socket exhaustion 并且必须避免,但我想每个主机有一个实例 是安全的 这个问题(因为同一个实例用于对同一主机的所有请求,我不希望单个应用程序需要与大量不同的主机通信)。

你同意吗?我错过了什么吗?

I know that, when using the Microsoft dependency injection container, the best practice to handle HttpClient instances is using the IHttpClientFactory interface provided by the Microsoft.Extensions.Http nuget package.

正确。

Unfortunately the classes implementing the IHttpClientFactory interface are not public (as you can verify here), so the only way to exploit this pattern is using the Microsoft dependency injection container (at least it's the only one that I know). Sometimes I need to maintain old applications using a different container, so I need to figure out a best practice even when the IHttpClientFactory approach cannot be used.

Microsoft.Extensions.DependencyInjection ("MEDI") 应该被认为是对多个 DI 系统的(简单的)抽象——它恰好带有自己的基本 DI 容器。您可以使用 MEDI 作为 Unity、SimpleInject、Ninject 和其他的前端。

As explained in this famous article and confirmed in the Microsoft docs too the HttpClient class is designed to be instantiated once per application lifetime and reused across multiple HTTP calls.

不完全是。

  • 您不希望应用程序中 HttpClient 的所有消费者都使用 单例 HttpClient,因为不同的消费者可能对(如你稍后指出)DefaultRequestHeaders 和其他 HttpClient 状态。一些代码也可能假设 HttpClient 也没有使用任何 DelegatingHandler 实例。
  • 您也不希望 HttpClient 的任何实例(使用其自己的无参数构造函数创建)具有无限的生命周期,因为它的默认内部 HttpClientHandler 处理方式(或者更确切地说,不处理)DNS 更改。因此,为什么默认 IHttpClientFactory 对每个 HttpClientHandler 实例施加 2 分钟的生命周期限制。

This opens a question: what happens if I have a singleton HttpClient instance and somewhere in my code I use the property DefaultRequestHeaders to set some common HTTP request headers useful to call one of the host my application needs to communicate with?

会发生什么?所发生的情况如您所料:同一 HttpClient 实例的不同消费者对错误的信息采取行动 - 例如将错误的 Authorization header 发送到错误的 BaseAddress。这就是不应共享 HttpClient 个实例的原因。

This is potentially dangerous, because different hosts could require different values for the same request header (think of authentication as an example of that). Furthermore, modifying DefaultRequestHeaders concurrently from two threads could potentially mess up the internal state of the HttpClient instance, because of the lack of thread safety guarantees.

这不一定是 "Thread safety" 问题 - 您可以有一个 single-threaded 应用程序以这种方式滥用单例 HttpClient 并且仍然有同样的问题。真正的问题是,不同的 objects(HttpClient 的消费者)假设他们是 HttpClient 所有者,而实际上他们不是t.

不幸的是,C# 和 .NET 没有 built-in 方法来声明和断言所有权或 object 生命周期(因此 IDisposable 今天有点乱)- 所以我们需要求助于不同的选择。

create one instace of HttpClient for each host the application needs to communicate with. Every call to one specific host will then use the same instance of HttpClient. Concurrent calls to the same host are safe, because of the documented thread safety of methods used to perform calls.

("host" 我假设你指的是 HTTP "origin")。如果您使用不同的 access-tokens 对同一服务发出不同的请求(如果 access-tokens 存储在 DefaultRequestHeaders 中),这是天真的,并且不会起作用。

create one service for each host the application needs to communicate with. The HttpClient instance is injected inside this service and the service itself is used as a singleton in the application. This service is used to abstract away the access to the host it is coupled with. Classes like this are fully testable as illustrated here.

同样,不要用 "hosts" 来考虑 HTTP 服务 - 否则会出现与上述相同的问题。

the only point where instances of HttpClient are created and configured is the composition root of the application. The code in the composition root is single threaded, so it is safe to use properties like DefaultRequestHeaders to configure the HttpClient instances.

我也不确定这有什么帮助。您的消费者可能是有状态的。

无论如何,imo,真正的解决方案是实现您自己的 IHttpClientFactory(它也可以是您自己的接口!)。为了简化事情,您的消费者的构造函数不会接受 HttpClient 实例,而是接受 IHttpClientFactory 并调用其 CreateClient 方法以获得他们自己的 privately-owned 和 HttpClient 的有状态 实例,然后使用 共享和无状态 HttpClientHandler 实例的池。

使用这种方法:

  • 每个消费者都有自己的 HttpClient 私有实例,他们可以根据自己的喜好进行更改 - 不用担心 object 修改他们不拥有的实例。
  • 每个消费者的 HttpClient 实例 不需要处理 - 你可以安全地忽略他们实现 IDisposable 的事实。

    • 没有池化处理程序,每个 HttpClient 实例都拥有自己的处理程序,必须将其处理掉。
    • 但是对于池处理程序,与这种方法一样,池管理处理程序生命周期和 clean-up,而不是 HttpClient 个实例。
    • 你的代码可以调用HttpClient.Dispose()如果它真的想要(或者你只是想制作FxCop shut-up) 但它不会做任何事情:底层 HttpMessageHandler (PooledHttpClientHandler) 有一个 NOOP dispose 方法。
  • 管理 HttpClient 的生命周期是无关紧要的,因为每个 HttpClient 只拥有自己的可变状态,如 DefaultRequestHeadersBaseAddress - 所以你可以具有瞬态、范围、long-life'd 或单例 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 )
    }
}