简介
HttpClient是C#中用于发送/接收HTTP请求的核心类,属于 System.Net.Http 命名空间。它是 .NET 中处理网络通信的现代 API,设计目标是替代早期的 WebClient/WebRequest/WebResponse/HttpWebRequest,支持异步编程、灵活配置和高性能网络交互,广泛用于调用 REST API、与 Web 服务通信、文件上传 / 下载等场景。
相对过时的类库,它的核心优势如下
特性 | 说明 |
---|---|
异步优先 | 所有方法都返回Task,支持async/await模式,避免线程阻塞 |
连接池复用 | 自动复用TCP连接(基于 Connection: keep-alive),减少重复握手 |
完善的Stream处理 | 支持大文件流式读取内容,避免缓冲区溢出 |
线程安全 | 实例本身是线程安全的(但需注意配置不可变,比如DNS),推荐复用实例(避免频繁创建导致端口耗尽)。 |
灵活的请求配置 | 支持链式配置自定义请求头,超时时间,代理,编码,SSL等功能 |
继承第三方HTTP库 | 可以替换默认的SocketsHttpHandler,来实现自定义HTTP请求库 |
发送请求
//GETHttpResponseMessage response = await _httpClient.GetAsync("https://www.baidu.com");response.EnsureSuccessStatusCode(); // 检查状态码是否为 200-299,否则抛异常string json = await response.Content.ReadAsStringAsync(); // 读取响应内容Console.WriteLine($"响应内容:{json}");//POSTvar formContent = new MultipartFormDataContent();var fileStream = new StreamContent(File.OpenRead("D:\\xxxx.jpg"));fileStream.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg");formContent.Add(fileStream,"file","test.jpg"); //"file" 是表单字段名HttpResponseMessage response = await _httpClient.PostAsync("https://api.example.com/upload", formContent);response.EnsureSuccessStatusCode();Console.WriteLine("文件上传成功");
处理响应
HttpResponseMessage 包含响应的状态码、头部、内容等信息,关键属性如下:
属性/方法 | 说明 |
---|---|
StatusCode | HTTP 状态码(如 200 OK、404 Not Found)。 |
Headers | 响应头部(如 Content-Type、Server)。 |
Content | 响应内容(类型为 HttpContent),支持读取为字符串、流、字节数组等。 |
EnsureSuccessStatusCode() | 若状态码非成功(2xx),抛出 HttpRequestException |
ReadAsStringAsync() | 异步读取内容为字符串(适合小数据,如 JSON)。 |
ReadAsStreamAsync() | 异步读取内容为流(适合大文件下载,避免内存占用)。 |
ReadAsByteArrayAsync() | 异步读取内容为字节数组(适合二进制数据,如图像) |
自定义配置
通过 HttpClient 的属性和 HttpClientHandler 可以自定义请求行为
配置项 | 说明 |
---|---|
timeout | 请求超时时间(默认 100 秒),设置为 Timeout.InfiniteTimeSpan 表示无超时 |
DefaultRequestHeaders | 所有请求默认携带的头部(如 User-Agent、Authorization) |
BaseAddress | 基础 URL后续请求只需指定相对路径 |
HttpClientHandler | 底层处理器,可配置代理、证书验证、自动重定向、压缩等 |
var handler = new HttpClientHandler { Proxy = new WebProxy("http://proxy.example.com:8080"), // 设置代理 UseProxy = true, ServerCertificateCustomValidationCallback = (req, cert, chain, errors) => true // 跳过证书验证(仅测试用)};var httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30), // 30 秒超时 BaseAddress = new Uri("https://api.example.com/")};// 添加默认请求头(如认证令牌)httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer","your_access_token");
结构解析
HttpClient
用户入口,封装SendAsync()方法,提供GET/POST/PUT/DELETE等友好API,协调请求发送于响应接收。HttpMessageInvoker
HttpClient基类,持有SendAsync()的具体类。并实现IDisposable,实现资源自动清理。HttpMessageHandler/HttpClientHandler
真正发起请求的底层抽象与实现,定义了处理HTTP请求的核心逻辑,由其派生类实现跨平台
。SocketsHttpHandler
.NET Core 2.1之后默认的HTTP处理器,直接基于System.Net.Sockets,性能高且跨平台。DelegatingHandler
DelegatingHandler是一个抽象类,以责任链模式
拓展请求处理逻辑,可以通过继承DelegatingHandler,以中间件的方式实现自定义逻辑(日志,重试,退让,缓存等)HttpRequestMessage/HttpResponseMessage
HttpRequestMessage:表示 HTTP 请求,包含 Method(如 HttpMethod.Get)、RequestUri、Headers、Content(请求体)等属性。
HttpResponseMessage:表示 HTTP 响应,包含 StatusCode(状态码)、Headers、Content(响应体)、ReasonPhrase(状态描述)等属性。HttpContent
请求体与响应体的基类,定义了内容的通用操作,再根据不同的数据,派生出不同的子类。
StringContent:文本内容(如 JSON、HTML)。
ByteArrayContent:二进制字节数组内容(如图像、文件)。
StreamContent:流式内容(如大文件上传)。
MultipartFormDataContent:表单内容(支持文件上传)
请求处理流程
.NET 9中优化
https://devblogs.microsoft.com/dotnet/dotnet-9-networking-improvements/#community-contributions
弹性处理(Polly)
总所周知,互联网是不稳定的,可能会网络波动、服务暂不可用等导致的瞬态故障。因此,一个健壮HttpClient还需要实现重试,断路,回退,超时
等弹性处理,避免因单次失败直接终止业务流程。
Polly 提供了 6 大核心策略,覆盖常见的弹性需求:
- 重试(Retry)
当操作失败时(如抛异常或返回特定状态码),自动重试若干次,适用于可自愈的瞬态故障(如网络抖动)。
var retryPolicy = Policy .Handle<HttpRequestException>() // 处理 HTTP 请求异常 .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode) // 或非成功状态码 .WaitAndRetryAsync( retryCount: 3, sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // 指数退避 onRetry: (result, sleepDuration, retryCount, context) => { Console.WriteLine($"重试 {retryCount} 次,等待 {sleepDuration},上次结果:{result.Exception?.Message ?? result.Result.StatusCode.ToString()}"); } );
- 断路(Circuit Breaker)
当故障频率超过阈值时,主动 “断开” 电路(拒绝后续请求),防止级联故障(如服务已崩溃,继续重试会加重负载)。
// 定义断路策略:10 秒内 5 次失败则断路 30 秒var circuitBreaker = Policy.Handle<HttpRequestException>().OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode).CircuitBreakerAsync(5,TimeSpan.FromSeconds(30));
- 回退(Fallback)
当操作失败时,提供一个 “备用方案”(如返回缓存数据、默认值),避免用户看到错误。
// 定义回退策略:失败时返回预设的默认响应var fallbackPolicy = Policy .Handle<HttpRequestException>() .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode) .FallbackAsync( fallbackValue: new HttpResponseMessage(HttpStatusCode.OK) // 默认成功响应 { Content = new StringContent("备用数据(来自缓存或默认值)") }, onFallbackAsync: (result, context) => { Console.WriteLine($"执行回退,原错误:{result.Exception?.Message}"); return Task.CompletedTask; } );
- 超时(Timeout)
限制操作的执行时间,避免长时间等待(如数据库查询超时)。
// 定义超时策略:操作超过 10 秒未完成则抛 TimeoutRejectedExceptionvar timeoutPolicy = Policy .TimeoutAsync( timeout: TimeSpan.FromSeconds(10), onTimeoutAsync: (context, timeout, task) => { Console.WriteLine($"操作超时,已等待 {timeout}"); return Task.CompletedTask; } );
- 组合
Polly支持将多个策略组合,按照组合顺序执行,用以应对复杂场景。
比如先重试,重试失败触发断路,断路期间执行回退。
// 组合策略,先执行最外层。var wrappedPolicy = Policy.WrapAsync(fallbackPolicy,//最外层:失败时回退circuitBreakerkPolicy,//中层:断路保护retryPolicy //内层:重试);
FAQ
为什么不推荐New HttpClient()的方式?
上面讲到,HttpClient是线程安全的,那为什么不推荐使用New HttpClient()呢?
主要是因为每一个HttpClient都会有一个独立的连接池造成的。
端口耗尽
每个 HttpClient 实例默认使用独立的 SocketsHttpHandler,在其内部按照Authority(https://xxxx.com:443)进行分组管理TCP连接。如果是同一个Authority,则会复用连接。
但如果是通过New HttpClient()的方式创建,即时是同一个URL,也会为分配新的端口号,无法实现复用,导致端口耗尽TIME_WAIT 状态堆积
当TCP连接关闭时,发起端会进入TIME_WAIT状态,并等待2MSL(约60s)。以确保接收端收到最终的ACK包。如若HttpClient被频繁创建和销毁,其底层的TCP连接会大量处于TIME_WAIT 状态,进一步加剧端口耗尽。无法统一配置与拓展
直接new HttpClient 难以统一管理公共配置,如代理、超时、证书验证等,每个实例都要配置一遍,代码非常冗余
。
且无法便捷集成扩展功能(如重试策略、断路机制、日志记录),这些需要通过 DelegatingHandler 实现,但 new 的方式难以统一添加中间件
眼见为实
为什么不推荐单例/静态的HttpClient?
既然不推荐New HttpClient(),那我将HttpClient设单例或者静态的行不行?
答案是也不行,因为HttpClient在首次解析域名后,会缓存DNS结果,默认缓存时间取决于操作系统和 DNS 服务器配置。
如果HttpClient实例被长期保留,当DNS记录更新时(比如服务器IP变更),会导致请求失败或者连接到旧服务器
。
眼见为实
连接池缓存的地址,是以传入的URL作为Key。不是最终的IP地址,因此需要依赖DNS缓存来动态解析服务器IP地址。
TIME_WAIT的优化几个思路
TCP的四次挥手是在内核态中进行处理的,我们难以在用户态层面进行大刀阔斧的优化
。
我们可以通过以下几种方式来进行小幅度优化:
- 开启端口复用(内核态)
调整操作系统配置,允许系统重用处于 TIME_WAIT 状态的端口。 - 缩短TIME_WAIT时间(内核态)
TIME_WAIT 状态的默认持续时间是 60 秒,缩短此值可减少状态堆积。但会增加旧数据包干扰新连接的风险。 - 开启HTTP2(用户态)
HTTP/2 支持 单 TCP 连接上的多路复用,多个请求 / 响应可并行传输,显著减少需要创建 / 关闭的 TCP 连接数量。
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler{ EnableHttp2 = true // 显式启用 HTTP/2(默认自动协商)});
- 禁用 Nagle 算法(用户态)
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler{ ConnectCallback = async (context, cancellationToken) => { var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); socket.NoDelay = true; // 禁用 Nagle 算法(减少延迟) await socket.ConnectAsync(context.DnsEndPoint, cancellationToken); return new NetworkStream(socket, ownsSocket: true); }});
- 延长连接空闲时间(用户态)
new SocketsHttpHandler{ PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), // 空闲 2 分钟后关闭(而非立即关闭) PooledConnectionLifetime = TimeSpan.FromMinutes(10) // 连接最长存活 10 分钟(避免频繁轮换)};
需要注意一点,服务器通常作为被动关闭方,一般不会产生大量TIME_WAIT连接,但在微服务大行其道的今天,服务器与服务器之间的通信,服务器也会作为发起方,导致出现大量的TIME_WAIT。
IHttpClientFactory的优势?
为了解决HttpClient端口耗尽与DNS缓存问题。.NET提供了IHttpClientFactory。
连接池复用与端口管理
IHttpClientFactory 负责管理 HttpClient 实例的生命周期
,共享底层 SocketsHttpHandler 和连接池
,避免重复创建连接池和端口.统一配置与扩展
通过AddHttpClient
注册客户端,集中配置,统一管理
serviceCollection.AddHttpClient<BaiduAPIService>(configure =>{configure.BaseAddress = new Uri("https://www.baidu.com");}).AddHttpMessageHandler<CustomerHandler>();
- DNS动态更新
通过 SetHandlerLifetime 配置 HttpMessageHandler 的生命周期(默认 2 分钟),到期后自动重建 Handler 并重新解析 DNS,避免缓存旧 IP。
serviceCollection.AddHttpClient() .SetHandlerLifetime(TimeSpan.FromMinutes(5));
高并发情况下,首次启动很慢?
HttpClient底层也用Task获取HttpConnection,当流量瞬增时,线程池创建新线程需要时间。等线程池预热
后,便会恢复正常。
完整示例
点击查看代码
internal class Program { static async Task Main(string[] args) { #region Policy // 定义重试策略:失败后重试 3 次,每次等待时间指数递增(1s → 2s → 4s) var retryPolicy = Policy .Handle<HttpRequestException>() // 处理 HTTP 请求异常 .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode) // 或非成功状态码 .WaitAndRetryAsync( retryCount: 3, sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // 指数退避 onRetry: (result, sleepDuration, retryCount, context) => { Console.WriteLine($"重试 {retryCount} 次,等待 {sleepDuration},上次结果:{result.Exception?.Message ?? result.Result.StatusCode.ToString()}"); } ); // 定义断路策略:10 秒内 5 次失败则断路 30 秒 var circuitBreakerkPolicy = Policy .Handle<HttpRequestException>() .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode) .CircuitBreakerAsync( 5, TimeSpan.FromSeconds(30) ); // 定义回退策略:失败时返回预设的默认响应 var fallbackPolicy = Policy .Handle<HttpRequestException>() .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode) .FallbackAsync( fallbackValue: new HttpResponseMessage(HttpStatusCode.OK) // 默认成功响应 { Content = new StringContent("备用数据(来自缓存或默认值)") }, onFallbackAsync: (result, context) => { Console.WriteLine($"执行回退,原错误:{result.Exception?.Message}"); return Task.CompletedTask; } ); // 定义超时策略:操作超过 10 秒未完成则抛 TimeoutRejectedException var timeoutPolicy = Policy .TimeoutAsync( timeout: TimeSpan.FromSeconds(10), onTimeoutAsync: (context, timeout, task) => { Console.WriteLine($"操作超时,已等待 {timeout}"); return Task.CompletedTask; } ); // 组合策略,先执行最外层。 var wrappedPolicy = Policy.WrapAsync( fallbackPolicy,//最外层:失败时回退 circuitBreakerkPolicy,//中层:断路保护 retryPolicy //内层:重试 ); #endregion var serviceCollection = new ServiceCollection(); serviceCollection .AddHttpClient<BaiduAPIService>(configure => { configure.BaseAddress = new Uri("https://www.baidu.com"); }) .AddHttpMessageHandler<CustomerHandler>() .AddAsKeyed()//.NET 9新特性Keyed DI .AddPolicyHandler(wrappedPolicy); // 添加弹性策略 serviceCollection.TryAddSingleton<CustomerHandler>(); var services = serviceCollection.BuildServiceProvider(); var httpClient= services.GetRequiredService<BaiduAPIService>(); var result= await httpClient.GetStringAsync(); Console.ReadKey(); } }internal class BaiduAPIService { private readonly HttpClient _httpClient; public BaiduAPIService(HttpClient httpClient) { _httpClient = httpClient; } public async Task<string?> GetStringAsync(string? url=null) { var response= await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return null; var result= await response.Content.ReadAsStringAsync(); return result; } } /// <summary> /// AOP /// </summary> class CustomerHandler:DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { //before Console.WriteLine($"请求前:{request.Method.Method}"); var response= await base.SendAsync(request, cancellationToken); //after Console.WriteLine($"请求后:{await response.Content.ReadAsStringAsync()}"); return response; } }