为我的应用程序需要与之通信的每个主机使用一个 HttpClient 实例是不是可以?
Posted
技术标签:
【中文标题】为我的应用程序需要与之通信的每个主机使用一个 HttpClient 实例是不是可以?【英文标题】:Is it fine to use one HttpClient instance for each host my application needs to talk to?为我的应用程序需要与之通信的每个主机使用一个 HttpClient 实例是否可以? 【发布时间】:2019-10-31 23:36:09 【问题描述】:我知道,在使用 Microsoft 依赖注入容器时,处理 HttpClient 实例的最佳做法是使用Microsoft.Extensions.Http nuget package 提供的IHttpClientFactory interface。
不幸的是,实现IHttpClientFactory interface 的类不是公共的(as you can verify here),因此利用这种模式的唯一方法是使用 Microsoft 依赖注入容器(至少我知道它是唯一的)。有时我需要使用不同的容器来维护旧应用程序,因此即使无法使用 IHttpClientFactory 方法,我也需要找出最佳实践。
正如this famous article 和confirmed in the Microsoft docs too 中所解释的,HttpClient 类被设计为在每个应用程序生命周期中实例化一次,并在多个 HTTP 调用中重用。这可以安全地完成,因为用于发出 HTTP 调用 are documented to be thread safe 的公共方法,因此可以安全地使用单例实例。在这种情况下,请务必遵循in this article 给出的提示,以避免与 DNS 更改相关的问题。
到目前为止一切顺利。
有时使用BaseAddress 或DefaultRequestHeaders 之类的属性很方便,它们不是线程安全的(至少,它们没有被证明是线程安全的,所以我认为它们不是)来配置HttpClient 实例。
这引发了一个问题:如果我有一个单例 HttpClient 实例并且在我的代码中的某处我使用属性 DefaultRequestHeaders 设置一些常见的 HTTP 请求标头,以调用我的应用程序需要与之通信的主机之一,会发生什么?这有潜在的危险,因为不同的主机可能对同一个请求标头需要不同的值(将身份验证视为一个示例)。此外,由于缺乏线程安全保证,同时从两个线程修改 DefaultRequestHeaders 可能会破坏 HttpClient 实例的内部状态。
出于所有这些原因,我认为使用 HttpClient(当 IServiceCollection 不可用时)的最佳方法如下:
为应用程序的每个主机创建 一个 HttpClient 实例 需要沟通。 对特定主机的每次调用都会 然后使用相同的 HttpClient 实例。并发调用 同一个主机是安全的,因为有记录的线程安全 用于执行调用的方法。
为应用程序需要的每个主机创建一个服务 与交流。 HttpClient 实例被注入其中 服务和服务本身在 应用。该服务用于抽象出对 主机与之耦合。像这样的类是完全可测试的as illustrated here。
创建和配置 HttpClient 实例的唯一点是应用程序的组合根。组合根中的代码是单线程的,因此可以安全地使用 DefaultRequestHeaders 等属性来配置 HttpClient 实例。
在为每个要调用的主机创建一个 HttpClient 实例时,您是否发现有任何问题?
我知道每个请求实例化一个 HttpClient 会导致 socket exhaustion 并且必须避免,但我想对于这个问题,每个主机有一个实例是安全的(因为同一个实例用于对同一主机的所有请求,我不希望单个应用程序需要与大量不同的主机通信)。
你同意吗?我错过了什么吗?
【问题讨论】:
旁注:q 没有说明它是客户端代码还是服务器端代码...如果是服务器端代码,最好删除 DefaultRequestHeaders 调用,而不是考虑这个修复,如果标头可能是特定于用户的(例如您提到的身份验证)。 @AlexeiLevenkov 我有客户端应用程序和服务器应用程序。当然,一种可能的方法是完全避免 DefaultRequestHeaders 并在执行的每个请求上正确配置 HttpRequestMessage。在这种情况下,使用 HttpClient 的一个单例实例对我来说似乎是最好的解决方案。 你应该管理的不是HttpClient
,而是HttpClientHandler
。重新实现 IHttpClientFactory
也不难 - 只需记住使用有时间限制的 HttpClientHandler
实例池 - IIRC 是 2 分钟。
旁注 2:“线程安全”和“产生任何意义/产生合理结果”不是同一件事 - 设置共享属性 本身 可能是线程安全的,但一个类绝对没有办法保证几个不相关调用的顺序(比如设置BaseAddress
、DefaultRequestHeaders
和发送请求)会产生你想要的结果——虽然类可以安全地处理每个操作,但没有办法知道实际用于“发送请求”的值,因为其他线程可以安全地同时更改这些值。
至于问题本身——我相信少量固定数量的 HttpClient 不会成为问题,本质上是编码风格——对我来说管理单例和“每次调用的实例”通常比“重用多个实例之一”更容易基于某些条件”-所以我会尝试确保安全使用单例 HttpClient...
【参考方案1】:
我知道,在使用 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闭嘴)但它不会做任何事情:底层@ 987654354@ (PooledHttpClientHandler
) 有一个 NOOP dispose 方法。
管理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 )
【讨论】:
以上是关于为我的应用程序需要与之通信的每个主机使用一个 HttpClient 实例是不是可以?的主要内容,如果未能解决你的问题,请参考以下文章