在开发者便利度角度,我们很轻松地使用HttpClient对象发出HTTP请求,只需要关注应用层协议的BaseAddr、Url、ReqHeader、timeout。

实际在HttpClient在源码级别是由 HttpMessageHandler实例发出的请求。

1. 早期.NET HttpClient遇到的Socket滥用/DNS解析问题

早期.NET的HttpClient使用HttpClientHandler, 该handler具备完整的async、proxy、dns、Connection pool 请求一条龙能力。

底层Handler又会构建tcp连接池 ,开发者不注意使用场景和底层原理容易造成Socket滥用、主机端口耗尽(参考资料是:tcp4次挥手,主动端开方不会立即释放端口,存在2min的time_wait状态)。

一般实践会采用单例模式,重用HttpClient对象(也即重用HttpClientHandler), 但此时又会遇到DNS解析问题的尴尬(HttpClient仅在创建时为连接解析DNS,不跟踪DNS服务器指令的TTL)。


意识到重用httpClient带上的dns解析副作用之后, .NET团队和.ASP.NETCore团队分别给出了技术路线来尝试解决这个问题,

前者在.NETCore 2.1 引入了具备对连接池中连接做生命周期管理能力的 SocketsHttpHandler;

后者基于ASP.NETCore框架随处可见的DI能力,实现了针对HttpClientHandler实例的缓存工厂。


2. .NET Core2.1+ HttpClient 改造HttpClientHandler证明自己

新版本的思路是哪里有问题, 我就改造哪里。

.NET Core 2.1改造了HttpClient原始的HttpClientHandler源码, 让其underlyingHandler=SocketsHttpHandler,也就是说在.NETCore2.1起HttpClient的核心Handler实质就是SocketsHttpHandler, HttpClientHandler只是一个套壳。

看上面的UML图,被改造后的套壳HttpClientHandler内置了一个默认的SocketsHttpHandler来完成一条龙HTTP服务 (Dispose工作也全权交给了SocketsHttpHandler), 当然开发者也可以在构建HttpClient实例时指定handler。

SocketsHttpHandler中与连接生命周期相关的三个关键属性:

var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(15), // 限制连接的生命周期,默认无限 Recreate every 15 minutes, 这个配置可用于缓解DNS解析问题
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), // 空闲连接在连接池中的存活时间, <=NET5默认2min, >NET6 1min
MaxConnectionsPerServer = 100, // 定义到每个目标服务节点能建立的最大连接数 未设置 = int.MaxValue };
var sharedClient = new HttpClient(handler);

都聊到此了,在打算重用HttpClient实例时,插入SocketsHttpHandler并调整PooledConnectionLifetime,可缓解DNS解析问题。

3. ASP.NETCore IHttpClientFactory缓存工厂 曲线救国

IHttpClientFactory 充分体现了“计算机领域的任何问题都可以通过增加一个间接的中间层来解决” 这一方法论。

为解决重用HttpClient引起的DNS解析副作用,IHttpClientFactory对实际使用的核心HttpClienthandler开启了缓存工厂模式,在外侧尝试跟踪并控制Handler的存活周期。

① 通过IHttpClientFactory注入的命名的/类型化的HttpClient实例,底层核心的Handler来自缓存字典;

② 缓存字典中的缓存项默认2min,意味着2min时间内产生的命名HttpClient实例都是引用同一个核心HttpMessageHandler实例(LifeTimeTrackingHttpMessageHandler);

