天天看点

Dotnet Core IHttpClientFactory深度研究

今天,我们深度研究一下IHttpClientFactory。

一、前言

最早,我们是在Dotnet Framework中接触到

HttpClient

HttpClient

给我们提供了与

HTTP

交互的基本方式。但这个

HttpClient

在大量频繁使用时,也会给我们抛出两个大坑:一方面,如果我们频繁创建和释放

HttpClient

实例,会导致

Socket

套接字资源耗尽,原因是因为

Socket

关闭后的

TIME_WAIT

时间。这个问题不展开说,如果需要可以去查

TCP

的生命周期。而另一方面,如果我们创建一个

HttpClient

单例,那当被访问的

HTTP

DNS

记录发生改变时,会抛出异常,因为

HttpClient

并不会允许这种改变。

现在,对于这个内容,有了更优的解决方案。

从Dotnet Core 2.1开始,框架提供了一个新的内容:

IHttpClientFactory

IHttpClientFactory

用来创建

HTTP

交互的

HttpClient

实例。它通过将

HttpClient

的管理和用于发送内容的

HttpMessageHandler

链分离出来,来解决上面提到的两个问题。这里面,重要的是管理管道终端

HttpClientHandler

的生命周期,而这个就是实际连接的处理程序。

除此之外,

IHttpClientFactory

还可以使用

IHttpClientBuilder

方便地来定制

HttpClient

和内容处理管道,通过前置配置创建出的

HttpClient

,实现诸如设置基地址或添加

HTTP

头等操作。

    为防止非授权转发,这儿给出本文的原文链接:https://www.cnblogs.com/tiger-wang/p/13752297.html

先来看一个简单的例子:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("WangPlus", c =>
    {
        c.BaseAddress = new Uri("https://github.com/humornif");
    })
    .ConfigureHttpClient(c =>
    {
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
    });
}
           

在这个例子中,当调用

ConfigureHttpClient()

AddHttpMessageHandler()

来配置

HttpClient

时,实际上是在向

IOptions

的实例

HttpClientFactoryOptions

添加配置。这个方法提供了非常多的配置选项,具体可以去看微软的文档,这儿不多说。

在类中使用

IHttpClientFactory

时,也是同样的方式:创建一个

IHttpClientFactory

的单例实例,然后调用

CreateClient(name)

创建一个具有名称

WangPlus

HttpClient

看下面的例子:

public class MyService
{
    private readonly IHttpClientFactory _factory;
    public MyService(IHttpClientFactory factory)
    {
        _factory = factory;
    }
    public async Task DoSomething()
    {
        HttpClient client = _factory.CreateClient("WangPlus");
    }
}
           

用法很简单。

下面,我们会针对

CreateClient()

进行剖析,来深入理解

IHttpClientFactory

背后的内容。

二、HttpClient & HttpMessageHandler的创建过程

CreateClient()

方法是与

IHttpClientFactory

交互的主要方法。

看一下

CreateClient()

的代码实现:

private readonly IOptionsMonitor<HttpClientFactoryOptions> _optionsMonitor

public HttpClient CreateClient(string name)
{
    HttpMessageHandler handler = CreateHandler(name);
    var client = new HttpClient(handler, disposeHandler: false);

    HttpClientFactoryOptions options = _optionsMonitor.Get(name);
    for (int i = 0; i < options.HttpClientActions.Count; i++)
    {
        options.HttpClientActions[i](client);
    }

    return client;
}
           

代码看上去很简单。首先通过

CreateHandler()

创建了一个

HttpMessageHandler

的处理管道,并传入要创建的

HttpClient

的名称。

有了这个处理管道,就可以创建

HttpClient

并传递给处理管道。这儿需要注意的是

disposeHandler:false

,这个参数用来保证当我们释放

HttpClient

的时候,处理管理不会被释放掉,因为

IHttpClientFactory

会自己完成这个管道的处理。

然后,从

IOptionsMonitor

的实例中获取已命名的客户机的

HttpClientFactoryOptions

。它来自

Startup.ConfigureServices()

中添加的

HttpClient

配置函数,并设置了

BaseAddress

Header

等内容。

最后,将

HttpClient

返回给调用者。

理解了这个内容,下面我们来看看

CreateHandler(name)

方法,研究一下

HttpMessageHandler

管道是如何创建的。

readonly ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>> _activeHandlers;;

readonly Func<string, Lazy<ActiveHandlerTrackingEntry>> _entryFactory = (name) =>
    {
        return new Lazy<ActiveHandlerTrackingEntry>(() =>
        {
            return CreateHandlerEntry(name);
        }, LazyThreadSafetyMode.ExecutionAndPublication);
    };

public HttpMessageHandler CreateHandler(string name)
{
    ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;

    entry.StartExpiryTimer(_expiryCallback);

    return entry.Handler;
}
           

看这段代码:

CreateHandler()

