可以为我的应用程序需要与之通信的每个主机使用一个 HttpClient 实例吗? [英] Is it fine to use one HttpClient instance for each host my application needs to talk to?

查看:21
本文介绍了可以为我的应用程序需要与之通信的每个主机使用一个 HttpClient 实例吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

到目前为止一切顺利.

有时使用诸如 BaseAddressDefaultRequestHeaders,它们不是线程安全的(至少,它们没有被记录为线程安全的,所以我假设它们不是)来配置 HttpClient 实例.

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

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

  • 为应用程序的每个宿主创建一个 HttpClient 实例需要与沟通.对特定主机的每次呼叫都会然后使用相同的 HttpClient 实例.并发调用同一主机是安全的,因为记录的线程安全用于执行调用的方法.

  • 为应用程序需要的每个主机创建一个服务与沟通.HttpClient 实例被注入到这个里面服务和服务本身在应用.该服务用于抽象访问主机它加上.像这样的类是完全可测试的 如图所示.

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

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

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

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

解决方案

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

正确.

<块引用>

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

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

<块引用>

正如在这篇著名文章中所解释并在 Microsoft 文档中也证实的那样,HttpClient 类旨在在每个应用程序生命周期内实例化一次,并在多个 HTTP 调用中重复使用.

不完全是.

  • 您不希望应用程序中 HttpClient 的所有使用者都使用 singleton HttpClient,因为不同的使用者可能对 (正如您稍后指出的那样)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 实例.
    • 你的代码可以调用 HttpClient.Dispose() 如果它真的想要(或者你只是想让 FxCop 闭嘴)但它不会做任何事情:底层的 HttpMessageHandler (PooledHttpClientHandler) 有一个 NOOP dispose 方法.
  • 管理 HttpClient 的生命周期是无关紧要的,因为每个 HttpClient 只拥有自己的可变状态,如 DefaultRequestHeadersBaseAddress - 这样你就可以拥有瞬态、范围、长寿命或单例 HttpClient 实例,这没关系,因为它们都只进入 HttpClientHandler 实例池当他们实际发送请求时.

像这样:

/// 此服务应注册为单例,否则具有无限的生命周期.</summary>public QuickAndDirtyHttpClientFactory : IHttpClientFactory//`IHttpClientFactory ` 可以是你自己的接口.你不需要使用`Microsoft.Extensions.Http`.{私有只读 HttpClientHandlerPool 池 = 新 HttpClientHandlerPool();公共 HttpClient CreateClient(字符串名称){PooledHttpClientHandler pooledHandler = new PooledHttpClientHandler( name, this.pool );返回新的 HttpClient( pooledHandler );}//替代方案,允许消费者设置自己的 DelegatingHandler 链,而无需在 DI 设置期间对其进行配置.public HttpClient CreateClient( String name, Func createHandlerChain ){PooledHttpClientHandler pooledHandler = new PooledHttpClientHandler( name, this.pool );DelegatingHandler chain = createHandlerChain( pooledHandler );返回新的 HttpClient(chain);}}内部类 HttpClientHandlerPool{公共 HttpClientHandler BorrowHandler( 字符串名称){//实现这个是给读者的一个练习.//或者,我可以作为顾问提供非常高的小时费率 :D}public void ReleaseHandler(字符串名称,HttpClientHandler 处理程序){//实现这个是给读者的一个练习.}}内部类 PooledHttpClientHandler : HttpMessageHandler{私有只读字符串名称;私有只读 HttpClientHandlerPool 池;公共 PooledHttpClientHandler(字符串名称,HttpClientHandlerPool 池){this.name = 名称;this.pool = 池 ??throw new ArgumentNullException(nameof(pool));}protected override async TaskSendAsync( HttpRequestMessage 请求, CancellationToken 取消令牌){HttpClientHandler handler = this.pool.BorrowHandler( this.name );尝试{return await handler.SendAsync( request, cancelToken ).ConfigureAwait(false);}最后{this.pool.ReleaseHandler( this.name, handler );}}//不要覆盖 `Dispose(Bool)` - 不需要.}

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

