Dotnet Core IHttpClientFactory深度研究

今天,我们深度研究一下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头等操作。

先来看一个简单的例子:

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> _optionsMonitorpublic 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的坑。

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

喜欢就来个三连,让更多人因你而受益

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307493.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java并发之synchronized实现原理及其优化

1:synchronnized概述 synchronized修饰的方法或代码块相当于并发中的临界区&#xff0c;即在同一时刻jvm只允许一个线程进入执行。synchronized是通过锁机制实现同一时刻只允许一个线程来访问共享资源的。另外synchronized锁机制还可以保证线程并发运行的原子性&#xff0c;有…

.NET 是信息技术应用创新产业重要参与者

今天是国庆节&#xff0c;也是中秋节&#xff0c;月满中秋&#xff0c;举国欢庆&#xff0c;在这里祝各位开发者中秋国庆快乐。放假在家就想把这几年对于.NET发展相关生态做个梳理&#xff0c;写一篇文章来总结一下这两年从腾讯出来自己创业&#xff0c;推动.NET在国内的应用的…

蓝桥杯- 包子凑数

一:题目 题目描述 小明几乎每天早晨都会在一家包子铺吃早餐。他发现这家包子铺有 NN 种蒸笼&#xff0c;其中第 ii 种蒸笼恰好能放 A_iA i ​ 个包子。每种蒸笼都有非常多笼&#xff0c;可以认为是无限笼。 每当有顾客想买 XX 个包子&#xff0c;卖包子的大叔就会迅速选出若…

秋天 | 等疫情过后,我们继续背起相机去旅行

这是头哥侃码的第218篇原创2020年&#xff0c;注定是不平凡的一年。八个月前&#xff0c;我正和家人一起沉浸在春节的喜悦中&#xff0c;可没成想一场疫情的到来彻底改变了欢快的气氛。别的倒没什么&#xff0c;只是之前所有的计划全部被打乱了。先说春节假期&#xff0c;和舅舅…

简单理解CAP-BASE

1、CAPCAP是分布式系统的指导理论&#xff0c;是NoSQL数据库的理论基石。CAP其实就是对分布式系统的特性总结&#xff0c;即一致性&#xff08;Consistency&#xff09;、可用性&#xff08;Availability&#xff09;、分区容错性&#xff08;Partition tolerance&#xff09;。…

面试必问系列之在浏览器中输入URL后到网页显示 其间发生了什么?

文章目录[TOC](文章目录)1:解析URL网址,从而生成发送给Web服务器的Http请求信息2:真实地址查询-----(DNS域名解析)3:调用协议栈4:可靠的传输 TCP5:远程定位--IP地址6:两点传输--获取MAC地址7:网卡--出口8:送别者--交换机9:出境大门--路由器10:相互扒皮--服务端与客户端1:解析UR…

C# 中的 is 真的是越来越强大,越来越语义化

一&#xff1a;背景 1. 讲故事最近发现 C#7 之后的 is 是越来越看不懂了&#xff0c;乍一看花里胡哨的&#xff0c;不过当我静下心来仔细研读&#xff0c;发现这 is 是越来越短小精悍&#xff0c;而且还特别语义化&#xff0c;那怎是一个爽字了得????&#xff0c;这一篇就和…

leetcode209. 长度最小的子数组(滑动窗口)