做了两件事:

  1. 创建或获取

    ActiveHandlerTrackingEntry

  2. 开始一个计时器。

_activeHandlers

是一个

ConcurrentDictionary<>

,里面保存的是

HttpClient

的名称(例如上面代码中的

WangPlus

)。这里使用

Lazy<>

是一个使

GetOrAdd()

方法保持线程安全的技巧。实际创建处理管道的工作在

CreateHandlerEntry

中,它创建了一个

ActiveHandlerTrackingEntry

ActiveHandlerTrackingEntry

是一个不可变的对象,包含

HttpMessageHandler

IServiceScope

注入。此外,它还包含一个与

StartExpiryTimer()

一起使用的内部计时器,用于在计时器过期时调用回调函数。

ActiveHandlerTrackingEntry

的定义:

internal class ActiveHandlerTrackingEntry
{
    public LifetimeTrackingHttpMessageHandler Handler { get; private set; }
    public TimeSpan Lifetime { get; }
    public string Name { get; }
    public IServiceScope Scope { get; }
    public void StartExpiryTimer(TimerCallback callback)
    {
        // Starts the internal timer
        // Executes the callback after Lifetime has expired.
        // If the timer has already started, is noop
    }
}
           

因此

CreateHandler

方法要么创建一个新的

ActiveHandlerTrackingEntry

,要么从字典中检索条目,然后启动计时器。

下一节,我们来看看

CreateHandlerEntry()

方法如何创建

ActiveHandlerTrackingEntry

实例。

三、在CreateHandlerEntry中创建和跟踪HttpMessageHandler

CreateHandlerEntry

方法是创建

HttpClient

处理管道的地方。

这个部分代码有点复杂,我们简化一下,以研究过程为主:

private readonly IServiceProvider _services;

private readonly IHttpMessageHandlerBuilderFilter[] _filters;

private ActiveHandlerTrackingEntry CreateHandlerEntry(string name)
{
    IServiceScope scope = _services.CreateScope(); 
    IServiceProvider services = scope.ServiceProvider;
    HttpClientFactoryOptions options = _optionsMonitor.Get(name);

    HttpMessageHandlerBuilder builder = services.GetRequiredService<HttpMessageHandlerBuilder>();
    builder.Name = name;

    Action<HttpMessageHandlerBuilder> configure = Configure;
    for (int i = _filters.Length - 1; i >= 0; i--)
    {
        configure = _filters[i].Configure(configure);
    }

    configure(builder);

    var handler = new LifetimeTrackingHttpMessageHandler(builder.Build());

    return new ActiveHandlerTrackingEntry(name, handler, scope, options.HandlerLifetime);

    void Configure(HttpMessageHandlerBuilder b)
    {
        for (int i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++)
        {
            options.HttpMessageHandlerBuilderActions[i](b);
        }
    }
}
           

先用根DI容器创建一个

IServiceScope

,从关联的

IServiceProvider

中获取关联的服务,再从

HttpClientFactoryOptions

中找到对应名称的

HttpClient

和它的配置。

从容器中查找的下一项是

HttpMessageHandlerBuilder

,默认值是

DefaultHttpMessageHandlerBuilder

,这个值通过创建一个主处理程序(负责建立

Socket

套接字和发送请求的

HttpClientHandler

)来构建处理管道。我们可以通过添加附加的委托来包装这个主处理程序,来为请求和响应创建自定义管理。

附加的委托

DelegatingHandlers

类似于Core的中间件管道:

  1. Configure()

    根据

    Startup.ConfigureServices()

    提供的配置构建

    DelegatingHandlers

    管道;
  2. IHttpMessageHandlerBuilderFilter

    是注入到

    IHttpClientFactory

    构造函数中的过滤器,用于在委托处理管道中添加额外的处理程序。

IHttpMessageHandlerBuilderFilter

类似于

IStartupFilters

,默认注册的是

LoggingHttpMessageHandlerBuilderFilter

。这个过滤器向委托管道添加了两个额外的处理程序:

  1. 管道开始位置的

    LoggingScopeHttpMessageHandler

    ,会启动一个新的日志

    Scope

  2. 管道末端的

    LoggingHttpMessageHandler

    ,在请求被发送到主

    HttpClientHandler

    之前,记录有关请求和响应的日志;

最后,整个管道被包装在一个

LifetimeTrackingHttpMessageHandler

中。管道处理完成后,将与用于创建它的

IServiceScope

一起保存在一个新的

ActiveHandlerTrackingEntry

实例中,并给定

HttpClientFactoryOptions

中定义的生存期(默认为两分钟)。

该条目返回给调用者(

CreateHandler()

方法),添加到处理程序的

ConcurrentDictionary<>

中,添加到新的

HttpClient

实例中(在

CreateClient()

方法中),并返回给原始调用者。

在接下来的生存期(两分钟)内,每当您调用

CreateClient()

时,您将获得一个新的

HttpClient

实例,但是它具有与最初创建时相同的处理程序管道。

