Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >三张大图剖析HttpClient和IHttpClientFactory在DNS解析问题上的殊途同归

三张大图剖析HttpClient和IHttpClientFactory在DNS解析问题上的殊途同归

作者头像
有态度的马甲
发布于 2025-04-07 06:19:44
发布于 2025-04-07 06:19:44
9700
代码可运行
举报
文章被收录于专栏:精益码农精益码农
运行总次数:0
代码可运行

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

实际在HttpClient请求在源码级别是 HttpMessageHandler在躬身前行。

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

早期.NET的HttpClient使用HttpClientHandler[1], 该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[2], HttpClientHandler只是一个套壳。

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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);

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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以惰性生成的方式实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
_activeHandlers = new ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>>(StringComparer.Ordinal);

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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[5]:能够在不影响gc的情况下,获得对象的“弱引用”, 并据此知道该实例是不是已经被gc清理了;本文是弱引用_livenessTracker跟踪了托管资源LifeTimeTrackingHttpMessageHandler, 该托管资源被gc清理后_livenessTracker会得到感知。

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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))  这一个配置函数。

IHttpCLientFactory 工厂除了具备 “通过管理HttpClientHandler实例的缓存生存期,避免手动管理 HttpClient 生存期时出现的DNS问题”, 还具有

  • HttpClient实例的产生更符合.NET 框架的调性:DI、 以委托方式配置HttpClient中间件的惯例
  • 中心化配置、 命名或者类型化客户端
  • 提供基于 Polly 的中间件的扩展方法,以利用 HttpClient 中的委托处理程序。
  • (通过 ILogger)添加可配置的记录体验,以处理工厂创建的客户端发送的所有请求。

总结

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

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

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

本篇文字和图片均为原创,读者可结合图片探索源码, 欢迎反馈 ~。。~。

参考资料

[1]

早期.NET的HttpClient使用HttpClientHandler: https://github.com/microsoft/referencesource/blob/51cf7850defa8a17d815b4700b67116e3fa283c2/System/net/System/Net/Http/HttpClientHandler.cs#L917

[2]

SocketsHttpHandler: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs

[3]

LifeTimeTrackingHttpMessageHandler: https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Http/src/LifetimeTrackingHttpMessageHandler.cs

[4]

过期handler队列: https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Http/src/ExpiredHandlerTrackingEntry.cs

[5]

