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

一、前言

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

HttpClient给我们提供了与HTTP交互的基本方式。但这个HttpClient在大量频繁使用时,也会给我们抛出两个大坑:一方面,如果我们频繁创建和释放HttpClient实例,会导致Socket套接字资源耗尽,原因是因为Socket关闭后的TIME_WAIT时间。这个问题不展开说,如果需要可以去查TCP的生命周期。而另一方面,如果我们创建一个HttpClient单例,那当被访问的HTTPDNS记录发生改变时,会抛出异常,因为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)创建一个具有名称WangPlusHttpClient

看下面的例子:

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配置函数,并设置了BaseAddressHeader等内容。

最后,将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是一个不可变的对象,包含HttpMessageHandlerIServiceScope注入。此外,它还包含一个与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实例将拥有相同的处理程序链,但名为apiHttpClient将拥有不同的处理程序链。

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

三、过期清理

以默认时间来说,两分钟后,存储在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使用LifetimeTrackingHttpMessageHandlerExpiredHandlerTrackingEntry来跟踪处理程序是否不再被引用。

看下面的代码:

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中。一旦垃圾收集器收集了所有这些HttpClientLifetimeTrackingHttpMessageHandler将没有引用,因此也将被释放。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的坑。

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


微信公众号:老王Plus

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

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

最新文章

  1. DDD 领域驱动设计-如何控制业务流程?
  2. neo4j-java连接
  3. JS设置cookie
  4. EventBus (四) Sticky事件
  5. 重拾C,一天一点点_3
  6. spot 5、ALOS监督分类波段组成
  7. yml文件数据的简洁表达方法(Hashes to OpenStruct)
  8. iOS 9之SFSafariViewController
  9. java学习笔记 --- 集合
  10. Flask学习 二 模板
  11. chart 目录结构 - 每天5分钟玩转 Docker 容器技术(164)
  12. 016.OpenStack及云计算(面试)常见问题
  13. Django Simple Captcha插件
  14. c++stack容器介绍
  15. HTTPS如何保证数据传输的安全性 -- 结合加密
  16. asmx 接收数据过大 发生错误
  17. Hibernate(二)
  18. VSFTP 配置虚拟用户
  19. 11.5 Daily Scrum
  20. Manacher&#39;s Algorithm 马拉车算法(求最长回文串)

热门文章

  1. JacaScript实现call apply bind函数
  2. 记录学习docker命令的随笔
  3. Transform与Vector3 的API
  4. Azure Storage 系列(二) .NET Core Web 项目中操作 Blob 存储
  5. 面试【JAVA基础】Web与网络
  6. 使用Telnet服务测试端口时,提示没有Telnet服务
  7. 浅析LR.Net工作流引擎
  8. 会话技术之 Session
  9. MySQL查询point类型类型的坐标,返回经度纬度
  10. python连接websocket wss