一:题目 二:上码 class Solution { public:/**思路:1.滑动窗口解法1>:确定窗口内是什么2>:确定窗口的起始位置3>:确定窗口的终止位置2.窗口:也就是我们的求的连续字符串的和3.窗口的起始位置: 窗口的起始位置最先是我们数组的起始位置,当窗口内连续数组的和大于targe…

开源特训营 - Lesson 4 - 如何运营社区

编辑&#xff1a;李明康责编&#xff1a;袁睿斌作者&#xff1a;课程助教 - 苏斌主讲教师&#xff1a;庄表伟&#xff08;开源社理事长&#xff09;、赵生宇&#xff08;同济大学&#xff09;本次课程的主题为《如何运营社区》&#xff0c;主讲者为开源社的理事长庄表伟老师和来…

leetcode904. 水果成篮(滑动窗口)

一:题目 二:上码 class Solution { public:/**思路:滑动窗口1.滑动窗口为我们 两个品种水果的个数2.滑动窗口的起始位置为数组的起始位置,当遇见一个新的水果种类的时候,这时候就需要移动我们的起始位置这里移动起始位置是要注意的,我们需要移动起始到我们的窗口中 完全没有 这…

拥抱.NET 5,从自研微服务框架开始

“ 2016年发布了.NET Core第一个正式版本&#xff0c;而.NET5也将在下个月就正式来临了&#xff0c;技术日新月异&#xff0c;也有点让人应接不暇。在框架设计上&#xff0c;.NET Framework的全家桶理念&#xff0c;培养了一大批的CRUD&#xff0c;而.NET Core转变成了按需使用…

leetcode周赛第一题6037. 按奇偶性交换后的最大数字(冒泡法)

一:题目 二:上码 class Solution { public:int largestInteger(int num) {string str to_string(num);for (int i 0; i < str.size()-1; i) {for (int j i1; j < str.size(); j) {int num1 str[j]-0;int num2 str[i]-0;if (num1%2 num2%2) {//这里我们控制的是奇数…

从零开始打造专属钉钉机器人

一、前言废话不多的实现简单的钉钉机器人。我们分为下面几个步骤来逐步讲解。1.钉钉机器人的类型2.如何打造一个最最简单的钉钉机器人二、钉钉机器人的类型常用的钉钉机器人有两种&#xff0c;1、通知类型的机器人&#xff1a;适合在群内进行一些通知&#xff0c;推荐消息等内容…

Azure 内容审查器之文本审查

内容审查器Azure 内容审查器也是一项认知服务。它支持对文本、图形、视频进行内容审核。可以过滤出某些不健康的内容&#xff0c;关键词。使你的网站内容符合当地的法律法规&#xff0c;提供更好的用户体验。文本内容审核其中文本内容的审核应用比较广泛。比如在审核游戏中的群…

java并发之CAS

文章目录1:何为CAS2:CAS图示3:CAS和Volatile的关系4:为何CAS效率更高一些5:CAS的特点7:受保护得共享数据类型(1):原子基本数据类型(2):原子引用数据类型7:ABA问题(1):什么是ABA(2):如何解决呢&#xff1f;1:何为CAS CAS(Compare And Swap)&#xff0c;即比较并交换。是解决多线…

八大排序算法之选择排序算法

文章目录一:简单选择排序算法1:思想(1):概念(2):举例验证(3):上码(3):时间复杂度和稳定性二:堆排序1:什么是堆2:堆排序的原理&#xff08;以升序序列为例&#xff09;3:堆排序算法的具体过程(1):先将无序序列建成一个堆(2):调换堆顶元素后,再调整剩下的元素为一个新的堆4:上码(…

快速了解 ASP.NET Core Blazor

最近在几个微信 .NET 交流群里大家讨论比较频繁的话题就是这几天自己的面试经历。面试官&#xff1a;“你刚说你喜欢研究新技术&#xff0c;那你有了解过 Blazor 吗&#xff1f;”作为一位专注于 .NET 开发的软件工程师&#xff0c;你好意思说你对 Blazor 一点也不解吗&#xf…

ASP.NET Core集成Nacos配置中心之适配多格式配置

前言 默认情况下&#xff0c;用nacos-sdk-csharp集成ASP.NET Core的配置系统&#xff0c;是基于JSON格式的数据。随着业务系统的多样化&#xff0c;可能用的配置格式也是各有千秋的。有的会用yaml/yml&#xff0c;有的会用ini&#xff0c;有的会用xml&#xff0c;等等。那么如果…

Git的使用(快速入门)

文章目录[TOC]一:将本地的项目提交到Git上二:GIt的基本操作1:建立一个本地仓库(1):类别(2):创建全新的仓库(3):克隆远程仓库2:查看文件的状态3:在桌面上的文件提交到远程仓库4:在idea中的文件提交到远程仓库5:分支(1):何为分支(2):常用命令(3)注意6:解决合并冲突(1):问题展示(2…

为什么这么忙,还依然做不好事情?

一直都很喜欢《重来》系列&#xff0c;最近出了《重来3&#xff1a;跳出疯狂的忙碌》&#xff0c;第一时间在微信读书中阅读了&#xff0c;让我们印象比较深刻的就是「冷静」和「效率」&#xff0c;本文主要说说效率的问题。书的作者是贾森弗里德&#xff08;Jason Fried&#…