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

Enr*_*one 4 .net c# http dotnet-httpclient .net-core

我知道,在使用 Microsoft 依赖项注入容器时,处理 HttpClient 实例的最佳做法是使用Microsoft.Extensions.Http nuget 包提供的IHttpClientFactory 接口

不幸的是,实现IHttpClientFactory 接口的类不是公开的(您可以在此处验证),因此利用此模式的唯一方法是使用 Microsoft 依赖项注入容器(至少这是我所知道的唯一一个)。有时我需要使用不同的容器维护旧的应用程序,因此即使无法使用 IHttpClientFactory 方法,我也需要找出最佳实践。

正如这篇著名文章中所述在 Microsoft 文档中也得到证实, HttpClient 类旨在在每个应用程序生命周期内实例化一次,并在多个 HTTP 调用中重用。这可以安全地完成,因为用于发出 HTTP 调用的公共方法被记录为线程安全的,因此可以安全地使用单例实例。在这种情况下,请务必遵循本文中给出的提示,以避免与 DNS 更改相关的问题。

到现在为止还挺好。

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

这就提出了一个问题:如果我有一个单独的 HttpClient 实例并且在我的代码中的某处我使用属性DefaultRequestHeaders来设置一些常见的 HTTP 请求标头,这对调用我的应用程序需要与之通信的主机之一有用,会发生什么?这是潜在的危险,因为不同的主机可能需要相同请求标头的不同值(以身份验证为例)。此外,由于缺乏线程安全保证,从两个线程同时修改DefaultRequestHeaders可能会扰乱 HttpClient 实例的内部状态。

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

  • 为应用程序需要与之通信的每个主机创建一个 HttpClient 实例对一个特定主机的每次调用都将使用 HttpClient 的相同实例。对同一主机的并发调用是安全的,因为用于执行调用的方法的线程安全性已记录在案。

  • 为应用程序需要与之通信的每个主机创建一个服务。HttpClient 实例被注入到这个服务中,并且服务本身在应用程序中被用作单例。该服务用于抽象出对其耦合的主机的访问。像这样的类是完全可测试的,如图所示

  • 创建和配置 HttpClient 实例的唯一点是应用程序的组合根。组合根中的代码是单线程的,因此使用DefaultRequestHeaders 之类的属性来配置 HttpClient 实例是安全的。

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

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

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

Dai*_*Dai 8

我知道,在使用 Microsoft 依赖项注入容器时,处理 HttpClient 实例的最佳实践是使用 Microsoft.Extensions.Http nuget 包提供的 IHttpClientFactory 接口。

正确的。

不幸的是,实现 IHttpClientFactory 接口的类不是公开的(你可以在这里验证),所以利用这种模式的唯一方法是使用 Microsoft 依赖注入容器(至少它是我知道的唯一一个)。有时我需要使用不同的容器维护旧的应用程序,因此即使无法使用 IHttpClientFactory 方法,我也需要找出最佳实践。

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

正如这篇著名文章中所述并在 Microsoft 文档中也得到证实,HttpClient该类被设计为在每个应用程序生命周期内实例化一次,并在多个 HTTP 调用中重用。

不完全是。

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

这就提出了一个问题:如果我有一个单独的 HttpClient 实例并且在我的代码中的某处使用属性 DefaultRequestHeaders 来设置一些常见的 HTTP 请求标头,这对调用我的应用程序需要与之通信的主机之一有用,会发生什么?

发生什么了?会发生什么是您可以预期的:同一HttpClient实例的不同使用者对错误信息采取行动 - 例如将错误的Authorization标头发送到错误的BaseAddress. 这就是HttpClient不应该共享实例的原因。

这是潜在的危险,因为不同的主机可能需要相同请求标头的不同值(以身份验证为例)。此外,由于缺乏线程安全保证,从两个线程同时修改 DefaultRequestHeaders 可能会扰乱 HttpClient 实例的内部状态。

这不一定是“线程安全”问题 - 您可以拥有一个以HttpClient这种方式滥用单例的单线程应用程序,但仍然存在相同的问题。真正的问题是不同的对象( 的消费者HttpClient)假设它们是 的所有者,而HttpClient实际上它们不是。

不幸的是,C# 和 .NET 没有内置的方法来声明和断言所有权或对象生命周期(因此IDisposable今天有点混乱) - 所以我们需要求助于不同的替代方案。

为应用程序需要与之通信的每个主机创建一个 HttpClient 实例。对一个特定主机的每次调用都将使用相同的 HttpClient 实例。对同一主机的并发调用是安全的,因为用于执行调用的方法的线程安全性已记录在案。

(“主机”我假设您的意思是 HTTP“来源”)。如果您使用不同的访问令牌(如果访问令牌存储在 中DefaultRequestHeaders)向同一服务发出不同的请求,则这是幼稚的并且不起作用。

为应用程序需要与之通信的每个主机创建一个服务。HttpClient 实例被注入到这个服务中,并且服务本身在应用程序中被用作单例。该服务用于抽象出对其耦合的主机的访问。像这样的类是完全可测试的,如图所示。

同样,不要从“主机”的角度考虑 HTTP 服务 - 否则会出现与上述相同的问题。

创建和配置 HttpClient 实例的唯一点是应用程序的组合根。组合根中的代码是单线程的,因此使用 DefaultRequestHeaders 之类的属性来配置 HttpClient 实例是安全的。

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

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

使用这种方法:

  • 每个消费者都有自己的私有实例HttpClient,他们可以随心所欲地更改 - 不用担心对象修改他们不拥有的实例。
  • 不需要处理每个使用者的HttpClient实例- 您可以安全地忽略它们实现的事实。IDisposable

    • 如果没有池化处理程序,每个HttpClient实例都拥有自己的处理程序,必须对其进行处理。
    • 但是对于池处理程序,与这种方法一样,池管理处理程序生命周期和清理,而不是HttpClient实例。
    • 如果你的代码真的想调用(或者你只是想让 FxCop 关闭),它可以调用HttpClient.Dispose(),但它不会做任何事情:底层( ) 有一个 NOOP dispose 方法。HttpMessageHandlerPooledHttpClientHandler
  • 管理 的生命周期HttpClient是无关紧要的,因为每个人HttpClient都只拥有自己的可变状态,例如DefaultRequestHeadersand 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.
}
Run Code Online (Sandbox Code Playgroud)

然后每个消费者都可以像这样使用它:

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 )
    }
}
Run Code Online (Sandbox Code Playgroud)