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()
做了两件事:
- 创建或获取
ActiveHandlerTrackingEntry
; - 开始一个计时器。
_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的中间件管道:
Configure()
根据Startup.ConfigureServices()
提供的配置构建DelegatingHandlers
管道;IHttpMessageHandlerBuilderFilter
是注入到IHttpClientFactory
构造函数中的过滤器,用于在委托处理管道中添加额外的处理程序。
IHttpMessageHandlerBuilderFilter
类似于IStartupFilters
,默认注册的是LoggingHttpMessageHandlerBuilderFilter
。这个过滤器向委托管道添加了两个额外的处理程序:
- 管道开始位置的
LoggingScopeHttpMessageHandler
,会启动一个新的日志Scope
; - 管道末端的
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
的坑。
有些时候,看看源代码,还是很有益处的。
![]() |
微信公众号:老王Plus 扫描二维码,关注个人公众号,可以第一时间得到最新的个人文章和内容推送 本文版权归作者所有,转载请保留此声明和原文链接 |
Dotnet Core IHttpClientFactory深度研究的更多相关文章
- dotnet core 开发体验之Routing
开始 回顾上一篇文章:dotnet core开发体验之开始MVC 里面体验了一把mvc,然后我们知道了aspnet mvc是靠Routing来驱动起来的,所以感觉需要研究一下Routing是什么鬼. ...
- dotnet core开发体验之开始MVC
开始 在上一篇文章:dotnet core多平台开发体验 ,体验了一把dotnet core 之后,现在想对之前做的例子进行改造,想看看加上mvc框架是一种什么样的体验,于是我就要开始诞生今天的这篇文 ...
- dotnet core部署方式两则:CLI、IIS
最近在使用dotnet core研究整个开发过程,使用下面两种方式部署: 一,使用 dotnet run 命令运行 在项目路径,shift+右键,选择 “在此处打开命令窗口”,在CMD窗口中运行“do ...
- (转载)dotnet core 中文乱码 codepages
引子 转载自:http://www.jianshu.com/p/1c9c59c5749a 参考:.Net Core 控制台输出中文乱码 上文中我查阅了一些cli的源码, 闲来无事就继续翻代码, 冥冥之 ...
- Windows 7 上面安装 dotnet core 之后 使用 应用报错的处理:api-ms-win-crt-runtime-l1-1-0.dll 丢失
Windows2016 使用 dotnet core的使用 安装了就可以了 但是发现 windows 7 不太行 报错如图示 没办法简单百度了下 https://www.microsoft.com/z ...
- 2017-03-05 CentOS中结合Nginx部署dotnet core Web应用程序
Visual Studio Live 倒计时2天,当然这是美国倒计时两天,中国应该是在3月8日的凌晨,正值"3.8妇女节".提前祝广大的女性同志节日快乐,当然还有奋斗在一线的程序媛 ...
- 2017-03-04 dotnet core网站发布到Linux系统中
今天开始学习dotnet core的开发,距离Visual Stuio 2017正式版的发布,也就是VS20周岁的生日还有三天,在我的电脑上安装的是VS2017 Enterprise RC版, 在VS ...
- Dotnet core基于ML.net的销售数据预测实践
ML.net已经进到了1.5版本.作为Microsoft官方的机器学习模型,你不打算用用? 一.前言 ML.net可以让我们很容易地在各种应用场景中将机器学习加入到应用程序中.这是这个框架很重要的 ...
- Dotnet Core使用特定的SDK&Runtime版本
Dotnet Core的SDK版本总在升级,怎么使用一个特定的版本呢? 假期过完了,心情还在.今天写个短的. 一.前言 写这个是因为昨天刷微软官方文档,发现global.json在 SDK 3.0 ...
随机推荐
- [CSP-S2019]Emiya 家今天的饭 题解
CSP-S2 2019 D2T1 很不错的一题DP,通过这道题学到了很多. 身为一个对DP一窍不通的蒟蒻,在考场上还挣扎了1h来推式子,居然还有几次几乎推出正解,然而最后还是只能打个32分的暴搜滚粗 ...
- python - 平方根格式化 + 字符串分段组合
题目来源:python123 平方根格式化 描述 获得用户输入的一个整数a,计算a的平方根,保留小数点后3位,并打印输出. ...
- codeblocks显示:不支持的16位应用程序 解决办法
我是win10 64位系统,写c++运行就会显示不兼容16位应用程序.以前编出来的exe还能用,今天编出的就炸了. 试了用vs编译.vs能用. 试了网上找的各种解决方案, 360修复, 注册表, 重构 ...
- 关于bat批处理的一些操作,如启动jar 关闭进程等
先说一下学习这个的前提: 公司要写个生成uid的工具,整完了之后就又整批处理工具,出于此目的,也是为了丰富自己的知识,就学习了一下,下面是相关的批处理脚本 我花了半天的时间找了相关的bat批处理,但是 ...
- SpringCloud 服务负载均衡和调用 Ribbon、OpenFeign
1.Ribbon Spring Cloud Ribbon是基于Netflix Ribbon实现的-套客户端―负载均衡的工具. 简单的说,Ribbon是Netlix发布的开源项目,主要功能是提供客户端的 ...
- 10.redis cluster介绍与gossip协议
一.redis cluster 介绍 自动将数据进行分片,每个 master 上放一部分数据 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的 redis cluster架构下的 ...
- 对Jenkinsfile语法说不,开源项目Jenkins Json Build挺你
对Jenkinsfile语法说不,开源项目Jenkins Json Build挺你 项目背景 我所在的组织项目数量众多,使用的语言和框架也很多,比如Java.ReactNative.C# .NET.A ...
- C#发送邮件三种方法,Localhost,SMTP,SSL-SMTP
C#发送邮件三种方法,Localhost,SMTP,SSL-SMTP 通过.Net FrameWork 2.0下提供的“System.Net.Mail”可以轻松的实现,本文列举了3种途径来发送: 1. ...
- basicInterpreter1.01 支持分支语句
源码:https://files.cnblogs.com/files/heyang78/basicInterpreter-20200531-1.rar 输入: count= print(count) ...
- 在微信公众号"码海"里学了一招:在update语句里使用case when 以避免多次更新导致的数据异常.
需求:将emp表中工资大于一万的降到9成,工资少于一万的乘以1.2. 难点:如果分成两句update执行,在10000附近的值可能会执行两次. 钥匙:在update语句里采用case when,使更新 ...