公共类Turboencabulator : IEncabulator{私有只读 HttpClient httpClient;公共涡轮增压器( IHttpClientFactory hcf ){this.httpClient = hcf.CreateClient();this.httpClient.DefaultRequestHeaders.Add( "Authorization", "my-secret-bearer-token" );this.httpClient.BaseAddress = "https://api1.example.com";}公共异步 InverseReactiveCurrent( UnilateralPhaseDetractor upd ){等待 this.httpClient.GetAsync( 等)}}公共类 SecretelyDivertDataToTheNsaEncabulator : IEncabulator{私有只读 HttpClient httpClientReal;私有只读 HttpClient httpClientNsa;公共 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: 它在我桌子上的便条上,可以从大楼外看到");this.httpClientNsa.BaseAddress = "https://totallylegit.nsa.gov";}公共异步 InverseReactiveCurrent( UnilateralPhaseDetractor upd ){等待 this.httpClientNsa.GetAsync( 等)等待 this.httpClientReal.GetAsync( 等)}}

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.

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. 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 in order to avoid issues related with DNS changes.

So far so good.

Sometimes it is handy to use properties like BaseAddress or DefaultRequestHeaders, which are not thread safe (at least, they are not documented to be thread safe, so I assume they are not) to configure the HttpClient instance.

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 ? 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.

For all these reasons, I think that the best approach to use HttpClient (when IServiceCollection is not available) is the following:

  • 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.

  • 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.

  • 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.

Do you see any problem in creating one instance of HttpClient per host to be called ?

I know that instantiating one HttpClient per request can lead to socket exhaustion and must be avoided, but I guess that having one instance per host is safe with regard to this problem (because the same instance is used for all the requests to the same host and I do not expect that a single application needs to talk with a large number of different hosts).

Do you agree ? Am I missing anything ?

解决方案

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.

Correct.

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") should be thought of a (simplistic) abstraction over multiple DI systems - it just so happens to come with its own basic DI container. You can use MEDI as a front for Unity, SimpleInject, Ninject, and others.

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.

Not exactly.

  • You don't want a singleton HttpClient used by all consumers of HttpClient in your application because different consumers might have different assumptions about (as you later point out) DefaultRequestHeaders and other HttpClient state. Some code may also assume that HttpClient is not using any DelegatingHandler instances either.
  • You also don't want any instances of HttpClient (created using its own parameterless constructor) with an unlimited lifetime because of how its default internal HttpClientHandler handles (or rather, doesn't handle) DNS changes. Hence why the default IHttpClientFactory imposes a lifetime limit of 2 minutes for each HttpClientHandler instance.

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?

What happens? What happens is what you can expect: different consumers of the same HttpClient instance acting on wrong information - such as sending the wrong Authorization header to the wrong BaseAddress. This is why HttpClient instances should not be shared.

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.

This isn't necessarily a "Thread safety" issue - you can have a single-threaded application that abuses a singleton HttpClient this way and still have the same issue. The real issue is that different objects (the consumers of HttpClient) are assuming that they are the owner of the HttpClient when they aren't.

Unfortunately C# and .NET do not have a built-in way to declare and assert ownership or object lifetimes (hence why IDisposable is a bit of a mess today) - so we need to resort to different alternatives.

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.

(By "host" I assume you mean HTTP "origin"). This is naive and won't work if you make different requests to the same service with different access-tokens (if the access-tokens are stored in 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.

Again, don't think of HTTP services in terms of "hosts" - otherwise this has the same problem as above.

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.

I'm not sure how this helps either. Your consumers might be stateful.

Anyway, the real solution, imo, is to implement your own IHttpClientFactory (it can also be your own interface!). To simplify things, your consumers' constructors won't accept a HttpClient instance, but instead accept the IHttpClientFactory and call its CreateClient method in order to get their own privately-owned and stateful instance of HttpClient which then uses the pool of shared and stateless HttpClientHandler instances.

Using this approach:

  • Each consumer gets its own private instance of HttpClient that they can alter as they like - no worries about objects modifying instances that they don't own.
  • Each consumer's HttpClient instance does not need to be disposed - you can safely disregard the fact they implement IDisposable.

    • Without pooled handlers, each HttpClient instance owns its own handler, which must be disposed.
    • But with pooled handlers, as with this approach, the pool manages handler lifetime and clean-up, not the HttpClient instances.
    • Your code can call HttpClient.Dispose() if it really wants to (or you just want to make FxCop shut-up) but it wont do anything: the underlying HttpMessageHandler (PooledHttpClientHandler) has a NOOP dispose method.
  • Managing the lifetime of HttpClient is irrelevant because each HttpClient only owns its own mutable state like DefaultRequestHeaders and BaseAddress - so you can have transient, scoped, long-life'd or singleton HttpClient instances and it's okay because they all dip into the pool of HttpClientHandler instances only when they actually send a request.

Like so:

/// <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.
}

Then each consuimer can use it like so:

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 )
    }
}

这篇关于可以为我的应用程序需要与之通信的每个主机使用一个 HttpClient 实例吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