今天,我们深度研究一下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

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

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

Dotnet Core IHttpClientFactory深度研究的更多相关文章

  1. dotnet core 开发体验之Routing

    开始 回顾上一篇文章:dotnet core开发体验之开始MVC 里面体验了一把mvc,然后我们知道了aspnet mvc是靠Routing来驱动起来的,所以感觉需要研究一下Routing是什么鬼. ...

  2. dotnet core开发体验之开始MVC

    开始 在上一篇文章:dotnet core多平台开发体验 ,体验了一把dotnet core 之后,现在想对之前做的例子进行改造,想看看加上mvc框架是一种什么样的体验,于是我就要开始诞生今天的这篇文 ...

  3. dotnet core部署方式两则:CLI、IIS

    最近在使用dotnet core研究整个开发过程,使用下面两种方式部署: 一,使用 dotnet run 命令运行 在项目路径,shift+右键,选择 “在此处打开命令窗口”,在CMD窗口中运行“do ...

  4. (转载)dotnet core 中文乱码 codepages

    引子 转载自:http://www.jianshu.com/p/1c9c59c5749a 参考:.Net Core 控制台输出中文乱码 上文中我查阅了一些cli的源码, 闲来无事就继续翻代码, 冥冥之 ...

  5. Windows 7 上面安装 dotnet core 之后 使用 应用报错的处理:api-ms-win-crt-runtime-l1-1-0.dll 丢失

    Windows2016 使用 dotnet core的使用 安装了就可以了 但是发现 windows 7 不太行 报错如图示 没办法简单百度了下 https://www.microsoft.com/z ...

  6. 2017-03-05 CentOS中结合Nginx部署dotnet core Web应用程序

    Visual Studio Live 倒计时2天,当然这是美国倒计时两天,中国应该是在3月8日的凌晨,正值"3.8妇女节".提前祝广大的女性同志节日快乐,当然还有奋斗在一线的程序媛 ...

  7. 2017-03-04 dotnet core网站发布到Linux系统中

    今天开始学习dotnet core的开发,距离Visual Stuio 2017正式版的发布,也就是VS20周岁的生日还有三天,在我的电脑上安装的是VS2017 Enterprise RC版, 在VS ...

  8. Dotnet core基于ML.net的销售数据预测实践

    ML.net已经进到了1.5版本.作为Microsoft官方的机器学习模型,你不打算用用?   一.前言 ML.net可以让我们很容易地在各种应用场景中将机器学习加入到应用程序中.这是这个框架很重要的 ...

  9. Dotnet Core使用特定的SDK&Runtime版本

    Dotnet Core的SDK版本总在升级,怎么使用一个特定的版本呢?   假期过完了,心情还在.今天写个短的. 一.前言 写这个是因为昨天刷微软官方文档,发现global.json在 SDK 3.0 ...

随机推荐

  1. wordpress个人常用标签调用

    wordpress常见标签调用,老是容易忘记,又要找半天,干脆搬到网站上. <?php bloginfo('name');?>网站名称 url <?php echo home_url ...

  2. ASP.NET Core3.1使用IdentityServer4中间件系列随笔(二):创建API项目,配置IdentityServer保护API资源

    配套源码:https://gitee.com/jardeng/IdentitySolution 接上一篇<ASP.NET Core3.1使用IdentityServer4中间件系列随笔(一):搭 ...

  3. FJOI2020 游记

    Day -1 啥都不会,药丸 看了看统考题,好难,爆零的节奏 文化课OI双爆炸 尽力吧 Day 0 花三个多小时才到考场 福州真的好热 签到 在小礼堂待了一会,顺便给手机充了电 四点试机,今年用了新系 ...

  4. 软件人如何快速晋升CTO(一)

    1.场景描述 ​ 软件人如何快速晋升CTO? 实战操作,介绍下如何0成本拥有自己的软件公司,出任CTO/CEO. ​ 2020年 8月16日,软件老王拿到公司的营业执照和公章了,税务登记也一起办理好了 ...

  5. Micro-CMS v1 (Hacker101 CTF)

    这道题一共有四个flag,初步观察可以创建或者修改发布的内容. Flag1: 首先随便创建了一个页面,创建完成后页面会直接跳转到我们所创建的页面. 初步判断网页应该是根据地址栏后的数字来查询并且显示页 ...

  6. 用Python实现十大经典排序算法-插入、选择、快速、冒泡、归并等

    本文来用图文的方式详细讲解了Python十大经典排序算法 —— 插入排序.选择排序.快速排序.冒泡排序.归并排序.希尔排序.插入排序.桶排序.基数排序.计数排序算法,想要学习的你们,继续阅读下去吧,如 ...

  7. Mybatis源码学习第七天(插件开发原理)

    插件概述: 插件是用来改变或者扩展mybatis的原有功能,mybatis的插件就是通过继承Interceptor拦截器实现的,在没有完全理解插件之前j禁止使用插件对mybatis进行扩展,有可能会导 ...

  8. 20190923-07Linux搜索查找类 000 015

    find 查找文件或者目录 find指令将从指定目录向下递归地遍历其各个子目录,将满足条件的文件显示在终端. 1.基本语法 find [搜索范围] [选项] 2.选项说明 表1-27 选项 功能 -n ...

  9. Prmise.all的简单实现

    注意点 入参一般是个由Promise实例组成的数组,但是也可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例.若参数如果不是 Promise 实例,就会先调用 ...

  10. latex tips

    latex tips  latex 插入图片 主要是插入图片位置的问题,代码中的  bb=   决定了插入 fig2.eps 图片的某一方形区域,其中     表示该方形区域的左下角像素坐标是 0 0 ...