每个命名或类型化的

HttpClient

都有自己的消息处理程序管道。例如,名称为

WangPlus

的两个

HttpClient

实例将拥有相同的处理程序链,但名为

api

HttpClient

将拥有不同的处理程序链。

下一节,我们研究下计时器过期后的清理处理。

三、过期清理

以默认时间来说,两分钟后,存储在

ActiveHandlerTrackingEntry

中的计时器将过期,并触发

StartExpiryTimer()

的回调方法

ExpiryTimer_Tick()

ExpiryTimer_Tick

负责从

ConcurrentDictionary<>

池中删除处理程序记录,并将其添加到过期处理程序队列中:

readonly ConcurrentQueue<ExpiredHandlerTrackingEntry> _expiredHandlers;

internal void ExpiryTimer_Tick(object state)
{
    var active = (ActiveHandlerTrackingEntry)state;

     _activeHandlers.TryRemove(active.Name, out Lazy<ActiveHandlerTrackingEntry> found);

    var expired = new ExpiredHandlerTrackingEntry(active);
    _expiredHandlers.Enqueue(expired);

    StartCleanupTimer();
}
           

当一个处理程序从

_activeHandlers

集合中删除后,当调用

CreateClient()

时,它将不再与新的

HttpClient

一起分发,但会保持在内存存,直到引用此处理程序的所有

HttpClient

实例全部被清除后,

IHttpClientFactory

才会最终释放这个处理程序管道。

IHttpClientFactory

使用

LifetimeTrackingHttpMessageHandler

ExpiredHandlerTrackingEntry

来跟踪处理程序是否不再被引用。

看下面的代码:

internal class ExpiredHandlerTrackingEntry
{
    private readonly WeakReference _livenessTracker;

    public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
    {
        Name = other.Name;
        Scope = other.Scope;

        _livenessTracker = new WeakReference(other.Handler);
        InnerHandler = other.Handler.InnerHandler;
    }

    public bool CanDispose => !_livenessTracker.IsAlive;

    public HttpMessageHandler InnerHandler { get; }
    public string Name { get; }
    public IServiceScope Scope { get; }
}
           

根据这段代码,

ExpiredHandlerTrackingEntry

创建了对

LifetimeTrackingHttpMessageHandler

的弱引用。根据上一节所写的,

LifetimeTrackingHttpMessageHandler

是管道中的“最外层”处理程序,因此它是

HttpClient

直接引用的处理程序。

LifetimeTrackingHttpMessageHandler

WeakReference

意味着对管道中最外层处理程序的直接引用只有在

HttpClient

中。一旦垃圾收集器收集了所有这些

HttpClient

LifetimeTrackingHttpMessageHandler

将没有引用,因此也将被释放。

ExpiredHandlerTrackingEntry

可以通过

WeakReference.IsAlive

检测到。

在将一个记录添加到

_expiredHandlers

队列之后,

StartCleanupTimer()

将启动一个计时器,该计时器将在10秒后触发。触发后调用

CleanupTimer_Tick()

方法,检查是否对处理程序的所有引用都已过期。如果是,处理程序和

IServiceScope

将被释放。如果没有,它们被添加回队列,清理计时器再次启动:

internal void CleanupTimer_Tick()
{
    StopCleanupTimer();

    int initialCount = _expiredHandlers.Count;
    for (int i = 0; i < initialCount; i++)
    {
        _expiredHandlers.TryDequeue(out ExpiredHandlerTrackingEntry entry);

        if (entry.CanDispose)
        {
            try
            {
                entry.InnerHandler.Dispose();
                entry.Scope?.Dispose();
            }
            catch (Exception ex)
            {
            }
        }
        else
        {
            _expiredHandlers.Enqueue(entry);
        }
    }

    if (_expiredHandlers.Count > 0)
    {
        StartCleanupTimer();
    }
}
           

为了看清代码的流程,这个代码我简单了。原始的代码中还有日志记录和线程锁相关的内容。

这个方法比较简单:遍历

ExpiredHandlerTrackingEntry

记录,并检查是否删除了对

LifetimeTrackingHttpMessageHandler

处理程序的所有引用。如果有,处理程序和

IServiceScope

就会被释放。

如果仍然有对任何

LifetimeTrackingHttpMessageHandler

处理程序的活动引用,则将条目放回队列,并再次启动清理计时器。

四、总结

如果你看到了这儿,那说明你还是很有耐心的。

这篇文章是一个对源代码的研究,能够帮我们理解

IHttpClientFactory

的运行方式,以及它是以什么样的方式填补了旧的

HttpClient

的坑。

有些时候,看看源代码,还是很有益处的。

Dotnet Core IHttpClientFactory深度研究

微信公众号:老王Plus

扫描二维码,关注个人公众号,可以第一时间得到最新的个人文章和内容推送

本文版权归作者所有,转载请保留此声明和原文链接

继续阅读