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

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

问题描述

我知道,当使用Microsoft依赖项注入容器时,处理HttpClient实例的最佳实践是使用 IHttpClientFactory接口 nofollow noreferrer> Microsoft.Extensions.Http nuget程序包。

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.

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

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文档中确认 HttpClient类旨在在每个应用程序生命周期中实例化一次,并在多个HTTP调用中重复使用。可以安全地完成此操作,因为用于发出HTTP调用的公共方法记录为线程安全的,因此可以安全地使用单例实例。在这种情况下,请务必遵循本文中给出的提示,以避免与DNS更改相关的问题。

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.

有时使用诸如 BaseAddress DefaultRequestHeaders ,它们不是线程安全的(至少,没有文档证明它们是线程安全的,所以我认为它们不是),以配置HttpClient实例。

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.

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

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.

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

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


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

  • 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 应用程序需要与每个主机进行
通讯的一项服务
。 HttpClient实例被注入到
服务内部,该服务本身在
应用程序中用作单例。此服务用于抽象化对与其耦合的
主机的访问。这样的类是完全可测试的

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.

创建和配置HttpClient实例的唯一点是应用程序的组合根。合成根目录中的代码是单线程的,因此可以安全使用 DefaultRequestHeaders 配置HttpClient实例。

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.

在每个要调用的主机上创建一个HttpClient实例时是否看到任何问题?

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

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

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 ?

推荐答案


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

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.

正确。


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

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和其他应用程序的前端。

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.


正如这篇著名文章中所述并在Microsoft中得到确认 HttpClient 类也旨在在每个应用程序生存期内实例化一次,并在多个HTTP调用中重复使用。

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.

不完全是。


  • 您不希望使用单个 <$ c $应用程序中 HttpClient 的所有使用者使用的c> HttpClient ,因为不同的使用者可能对(如您稍后指出) DefaultRequestHeaders 和其他 HttpClient 状态。某些代码可能还假设 HttpClient 也不使用任何 DelegatingHandler 实例。

  • 您还不希望任何 HttpClient 实例(使用其自己的无参数构造函数创建)具有无限的生存期,这是因为其默认内部 HttpClientHandler的方式处理(或更确切地说,不处理)DNS更改。因此,为什么默认的 IHttpClientFactory 对每个 HttpClientHandler 实例施加2分钟的生命周期限制。

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

这就提出了一个问题:如果我有一个单例HttpClient实例,并且在我的代码中的某个地方,我会使用DefaultRequestHeaders属性来设置公共HTTP请求标头可用于调用与应用程序进行通信的主机之一?

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 标头指向错误的 BaseAddress 。这就是为什么不应该共享 HttpClient 实例的原因。

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.


这是潜在的危险,因为不同的主机可能针对同一请求标头要求不同的值(例如,以身份验证为例)。此外,由于缺少线程安全性保证,从两个线程同时修改DefaultRequestHeaders可能会破坏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.

这不一定是线程安全问题-您可以拥有一个单线程应用程序,该应用程序以这种方式滥用单例 HttpClient 并仍然存在相同的问题。真正的问题是,不同的对象( HttpClient 的使用者)假设它们是 HttpClient <的所有者 / p code>。

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.

不幸的是,C#和.NET没有内置的方法来声明和声明所有权或对象生存期(因此为什么 IDisposable 今天有点混乱)-所以我们需要求助于其他选择。

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.


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

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.

(由主机我假设您的意思是HTTP起源)。如果您对具有不同访问令牌的同一服务提出不同的请求(如果访问令牌存储在 DefaultRequestHeaders 中),这是天真的,将不起作用。

(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).


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

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.

同样,不要以主机的形式来考虑HTTP服务-否则,

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


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

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'我不确定这对您有什么帮助。您的消费者可能是有状态的。

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

无论如何,真正的解决方案imo是实现您自己的 IHttpClientFactory (它也可以是您自己的界面!)。为简化起见,消费者的构造函数将不接受 HttpClient 实例,而是接受 IHttpClientFactory 并调用其 CreateClient 方法,以获取自己的 HttpClient 私有和有状态实例。然后使用共享和无状态 HttpClientHandler 实例池。

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.

使用此方法:


  • 每个消费者都拥有自己的 HttpClient 私有实例,他们可以随其更改就像-不用担心对象会修改它们不拥有的实例。

  • 每个使用者的 HttpClient 实例确实-您可以放心地忽略它们实现了 IDisposable 的事实。

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


  • 没有池处理程序,每个 HttpClient 实例拥有自己的处理程序,必须将其处理。

  • 但是使用池化处理程序,像这种方法一样,池管理处理程序的生存期和清理,而不是 HttpClient 实例。

  • 您的代码可以调用 HttpClient.Dispose()(如果确实需要)(或者您只想关闭FxCop) ),但不会做任何事情:基础 HttpMessageHandler PooledHttpClientHandler )具有NOOP处置方法。

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

管理 HttpClient 的生存期是不相关的,因为每个 HttpClient 仅拥有自己的可变状态,例如 DefaultRequestHeaders BaseAddress -您可以拥有瞬态,作用域,长寿命或单例 HttpClient 实例,这是可以的,因为它们都浸入了 HttpClientHandler

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.

就像这样:

/// <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天全站免登陆