弱引用WeakReference: https://learn.microsoft.com/en-us/dotnet/api/system.weakreference?view=net-9.0

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-11-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 精益码农 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
C#内存泄漏的成因、检测与预防策略
C#作为一种托管语言,虽由.NET框架的垃圾回收机制自动管理内存,但仍存在多种内存泄漏的可能性。内存泄漏会导致应用程序内存占用持续增长,最终引发性能下降甚至崩溃。在C#程序中,主要的内存泄漏原因包括事件订阅未取消、静态变量持有引用、非托管资源未释放以及匿名函数闭包等。针对这些潜在问题,开发者可以通过使用Visual Studio内存分析器或JetBrains dotMemory等工具进行检测,并采取相应的预防措施。本文将系统分析内存泄漏的成因、检测方法及预防策略,帮助开发者提升C#程序的内存管理能力。
代码小李
2025/05/16
900
.net core HttpClient 使用之掉坑解析(一)
在我们开发当中经常需要向特定URL地址发送Http请求操作,在.net core 中对httpClient使用不当会造成灾难性的问题,这篇文章主要来分享.net core中通过IHttpClientFactory 工厂来使用HttpClient的正确打开方式。
Jlion
2022/04/07
9920
.net core HttpClient 使用之掉坑解析(一)
.NET Core 3.0之深入源码理解HttpClientFactory(一)
创建HttpClient实例的时候,在内部会创建HttpMessageHandler链,我们知道HttpMessageHandler是负责建立连接的抽象处理程序,所以HttpClient的维护实际上就是维护HttpMessageHandler的使用,释放HttpClient并不会及时释放连接,而通常情况下一般是创建全局使用的HttpClient实例,以减少重复连接的次数。当然这种方式所带来的的弊端也是显而易见的,因为当前的HttpClient实例所指向的服务器发生问题或者DNS发生变更,那么该实例是无法做到自动更新指向的。
AI.NET 极客圈
2019/07/25
1.1K0
.NET Core 3.0之深入源码理解HttpClientFactory(一)
.net core HttpClient 使用之消息管道解析(二)
前面分享了 .net core HttpClient 使用之掉坑解析(一),今天来分享自定义消息处理HttpMessageHandler和PrimaryHttpMessageHandler 的使用场景和区别
Jlion
2022/04/07
7170
.net core HttpClient 使用之消息管道解析(二)
HttpClientFactory的套路,你知多少?
ASP.NET Core 在 2.1 之后推出了具有弹性 HTTP 请求能力的 HttpClient 工厂类 HttpClientFactory。
有态度的马甲
2020/04/15
1.5K0
HttpClientHandler VS SocketsHttpHandler
.NET Framework 和 .NET Core 2.0 及更低版本中由 HttpClient 使用的默认消息处理程序为HttpClientHandler。
Chester Chen
2024/03/13
1430
HttpClientHandler VS SocketsHttpHandler
.NetCore 2.1以后的HttpClient最佳实践
ASP.NET Core 2.1中出现一个新的HttpClientFactory功能,
leon公众号精选
2022/04/27
1.2K0
.NetCore 2.1以后的HttpClient最佳实践
dotnet 6 HttpClientHandler 和 SocketsHttpHandler 有什么差别
本文来告诉大家在 dotnet 6 的 HttpClientHandler 和 SocketsHttpHandler 两个类型有什么不同
林德熙
2022/08/12
1.1K0
.NET Core 3.0之深入源码理解HttpClientFactory(二)
上一篇文章讨论了通过在ConfigureServices中调用services.AddHttpClient()方法,并基于此进一步探讨了DefaultHttpClientFactory是如何创建HttpClient实例和HttpMessageHandler实例的,并了解了DefaultHttpClientFactory内部维护者一个定时器和两个HttpMessageHandler对象集合,以定期清理无效的 HttpMessageHandler对象,详细的内容可以点击链接跳转,接下来我会接着前一篇文章继续展开相关讨论。
AI.NET 极客圈
2019/07/30
8860
ASP.NET Core 6框架揭秘实例演示[17]:利用IHttpClientFactory工厂来创建HttpClient
在一个采用依赖注入框架的应用中,我们一般不太推荐利用手工创建的HttpClient对象来进行HTTP调用,使用的HttpClient对象最好利用注入的IHttpClientFactory工厂来创建。前者引起的问题,以及后者带来的好处,将通过如下这几个演示程序展现出来。IHttpClientFactory类型由“Microsoft.Extensions.Http”这个NuGet包提供,“Microsoft.NET.Sdk.Web”SDK具有该包的默认引用。如果采用“Microsoft.NET.Sdk”这个SDK,需要添加该包的引用。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)
蒋金楠
2022/05/09
8860
ASP.NET Core 6框架揭秘实例演示[17]:利用IHttpClientFactory工厂来创建HttpClient
ASP.NET Core 中的内存管理和垃圾回收 (GC)
GC 会分配堆段,其中每个段都是一系列连续的内存。 置于堆中的对象归类为 3 个代系之一:0、1 或 2。 代系可确定 GC 尝试在应用不再引用的托管对象上释放内存的频率。 编号较低的代系会更加频繁地进行 GC。 对象会基于其生存期从一个代系移到另一个代系。 随着对象生存期延长,它们会移到较高代系。 如前所述,较高代系进行 GC 的频率较低。 短期生存的对象始终保留在第 0 代中。 例如,在 Web 请求存在期间引用的对象的生存期较短。 应用程序级别单一实例通常会迁移到第 2 代。 当 ASP.NET Core 应用启动时,GC 会:
用户9857551
2023/10/17
5390
ASP.NET Core 中的内存管理和垃圾回收 (GC)
基于 keyed DI 的 HttpClient
.NET 8 中依赖注入引入了 keyed service 的支持 可以参考 .NET 8 中的 KeyedService,.NET 9 中改进了 HttpClient 基于名称的 HttpClient 依赖注入,使用基于名称的 HttpClient 的时候可以直接使用 keyed service 来解析了
郑子铭
2025/03/29
460
基于 keyed DI 的 HttpClient
.NetCore HttpClient发送请求的时候为什么自动带上了一个RequestId头部?
最近在公司有个系统需要调用第三方的一个webservice。本来调用一个下很简单的事情,使用HttpClient构造一个SOAP请求发送出去拿到XML解析就是了。 可奇怪的是我们的请求在运行一段时间后就会被服务器504给拒绝掉了。导致系统无法使用,用户叫苦连天。 古怪就古怪在这个问题不是每次都会出现,是隔三差五的查询,每次修改完代码发布上去以为好了, 过了两天又不行了,简直让人奔溃。
MJ.Zhou
2020/11/11
1.2K0
.NetCore HttpClient发送请求的时候为什么自动带上了一个RequestId头部?
ASP.NET Core 6框架揭秘实例演示[18]:HttpClient处理管道
在《利用IHttpClientFactory工厂来创建HttpClient》之后,我们将关注点放到HttpClient对象上。我们知道ASP.NET的核心就是由中间件组成的请求处理管道,HttpClient也采用了类似的设计。HttpClient管道由一组HttpMessageHandler对象构成,这些HttpMessageHandler相当于ASPNET的中间件。如下这些示例演示帮助我们更清楚地认识HttpMessageHandler处理管道。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)
蒋金楠
2022/05/09
5820
ASP.NET Core 6框架揭秘实例演示[18]:HttpClient处理管道
.NET性能优化的10个关键教训:资深工程师的实战经验
在作为高级软件工程师开发高规模.NET应用的十多年中,我亲历了众多性能挑战。有些经验来自深夜的生产事故,有些来自艰难的优化冲刺。以下是团队和我通过惨痛教训总结的十大最具影响力的性能优化实践。
郑子铭
2025/04/24
1550
.NET性能优化的10个关键教训:资深工程师的实战经验
dotnet 6 精细控制 HttpClient 网络请求超时
本文告诉大家如何在 dotnet 6 下使用 HttpClient 更加精细的控制网络请求的超时,实现 HttpWebRequest 的 ReadWriteTimeout 功能
林德熙
2022/08/12
1.3K0
C# HttpClient使用和注意事项,.NET Framework连接池并发限制
HttpClient实例是执行网络请求的设置集合,每个实例会使用一个连接池。通过这段描述我们知道实际使用HttpClient的时候我们只需要实例化一个就行了,在处理程序实例内池连接,并在多个请求之间重复使用连接。也就是官方提倡的使用单个实例,如果每次请求就实例化一个HttpClient,则会创建不必要的连接降低性能,并且TCP 端口不会在连接关闭后立即释放。
SpringSun
2023/02/24
3.2K0
HttpClientFactory日志不好用,自己扩展一个?
  .NetCore2.1新推出HttpClientFactory工厂类, 替代了早期的HttpClient,并新增了弹性Http调用机制 (集成Policy组件)。
