为我的应用程序需要与之通信的每个主机使用一个 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 实例。并发调用
相同的主机是安全的,因为有记录的线程安全
用于执行调用的方法。
为应用程序需要的每个主机创建一个服务
与交流。 HttpClient 实例被注入其中
服务和服务本身在
应用。该服务用于抽象出对
主机它是耦合的。 类 像这样完全可以测试 as illustrated here.
创建和配置 HttpClient 实例的唯一点是应用程序的组合根。组合根中的代码是单线程的,因此使用 DefaultRequestHeaders 之类的属性来配置 HttpClient 实例是安全的。
您在为每个要调用的主机创建一个 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
只拥有自己的可变状态,如 DefaultRequestHeaders
和 BaseAddress
- 所以你可以具有瞬态、范围、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 )
}
}
我知道,在使用 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 实例。并发调用 相同的主机是安全的,因为有记录的线程安全 用于执行调用的方法。
为应用程序需要的每个主机创建一个服务 与交流。 HttpClient 实例被注入其中 服务和服务本身在 应用。该服务用于抽象出对 主机它是耦合的。 类 像这样完全可以测试 as illustrated here.
创建和配置 HttpClient 实例的唯一点是应用程序的组合根。组合根中的代码是单线程的,因此使用 DefaultRequestHeaders 之类的属性来配置 HttpClient 实例是安全的。
您在为每个要调用的主机创建一个 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
只拥有自己的可变状态,如DefaultRequestHeaders
和BaseAddress
- 所以你可以具有瞬态、范围、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 )
}
}