<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>IHttpClientFactory on 纪伟的个人博客</title><link>https://www.jiwei.space/tags/ihttpclientfactory/</link><description>Recent content in IHttpClientFactory on 纪伟的个人博客</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Wed, 04 Feb 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://www.jiwei.space/tags/ihttpclientfactory/index.xml" rel="self" type="application/rss+xml"/><item><title>HttpClient 的前世今生：从 Socket 耗尽到 .NET 8 韧性管线</title><link>https://www.jiwei.space/posts/dotnet/httpclient-deep-dive/</link><pubDate>Wed, 04 Feb 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/dotnet/httpclient-deep-dive/</guid><description>&lt;p&gt;.NET 里很少有哪个类型像 &lt;code&gt;HttpClient&lt;/code&gt; 这样：API 简单到五分钟就能上手，却又被全社区的工程师集体用错了十年。它的每一个“坑”——socket 耗尽、DNS 不刷新、超时分不清——背后其实都是同一个设计张力的不同投影：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;HTTP 连接是昂贵资源，必须复用；而复用又与世界的变化（DNS、故障、超时）天然冲突。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;理解了这层张力，HttpClient 的“前世今生”就不再是一堆要背诵的最佳实践，而是一条清晰的演进脉络：从无脑 &lt;code&gt;new&lt;/code&gt;、到静态单例、到 &lt;code&gt;IHttpClientFactory&lt;/code&gt;、再到 .NET 8 的标准韧性管线，每一步都是在重新回答“怎么既复用、又保鲜”。&lt;/p&gt;
&lt;h2 id="一前世using-var-client--new-httpclient-为什么是反模式"&gt;&lt;a href="#%e4%b8%80%e5%89%8d%e4%b8%96using-var-client--new-httpclient-%e4%b8%ba%e4%bb%80%e4%b9%88%e6%98%af%e5%8f%8d%e6%a8%a1%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;一、前世：&lt;code&gt;using (var client = new HttpClient())&lt;/code&gt; 为什么是反模式
&lt;/h2&gt;&lt;p&gt;先看那段几乎每个 .NET 新手都写过的代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ← 灾难的起点&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;它看起来人畜无害，但在高并发下，服务会以 &lt;code&gt;Unable to connect to the remote server&lt;/code&gt; / &lt;code&gt;SocketException&lt;/code&gt; 集体阵亡。根因不是 HttpClient 有 bug，而是你误用了 TCP。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TCP 连接关闭后并不会立刻消失。&lt;/strong&gt; 主动关闭方会进入 &lt;code&gt;TIME_WAIT&lt;/code&gt; 状态，持续约 2×MSL（Linux 约 60 秒，Windows 约 240 秒），目的是让网络上残留的报文自然消亡，保证同一个四元组（源 IP:源端口 → 目的 IP:目的端口）能被安全复用。&lt;/p&gt;
&lt;p&gt;问题在于：&lt;code&gt;new HttpClient()&lt;/code&gt; + &lt;code&gt;Dispose&lt;/code&gt; 会把底层 TCP 连接&lt;strong&gt;主动关闭&lt;/strong&gt;，于是每来一个请求，你就消耗一个临时源端口、把它踢进 &lt;code&gt;TIME_WAIT&lt;/code&gt;。而操作系统的临时端口范围是有限的（Linux 约 32768–60999，Windows 约 49152–65535，几万个）。在每秒成百上千请求的压力下，几分钟内端口就会被耗尽——新连接无源端口可用，于是报 &lt;code&gt;SocketException&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这是 .NET 团队当年在官方文档里专门加警告的著名陷阱。&lt;code&gt;Dispose&lt;/code&gt; 不但没帮你，反而&lt;strong&gt;加速&lt;/strong&gt;了耗尽，因为它把本该复用的连接主动掐断了。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;排障信号&lt;/strong&gt;：&lt;code&gt;netstat -an | findstr TIME_WAIT&lt;/code&gt;（Windows）或 &lt;code&gt;ss -tan | grep TIME-WAIT | wc -l&lt;/code&gt;（Linux）能看到成千上万的 TIME_WAIT 指向同一个目的端——就是这个反模式的铁证。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="二第一次救赎静态单例与-dns-陈旧"&gt;&lt;a href="#%e4%ba%8c%e7%ac%ac%e4%b8%80%e6%ac%a1%e6%95%91%e8%b5%8e%e9%9d%99%e6%80%81%e5%8d%95%e4%be%8b%e4%b8%8e-dns-%e9%99%88%e6%97%a7" class="header-anchor"&gt;&lt;/a&gt;二、第一次救赎：静态单例与 DNS 陈旧
&lt;/h2&gt;&lt;p&gt;既然不能每次 &lt;code&gt;new&lt;/code&gt;，那就复用——进程内共享一个静态 &lt;code&gt;HttpClient&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;socket 耗尽立刻消失，因为连接被池化复用、不再频繁关闭。但这套方案运行一段时间后，会在某些场景下出现一种更隐蔽的故障：&lt;strong&gt;服务偶尔连不上，且恰好发生在对端故障转移之后。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这就是 &lt;strong&gt;DNS 陈旧（DNS staleness）&lt;/strong&gt;。&lt;code&gt;HttpClient&lt;/code&gt; 一旦建立连接，就会把 DNS 解析出的 IP 缓存住，此后一直复用这个 IP。当对端服务做了故障转移、蓝绿切换、K8s Pod 重建（IP 变了），你的 client 还在傻傻地连旧 IP，于是请求超时或被拒。&lt;/p&gt;
&lt;p&gt;于是我们陷入了一个死结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;复用连接 → socket 不耗尽，但 DNS 不刷新；&lt;/li&gt;
&lt;li&gt;每次新建 → DNS 是新的，但 socket 耗尽。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是 HttpClient 演进的核心驱动力——&lt;strong&gt;如何在“连接复用”和“端点保鲜”之间找到平衡&lt;/strong&gt;。后续所有方案（&lt;code&gt;IHttpClientFactory&lt;/code&gt;、&lt;code&gt;SocketsHttpHandler.PooledConnectionLifetime&lt;/code&gt;）本质上都在回答这一个问题。&lt;/p&gt;
&lt;h2 id="三原理httpclient-其实是个门面"&gt;&lt;a href="#%e4%b8%89%e5%8e%9f%e7%90%86httpclient-%e5%85%b6%e5%ae%9e%e6%98%af%e4%b8%aa%e9%97%a8%e9%9d%a2" class="header-anchor"&gt;&lt;/a&gt;三、原理：HttpClient 其实是个“门面”
&lt;/h2&gt;&lt;p&gt;要看懂后面的方案，先得看清 HttpClient 的分层。它本身几乎是空的——真正干活的是它持有的 &lt;code&gt;HttpMessageHandler&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HttpClient ← 薄薄的门面（Facade），公开 GetAsync/PostAsync
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HttpMessageHandler 管线 ← 真正的架构在这里
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ DelegatingHandler：日志 / 追踪
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ DelegatingHandler：韧性（重试 / 熔断 / 超时）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ DelegatingHandler：认证 / 签名
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SocketsHttpHandler（主处理器） ← 连接池、HTTP/2、HTTP/3，真正收发字节
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TCP / HTTP/2 / HTTP/3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这张图藏着三层关键设计：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HttpClient 是门面，不是实现。&lt;/strong&gt; 你调用的 &lt;code&gt;GetAsync&lt;/code&gt; 只是把请求顺着一条 handler 链往下传，最后由“主处理器”（primary handler）真正发到网络上。这解释了为什么“换一个 handler”就能换一套行为——比如测试时换成 mock handler，连线都不用发。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;DelegatingHandler&lt;/code&gt; 就是出站中间件。&lt;/strong&gt; 它和 ASP.NET Core 的入站中间件是同一个思想，只不过方向相反——请求在这里被一层层加工：加日志、加重试、加 Authorization 头。横向关注点（cross-cutting concerns）从业务代码里剥离，干净地组合进管线。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;连接池不在 HttpClient 里，而在主处理器里。&lt;/strong&gt; 这是最容易被忽略、却最关键的一点：&lt;strong&gt;HttpClient 是廉价的、可随意创建的；昂贵的是它背后的 handler 和连接池。&lt;/strong&gt; 所以“复用 HttpClient”真正要复用的，是 handler。这一认知直接决定了后面所有的正确用法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="四socketshttphandler现代连接池的真正主角"&gt;&lt;a href="#%e5%9b%9bsocketshttphandler%e7%8e%b0%e4%bb%a3%e8%bf%9e%e6%8e%a5%e6%b1%a0%e7%9a%84%e7%9c%9f%e6%ad%a3%e4%b8%bb%e8%a7%92" class="header-anchor"&gt;&lt;/a&gt;四、SocketsHttpHandler：现代连接池的真正主角
&lt;/h2&gt;&lt;p&gt;从 .NET Core 2.1 起，所有平台的默认主处理器都是 &lt;code&gt;SocketsHttpHandler&lt;/code&gt;（在此之前是基于各平台原生栈的 &lt;code&gt;HttpClientHandler&lt;/code&gt;）。它用纯托管代码重写了传输层，带来几个关键能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;连接池化&lt;/strong&gt;：通过 &lt;code&gt;PooledConnectionLifetime&lt;/code&gt;、&lt;code&gt;PooledConnectionIdleTimeout&lt;/code&gt;、&lt;code&gt;MaxConnectionsPerServer&lt;/code&gt; 精细控制连接的生命周期与并发上限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP/2 多路复用&lt;/strong&gt;：一个 TCP 连接上跑多条并发流，连接不再是“一个请求一个”的粗粒度资源，连接池的数学模型因此改变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP/3 (QUIC)&lt;/strong&gt;：.NET 6+ 支持（需显式开启），基于 UDP，把传输层握手与队头阻塞问题一并优化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低分配&lt;/strong&gt;：基于 &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt; / &lt;code&gt;Memory&amp;lt;T&amp;gt;&lt;/code&gt; 的实现，请求路径上的内存分配大幅下降。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而 &lt;code&gt;PooledConnectionLifetime&lt;/code&gt; 正是解开第二节那个死结的钥匙：&lt;strong&gt;它让池化连接按固定周期轮换。&lt;/strong&gt; 连接不再“活到天荒地老”（默认 &lt;code&gt;InfiniteTimeSpan&lt;/code&gt;，这正是 DNS 陈旧的根源），而是每隔一段时间（比如 5–15 分钟）被关闭、重建——重建时自然重新解析 DNS。于是“复用”与“保鲜”第一次被调和：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 现代“静态单例”的正确写法：复用 client，但让连接定期轮换以刷新 DNS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SocketsHttpHandler&lt;/span&gt; &lt;span class="n"&gt;_handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PooledConnectionLifetime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;disposeHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;注意 &lt;code&gt;disposeHandler: false&lt;/code&gt;——HttpClient 被 dispose 时不去动底层 handler，避免又把连接池掐死。这是 .NET 官方文档里和 &lt;code&gt;IHttpClientFactory&lt;/code&gt; 并列推荐的两种方案之一，特别适合库代码、非 DI 场景或对性能极其敏感的路径。&lt;/p&gt;
&lt;h2 id="五ihttpclientfactorydi-时代的解法"&gt;&lt;a href="#%e4%ba%94ihttpclientfactorydi-%e6%97%b6%e4%bb%a3%e7%9a%84%e8%a7%a3%e6%b3%95" class="header-anchor"&gt;&lt;/a&gt;五、IHttpClientFactory：DI 时代的解法
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;IHttpClientFactory&lt;/code&gt;（.NET Core 2.1，&lt;code&gt;Microsoft.Extensions.Http&lt;/code&gt;）用另一套思路解决同一个问题。它的核心招式一句话能说清：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;创建瞬态的 &lt;code&gt;HttpClient&lt;/code&gt;，但让它共享一个按固定生命周期轮换的 handler 池。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;工厂把“昂贵的 handler”和“廉价的 HttpClient”彻底解耦：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HttpClient&lt;/code&gt; 每次 &lt;code&gt;CreateClient&lt;/code&gt; 都新建一个（瞬态），随便用、随便 dispose；&lt;/li&gt;
&lt;li&gt;但这些 HttpClient 背后挂的是&lt;strong&gt;池化的 handler&lt;/strong&gt;，handler 的 &lt;code&gt;HandlerLifetime&lt;/code&gt; 默认 2 分钟，到期后旧 handler 退役、新 handler 上岗——退役与重建的时机正好让 DNS 自然刷新。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而且工厂创建 HttpClient 时用了 &lt;code&gt;disposeHandler: false&lt;/code&gt;，所以 &lt;strong&gt;dispose 工厂创建的 HttpClient 是安全且廉价的&lt;/strong&gt;——它只释放这一次 message，不碰池化 handler。这纠正了一个流传甚广的误区：“HttpClient 绝对不能 dispose”。对工厂创建的 client，你完全可以 &lt;code&gt;using var resp = ...&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;三种用法，复杂度递增：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 1) 基础用法：从工厂拿一个默认 client&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateClient&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;GetStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://api.example.com&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 2) 命名 client：预配置一组客户端&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;github&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://api.github.com&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;my-blog&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 取用：factory.CreateClient(&amp;#34;github&amp;#34;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 3) 类型化 client（最推荐）：把配置和调用封装进一个类型&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubApi&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://api.github.com&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GitHubApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// HttpClient 由工厂注入，已预配置&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Repo&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GetRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetFromJsonAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Repo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;repos/{owner}/{name}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;类型化 client 把“基础地址、默认头、handler 管线”全部封装在 DI 里，业务代码只管调用——这是 ASP.NET Core 应用里最干净的写法，也是后续挂载韧性 handler 的入口。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;一个要权衡的成本&lt;/strong&gt;：工厂抽象带来极小的额外开销，&lt;code&gt;HandlerLifetime&lt;/code&gt; 设得过短会让 handler 频繁重建、抵消池化收益。默认 2 分钟对绝大多数场景都合适，别瞎调。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="六delegatinghandler把横向关注点编进管线"&gt;&lt;a href="#%e5%85%addelegatinghandler%e6%8a%8a%e6%a8%aa%e5%90%91%e5%85%b3%e6%b3%a8%e7%82%b9%e7%bc%96%e8%bf%9b%e7%ae%a1%e7%ba%bf" class="header-anchor"&gt;&lt;/a&gt;六、DelegatingHandler：把横向关注点编进管线
&lt;/h2&gt;&lt;p&gt;回到第三节那张管线图。&lt;code&gt;DelegatingHandler&lt;/code&gt; 让你能在请求“出门”前和响应“回来”后插入逻辑——加 traceId、统一鉴权、结构化日志。写一个自定义 handler 很直观：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CorrelationIdHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DelegatingHandler&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpResponseMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;HttpRequestMessage&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;N&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAddWithoutValidation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;X-Correlation-Id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 交给下一层&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAddWithoutValidation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;X-Correlation-Id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 注册到某个命名 / 类型化 client 的管线&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubApi&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(...)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHttpMessageHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CorrelationIdHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;注意 handler 的&lt;strong&gt;顺序即语义&lt;/strong&gt;：先注册的在外层、先执行。把“日志”放在最外层，就能记录到含重试在内的完整耗时；把“鉴权”放在韧性层之内，就能让重试带上最新的 token。这种“顺序敏感的洋葱模型”和 ASP.NET Core 中间件如出一辙。&lt;/p&gt;
&lt;h2 id="七韧性从手写重试到-net-8-标准管线"&gt;&lt;a href="#%e4%b8%83%e9%9f%a7%e6%80%a7%e4%bb%8e%e6%89%8b%e5%86%99%e9%87%8d%e8%af%95%e5%88%b0-net-8-%e6%a0%87%e5%87%86%e7%ae%a1%e7%ba%bf" class="header-anchor"&gt;&lt;/a&gt;七、韧性：从手写重试到 .NET 8 标准管线
&lt;/h2&gt;&lt;p&gt;网络请求天生不可靠：会超时、会抖动、会对端短暂故障。所以一个生产级 HTTP 客户端必须自带&lt;strong&gt;韧性（resilience）&lt;/strong&gt;：重试、熔断、超时、降级。这条线同样经历了一次范式升级。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;早期：手写 &lt;code&gt;try/catch&lt;/code&gt; + &lt;code&gt;for&lt;/code&gt; 循环。&lt;/strong&gt; 散落在各处、难以一致、几乎必然漏掉某种异常类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Polly 时代：策略即对象。&lt;/strong&gt; &lt;code&gt;Policy.Handle&amp;lt;HttpRequestException&amp;gt;().OrResult(...).WaitAndRetryAsync(...)&lt;/code&gt; 把“什么算可重试、退避多久、最多几次”抽象成可组合的策略对象，挂到管线上。这是巨大进步，但策略组合的写法仍有学习成本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;.NET 8：标准韧性管线。&lt;/strong&gt; &lt;code&gt;Microsoft.Extensions.Http.Resilience&lt;/code&gt; 在 Polly v8 的 &lt;code&gt;ResiliencePipeline&lt;/code&gt; 之上，提供了一行就能挂载的“出厂合理”配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubApi&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(...)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddStandardResilienceHandler&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 自带：尝试超时 + 重试 + 熔断，比例与退避都已调好&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;AddStandardResilienceHandler&lt;/code&gt; 组合了一条合理的管线：&lt;strong&gt;每次尝试有超时&lt;/strong&gt;（attempt timeout）→ &lt;strong&gt;失败按指数退避重试&lt;/strong&gt; → &lt;strong&gt;连续失败触发熔断&lt;/strong&gt;（circuit breaker）保护下游。默认值是 .NET 团队基于大量实践调出来的“安全默认”，开箱即用；也可通过 &lt;code&gt;HttpStandardResilienceOptions&lt;/code&gt; 精细覆盖。&lt;/p&gt;
&lt;p&gt;还有 &lt;code&gt;AddStandardHedgingHandler()&lt;/code&gt;——&lt;strong&gt;对冲（hedging）&lt;/strong&gt;：发出第一个请求后，若它在指定时间内没返回，就并行再发一个。这是一种针对**尾延迟（tail latency）**的武器：在 p99 抖动严重的分布式系统里（想想 Jeff Dean 那张著名的“延迟尾部放大”图），对冲能把慢请求的尾部显著拉平，代价是多打了一些请求。&lt;/p&gt;
&lt;p&gt;韧性的设计思想值得单独点出来：&lt;strong&gt;它是一组横向关注点，应当作为管线的一部分组合进去，而不是用 &lt;code&gt;try/catch&lt;/code&gt; 在每个调用点重复实现。&lt;/strong&gt; 这正是把“网络不可靠”从业务逻辑里剥离出来的关键一步。&lt;/p&gt;
&lt;h2 id="八设计思想httpclient-教会了我们什么"&gt;&lt;a href="#%e5%85%ab%e8%ae%be%e8%ae%a1%e6%80%9d%e6%83%b3httpclient-%e6%95%99%e4%bc%9a%e4%ba%86%e6%88%91%e4%bb%ac%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;八、设计思想：HttpClient 教会了我们什么
&lt;/h2&gt;&lt;p&gt;回头看，HttpClient 的演进浓缩了几个 .NET 生态里反复出现的设计哲学：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“门面 + 可组合管线”。&lt;/strong&gt; HttpClient 本身是薄门面，真正的架构是那条 handler 链。同样的哲学你能在 ASP.NET Core（入站中间件）、日志（&lt;code&gt;ILoggerProvider&lt;/code&gt; 链）、依赖注入里反复看到——&lt;strong&gt;把核心逻辑做成管线，把变化点做成可插拔的 handler / provider。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“池化昂贵资源，按生命周期保鲜”。&lt;/strong&gt; 连接池的核心张力是“复用 vs 新鲜”。解法不是二选一，而是&lt;strong&gt;给资源加生命周期&lt;/strong&gt;——让池里的连接 / handler 定期轮换，既享受复用的性能，又定期接触真实世界（刷新 DNS）。这是一个可以推广到连接池、缓存、长连接的通用模式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“让正确的事变简单”。&lt;/strong&gt; &lt;code&gt;IHttpClientFactory&lt;/code&gt; + 类型化 client + &lt;code&gt;AddStandardResilienceHandler&lt;/code&gt;，本质上是在引导用户走向“开箱即正确”的默认路径——你只要照着模板写，就不会 socket 耗尽、不会 DNS 陈旧、不会裸奔无重试。好的框架用&lt;strong&gt;合理默认值&lt;/strong&gt;把最佳实践固化下来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“横向关注点即中间件”。&lt;/strong&gt; 鉴权、日志、追踪、韧性，都不该污染业务代码。把它们做成 &lt;code&gt;DelegatingHandler&lt;/code&gt;、按顺序组合进管线，业务方法里就只剩下纯粹的领域调用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="九生产实践与排障清单"&gt;&lt;a href="#%e4%b9%9d%e7%94%9f%e4%ba%a7%e5%ae%9e%e8%b7%b5%e4%b8%8e%e6%8e%92%e9%9a%9c%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;九、生产实践与排障清单
&lt;/h2&gt;&lt;p&gt;把上面的原理落到生产线，常用的判断与排查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;socket 耗尽&lt;/strong&gt;：&lt;code&gt;netstat&lt;/code&gt; / &lt;code&gt;ss&lt;/code&gt; 统计 &lt;code&gt;TIME_WAIT&lt;/code&gt; 数；根因几乎都是某处还在 &lt;code&gt;new HttpClient()&lt;/code&gt;。修法：换工厂或静态单例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接复用率低&lt;/strong&gt;：检查是否“每请求一个 client”；HTTP/2 场景下注意 &lt;code&gt;MaxConnectionsPerServer&lt;/code&gt; 的语义变化（多路复用下不需要太多连接）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时分不清&lt;/strong&gt;：&lt;code&gt;HttpClient.Timeout&lt;/code&gt; 默认 100 秒，&lt;strong&gt;几乎一定要显式覆盖&lt;/strong&gt;。注意超时抛的是 &lt;code&gt;TaskCanceledException&lt;/code&gt;；.NET 6+ 起它会包一个 &lt;code&gt;TimeoutException&lt;/code&gt; 作为 &lt;code&gt;InnerException&lt;/code&gt;，借此区分“客户端超时”和“外部 &lt;code&gt;CancellationToken&lt;/code&gt; 主动取消”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS 故障转移失效&lt;/strong&gt;：云上 / K8s 场景，确认 &lt;code&gt;HandlerLifetime&lt;/code&gt;（工厂）或 &lt;code&gt;PooledConnectionLifetime&lt;/code&gt;（单例）已设成合理值；要求高的场景可叠加 hedging。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;gRPC&lt;/strong&gt;：.NET 的 gRPC 客户端底层就是 HttpClient，同样走工厂与连接池，韧性机制完全通用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="十决策清单我该用哪一种"&gt;&lt;a href="#%e5%8d%81%e5%86%b3%e7%ad%96%e6%b8%85%e5%8d%95%e6%88%91%e8%af%a5%e7%94%a8%e5%93%aa%e4%b8%80%e7%a7%8d" class="header-anchor"&gt;&lt;/a&gt;十、决策清单：我该用哪一种
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;场景&lt;/th&gt;
 &lt;th&gt;推荐&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;库代码 / 非 DI / 极致性能&lt;/td&gt;
 &lt;td&gt;静态 &lt;code&gt;HttpClient&lt;/code&gt; + &lt;code&gt;SocketsHttpHandler{ PooledConnectionLifetime }&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ASP.NET Core 应用&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;IHttpClientFactory&lt;/code&gt; + 类型化 client + &lt;code&gt;AddStandardResilienceHandler()&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;需要拉平尾延迟&lt;/td&gt;
 &lt;td&gt;叠加 &lt;code&gt;AddStandardHedgingHandler()&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;永远别用&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;每请求 &lt;code&gt;new HttpClient()&lt;/code&gt;（无论是否 &lt;code&gt;using&lt;/code&gt;）&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="结语"&gt;&lt;a href="#%e7%bb%93%e8%af%ad" class="header-anchor"&gt;&lt;/a&gt;结语
&lt;/h2&gt;&lt;p&gt;HttpClient 的“难”，不在 API，而在它把一个&lt;strong&gt;分布式系统的本质矛盾&lt;/strong&gt;——网络不可靠、资源要复用、世界会变化——直接暴露给了每一个写业务代码的人。从 socket 耗尽到韧性管线，它的每一次演进都在把这个矛盾往框架深处藏一点，让业务代码更干净一点。&lt;/p&gt;
&lt;p&gt;当你下一次写出 &lt;code&gt;builder.Services.AddHttpClient&amp;lt;T&amp;gt;().AddStandardResilienceHandler()&lt;/code&gt; 时，你其实正站在一条十年的演进线上——背后是无数人踩过的 TIME_WAIT、调过的 DNS、修过的尾延迟。理解了这条脉络，HttpClient 就从一个“容易踩坑的类型”，变成一个“设计优雅的分布式客户端”。&lt;/p&gt;</description></item></channel></rss>