心莱科技雪雁
2019/12/11
1.5K0
HttpClientFactory日志不好用,自己扩展一个?
C# HttpClient 请求认证、数据传输笔记
客户端请求服务器时,需要通过授权认证许可,方能获取服务器资源,目前比较常见的认证方式有 Basic 、JWT、Cookie。
痴者工良
2021/04/26
2.6K0
.Net Core HttpClient处理响应压缩「建议收藏」
在上篇文章[ASP.NET Core中的响应压缩]中我们谈到了在ASP.NET Core服务端处理关于响应压缩的请求,服务端的主要工作就是根据Content-Encoding头信息判断采用哪种方式压缩并返回。之前在群里有人问道过,现在的网络带宽这么高了还有必要在服务端针对请求进行压缩吗?确实,如今分布式和负载均衡技术这么成熟,很多需要处理高并发大数据的场景都可以通过增加服务器节点来进行。但是,在资源受限的情况下,或者是还没必要为了某一个点去增加新的服务器节点的时候,我们还是要采用一些程序本身的常规处理手段来进行处理。笔者个人认为响应压缩的使用场景是这样的,在带宽压力比较紧张的情况,且CPU资源比较充足的情况下,使用响应压缩整体效果还是比较明显的。
全栈程序员站长
2022/09/14
7480
推荐阅读
相关推荐
C#内存泄漏的成因、检测与预防策略
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验