public HttpClient CreateClient(string name)
{
ThrowHelper.ThrowIfNull(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 "i");
} return client;
} public HttpMessageHandler CreateHandler(string name)
{
ThrowHelper.ThrowIfNull(name); ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value; // 工厂模式,惰性取值 StartHandlerEntryTimer(entry); // 跟踪缓存项的过期时间 return entry.Handler; }

缓存是用线程安全的字典ConcurrentDictionary以惰性生成的方式实现:

_activeHandlers  =new ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>>(StringComparer.Ordinal);

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

缓存的是LifeTimeTrackingHttpMessageHandler对象,这是一个托管资源。

③ 每个活跃的核心handler上外挂了存活时间, 一旦到期便从活跃字典中移出, 并移动到过期handler队列;

internal sealed class ExpiredHandlerTrackingEntry
{
private readonly WeakReference _livenessTracker; // IMPORTANT: don't cache a reference to `other` or `other.Handler` here.
// We need to allow it to be GC'ed.
public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
{
Name = other.Name;
Scope = other.Scope; _livenessTracker = new WeakReference(other.Handler); // 跟踪LifeTimeTrackingHttpMessageHandler 托管资源
InnerHandler = other.Handler.InnerHandler!; // InnerHandler 是托管资源底层引用的非托管资源
} public bool CanDispose => !_livenessTracker.IsAlive; public HttpMessageHandler InnerHandler { get; } public string Name { get; } public IServiceScope? Scope { get; }
}

托管资源LifeTimeTrackingHttpMessageHandler 不接受dispose(httpclient)的指引,而是由gc跟踪再无HttpClient引用而被清理。

Q: 此时就出现了一个问题, 托管资源已经被gc清理, 那依赖的底层非托管资源什么时候清理的? 这个不清理可是有大问题。

A :这里使用了一个C#高级的用法:弱引用WeakReference:能够在不影响gc的情况下,获得对象的“弱引用”, 并据此知道该实例是不是已经被gc清理了;本文是弱引用_livenessTracker跟踪了托管资源LifeTimeTrackingHttpMessageHandler, 该托管资源被gc清理后_livenessTracker会得到感知。

btw,关于弱引用,我会开一新篇章来讲述。

④ 最后由程序内置的定时清理程序来清理底层非托管资源。

if (entry.CanDispose)       //跟踪到托管对象已经被gc
{
try
{
entry.InnerHandler.Dispose();
entry.Scope?.Dispose();
disposedCount++;
}
catch (Exception ex)
{
Log.CleanupItemFailed(_logger, entry.Name, ex);
}
}
//注意:InnerHandler并不是托管对象LifeTimeTrackingHttpMessageHandler

具体是通过弱引用entry.CanDispose得知引用被gc之后,再去清理底层的非托管资源:InnerHandler.Dispose()。

在使用层面, IHttpClientFactory并非直接管控连接池连接,而是在外层对Handler做存活缓存,故工厂对外只提供了SetHandlerLifetime(TimeSpan.FromMinutes(5)) 这一个配置函数。

总结

本文从早期的HttpClient带来的尴尬(重用HttpClient带来的DNS解析问题), 扩展到.NET团队尝试解决该问题的两个思路。

.NET Core 2.1的思路是增强HttpClient库底层的连接池能力,提供了SocketsHttpHandler来控制连接的生命周期,

IHttpClientFactory的思路是绕过HttpClient本身的问题,在上层用存活缓存的思路来控制HttpClientHandler实例, 充分贯彻了“计算机领域的任何问题都可以通过增加一个间接的中间层来解决”的思想。

3张大图剖析HttpClient和IHttpClientFactory在解决DNS解析问题上的殊途同归的更多相关文章

  1. 一步步教你为网站开发Android客户端---HttpWatch抓包,HttpClient模拟POST请求,Jsoup解析HTML代码,动态更新ListView

    本文面向Android初级开发者,有一定的Java和Android知识即可. 文章覆盖知识点:HttpWatch抓包,HttpClient模拟POST请求,Jsoup解析HTML代码,动态更新List ...

  2. 剖析非同质化代币ERC721-全面解析ERC721标准

    什么是ERC-721?现在我们看到的各种加密猫猫狗狗都是基于ERC-721创造出来的,每只都是一个独一无二的ERC-721代币,不过ERC-721在区块链世界远不止猫猫狗狗,它更大的想象空间在于将物理 ...

  3. PHP将多张小图拼接成一张大图

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> < ...

  4. dede调用第一张大图,非缩略图

    1.找到include/extend.func.php加入现在函数 function firstimg($str_pic) { $str_sub=substr($str_pic,0,-7)." ...

  5. 自己写的jQuery放大镜插件效果(一)(采用一张大图和一张小图片的思路)

    这个思路的方法会带来一个小问题,就是当鼠标放到小图上去时,会开始加载大图片,网速不佳的时候,会出现加载慢的情况.但是放大的效果和你所给出的大图片的清晰度是一样的. 先看效果图: html代码: < ...

  6. HttpClient和HttpURLConnection的使用和区别(上)

    转自:点击打开链接 相信很多Android开发者碰到涉及到Http协议的需求时,都和我一样在犹豫是使用HttpClient还是使用HttpURLConnection呢.我在网上也搜索了很多文章,来分析 ...

  7. Android 6.0删除Apache HttpClient相关类的解决方法

    相应的官方文档如下: 上面文档的大致意思是,在Android 6.0(API 23)中,Google已经移除了Apache HttpClient相关的类,推荐使用HttpUrlConnection. ...

  8. android通过httpClient请求获取JSON数据并且解析

    使用.net创建一个ashx文件,并response.write  json格式 public void ProcessRequest(HttpContext context) { context.R ...

  9. HttpClient post中文乱码解决

    在javase方式下使用HttpClient没有进行任何编码设置,本地从服务端获取到数据不存在中文乱码. 但是将此段代码部署到Tomcat下面出现了中文乱码,此时设置: post.getParams( ...

  10. Angular4---认证---使用HttpClient拦截器,解决循环依赖引用的问题

    在angular4 项目中,每次请求服务端需要添加头部信息AccessToken作为认证的凭据.但如果在每次调用服务端就要写代码添加一个头部信息,会变得很麻烦.可以使用angular4的HttpCli ...

随机推荐

  1. 基于rabbitmq的事件总线

    在这个微服务火热的时代,如果不懂一点微服务相关的技术,想吹下牛都没有法子.于是有必要了解学习一下.所以我最近看了下微服务相关的知识.微服务所涉及的知识是很广的,我这里只是讲一下事件总线,当然,有现成很 ...

  2. HTB-Runner靶机笔记

    HTB-Runner靶机笔记 概述 Runner是HTB上一个中等难度的Linux靶机,它包含以下teamcity漏洞(CVE-2023-42793)该漏洞允许用户绕过身份验证并提取API令牌.以及d ...

  3. 传染病模型 SI

    参考了这篇写的很好的[1],讲了各种模型 因为是各种模型都是用微分方程写的,所以又去学习了一下微分方程 ,真的忘了有没有学过这个,反正一点印象也没有了. 好在[2] 这个文章又把我带回去了. SI 的 ...

  4. EF Core – 乐观并发

    前言 之前写过 EF Core 悲观并发, 这篇主要讲一下乐观并发. 乐观并发的机制可以看这篇. Why Need This? 如果你用 EF Core 做数据管理, 建议你每个 Entity 都配置 ...

  5. 关于建表字段是否该使用not null这个问题你怎么看?

    大家好,我是 V 哥,在数据库设计中,是否使用 NOT NULL 是一个非常重要的决策,直接影响数据完整性.查询性能以及业务逻辑的复杂度.使用 NOT NULL 的关键在于理解业务需求和具体场景. 下 ...

  6. [OI] Kruskal 重构树

    算法介绍 Kruskal 重构树用于快速判断节点的连通性. 考虑到,假如两个节点是联通的,则他们之间总会有一条边被选入最小生成树内,因此他们在最小生成树内也是联通的. 也就是说,我们可以通过求最小生成 ...

  7. AD域下,普通用户环境下安装软件需要管理员权限的解决办法

    原理:将AD域普通用户添加到管理组中,使其拥有管理员权限 做法: 1.切换到AD域管理员账户: 2.计算机 -> 管理 -> 用户和组 -> 组 -> Administrato ...

  8. LeetCode 407. 接雨水 II (优先队列)

    参考 优先队列的思路解决接雨水II,逐行解释 从最外圈开始不断向内遍历,如果内部的高度小于外部的高度,则证明该位置可以蓄水,否则不能,水会顺着该外圈流出去. 每次都处理外圈高度最小的那个位置 a,遍历 ...

  9. CentOS 7 下通过 Cython 编写 python 扩展

    1. 安装 python 和 python-devel(没有后者,install 的时候会报错 "Scanners.c:21:20: fatal error: Python.h: No su ...

  10. 高通ramdump

    背景 高通平台下提供了一个工具,专门用来抓取内核死机以后的dump信息.如果只是非系统层面的crash(例如底层应用,安卓程序),则不能抓取dump信息. 在阅读一些文档的时候知道有这个功能,但是一直 ...