找回密码
 立即注册
首页 业界区 业界 C#网络编程(四)----HttpClient

C#网络编程(四)----HttpClient

挫莉虻 2025-6-2 23:36:12
简介

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请求库发送请求
  1. //GET
  2. HttpResponseMessage response = await _httpClient.GetAsync("https://www.baidu.com");
  3. response.EnsureSuccessStatusCode(); // 检查状态码是否为 200-299,否则抛异常
  4. string json = await response.Content.ReadAsStringAsync(); // 读取响应内容
  5. Console.WriteLine($"响应内容:{json}");
  6. //POST
  7. var formContent = new MultipartFormDataContent();
  8. var fileStream = new StreamContent(File.OpenRead("D:\\xxxx.jpg"));
  9. fileStream.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg");
  10. formContent.Add(fileStream, "file", "test.jpg"); // "file" 是表单字段名
  11. HttpResponseMessage response = await _httpClient.PostAsync("https://api.example.com/upload", formContent);
  12. response.EnsureSuccessStatusCode();
  13. Console.WriteLine("文件上传成功");
复制代码
处理响应

HttpResponseMessage 包含响应的状态码、头部、内容等信息,关键属性如下:
属性/方法说明StatusCodeHTTP 状态码(如 200 OK、404 Not Found)。Headers响应头部(如 Content-Type、Server)。Content响应内容(类型为 HttpContent),支持读取为字符串、流、字节数组等。EnsureSuccessStatusCode()若状态码非成功(2xx),抛出 HttpRequestExceptionReadAsStringAsync()异步读取内容为字符串(适合小数据,如 JSON)。ReadAsStreamAsync()异步读取内容为流(适合大文件下载,避免内存占用)。ReadAsByteArrayAsync()异步读取内容为字节数组(适合二进制数据,如图像)自定义配置

通过 HttpClient 的属性和 HttpClientHandler 可以自定义请求行为
配置项说明timeout请求超时时间(默认 100 秒),设置为 Timeout.InfiniteTimeSpan 表示无超时DefaultRequestHeaders所有请求默认携带的头部(如 User-Agent、Authorization)BaseAddress基础 URL后续请求只需指定相对路径HttpClientHandler底层处理器,可配置代理、证书验证、自动重定向、压缩等
  1. var handler = new HttpClientHandler {
  2.     Proxy = new WebProxy("http://proxy.example.com:8080"), // 设置代理
  3.     UseProxy = true,
  4.     ServerCertificateCustomValidationCallback = (req, cert, chain, errors) => true // 跳过证书验证(仅测试用)
  5. };
  6. var httpClient = new HttpClient(handler) {
  7.     Timeout = TimeSpan.FromSeconds(30), // 30 秒超时
  8.     BaseAddress = new Uri("https://api.example.com/")
  9. };
  10. // 添加默认请求头(如认证令牌)
  11. httpClient.DefaultRequestHeaders.Authorization =
  12.     new AuthenticationHeaderValue("Bearer", "your_access_token");
复制代码
结构解析

1.png


  • HttpClient/HttpMessageInvoker
    用户入口,提供GET/POST/PUT/DELETE等友好API,协调请求发送于响应接收。
    HttpClient本身是轻量级对象,仅包含了配置信息和对HttpMessageHandler的引用。
  • HttpMessageHandler
    HttpMessageHandler是 核心抽象类,定义了处理 HTTP 请求的基本行为。它通过抽象方法 SendAsync 接收 HttpRequestMessage,并返回 HttpResponseMessage。
  • HttpClientHandler
    HttpClientHandler是HttpMessageHandler 的具体子类,
    是早期 .NET中默认的 HTTP 处理程序。在.NET Core 2.1后,它演变为兼容层,底层通过 SocketsHttpHandler 实现网络通信,保持 API 兼容性。
  • SocketsHttpHandler
    这才是持有网络资源(TCP 连接池、TLS 会话)的核心组件,.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:表单内容(支持文件上传)
请求处理流程

2.png

.NET 9中优化

https://devblogs.microsoft.com/dotnet/dotnet-9-networking-improvements/#community-contributions
弹性处理(Polly)

总所周知,互联网是不稳定的,可能会网络波动、服务暂不可用等导致的瞬态故障。因此,一个健壮HttpClient还需要实现重试,断路,回退,超时等弹性处理,避免因单次失败直接终止业务流程。
Polly 提供了 6 大核心策略,覆盖常见的弹性需求:

  • 重试(Retry)
    当操作失败时(如抛异常或返回特定状态码),自动重试若干次,适用于可自愈的瞬态故障(如网络抖动)。
  1. var retryPolicy = Policy
  2.     .Handle<HttpRequestException>() // 处理 HTTP 请求异常
  3.     .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode) // 或非成功状态码
  4.     .WaitAndRetryAsync(
  5.         retryCount: 3,
  6.         sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // 指数退避
  7.         onRetry: (result, sleepDuration, retryCount, context) =>
  8.         {
  9.             Console.WriteLine($"重试 {retryCount} 次,等待 {sleepDuration},上次结果:{result.Exception?.Message ?? result.Result.StatusCode.ToString()}");
  10.         }
  11.     );
复制代码

  • 断路(Circuit Breaker)
    当故障频率超过阈值时,主动 “断开” 电路(拒绝后续请求),防止级联故障(如服务已崩溃,继续重试会加重负载)。
  1. // 定义断路策略:10 秒内 5 次失败则断路 30 秒
  2. var circuitBreaker = Policy
  3.         .Handle<HttpRequestException>()
  4.         .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
  5.         .CircuitBreakerAsync(
  6.                 5,
  7.                 TimeSpan.FromSeconds(30)
  8.         );
复制代码

  • 回退(Fallback)
    当操作失败时,提供一个 “备用方案”(如返回缓存数据、默认值),避免用户看到错误。
  1. // 定义回退策略:失败时返回预设的默认响应
  2. var fallbackPolicy = Policy
  3.     .Handle<HttpRequestException>()
  4.     .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
  5.     .FallbackAsync(
  6.         fallbackValue: new HttpResponseMessage(HttpStatusCode.OK) // 默认成功响应
  7.         { Content = new StringContent("备用数据(来自缓存或默认值)") },
  8.         onFallbackAsync: (result, context) =>
  9.         {
  10.             Console.WriteLine($"执行回退,原错误:{result.Exception?.Message}");
  11.             return Task.CompletedTask;
  12.         }
  13.     );
复制代码

  • 超时(Timeout)
    限制操作的执行时间,避免长时间等待(如数据库查询超时)。
  1. // 定义超时策略:操作超过 10 秒未完成则抛 TimeoutRejectedException
  2. var timeoutPolicy = Policy
  3.     .TimeoutAsync(
  4.         timeout: TimeSpan.FromSeconds(10),
  5.         onTimeoutAsync: (context, timeout, task) =>
  6.         {
  7.             Console.WriteLine($"操作超时,已等待 {timeout}");
  8.             return Task.CompletedTask;
  9.         }
  10.     );
复制代码

  • 组合
    Polly支持将多个策略组合,按照组合顺序执行,用以应对复杂场景。
    比如先重试,重试失败触发断路,断路期间执行回退。
  1. // 组合策略,先执行最外层。
  2. var wrappedPolicy = Policy.WrapAsync(
  3.         fallbackPolicy,//最外层:失败时回退
  4.         circuitBreakerkPolicy,//中层:断路保护
  5.         retryPolicy //内层:重试
  6.         );
复制代码
Resilience

Resilience 是微软官方推出的现代弹性库,它是对Polly的封装,其核心目标是降低弹性编程的门槛。
具体介绍请查阅MSDN,https://learn.microsoft.com/zh-cn/dotnet/core/resilience/http-resilience?tabs=dotnet-cli
它相对Polly的优势在于:
特性ResiliencePolly生态集成深度集成 ASP.NET Core(DI、IHttpClientFactory、配置)需手动集成(如通过 AddPolicyHandler)配置方式支持从 IConfiguration 动态加载,支持热更新需手动解析配置,无内置热更新可观测性内置日志、指标、诊断事件需通过回调手动实现API 设计类型安全的泛型 ResiliencePipeline基于 Policy 和 Policy动态调整支持运行时修改策略参数(如重试次数)需重建策略实例长期支持微软官方维护,长期演进方向社区维护(Polly 7+ 支持 .NET Standard)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 的方式难以统一添加中间件
眼见为实

3.png

图出处
为什么在.NET Framework中不推荐单例/静态的HttpClient?

既然不推荐New HttpClient(),那我将HttpClient设单例或者静态的行不行?
答案是也不行,因为HttpClient在首次解析域名后,会缓存DNS结果,默认缓存时间取决于操作系统和 DNS 服务器配置。
如果HttpClient实例被长期保留,当DNS记录更新时(比如服务器IP变更),会导致请求失败或者连接到旧服务器。
仅针对 .NET Framework,在.NET Core之后,可以设置PooledConnectionLifetime来解决DNS缓存问题,从而变相解决端口耗尽问题。
眼见为实

4.png

连接池缓存的地址,是以传入的URL作为Key。不是最终的IP地址,因此需要依赖DNS缓存来动态解析服务器IP地址。
源码
TIME_WAIT的优化几个思路

TCP的四次挥手是在内核态中进行处理的,我们难以在用户态层面进行大刀阔斧的优化。
我们可以通过以下几种方式来进行小幅度优化:

  • 开启端口复用(内核态)
    调整操作系统配置,允许系统重用处于 TIME_WAIT 状态的端口。
  • 缩短TIME_WAIT时间(内核态)
    TIME_WAIT 状态的默认持续时间是 60 秒,缩短此值可减少状态堆积。但会增加旧数据包干扰新连接的风险。
  • 开启HTTP2(用户态)
    HTTP/2 支持 单 TCP 连接上的多路复用,多个请求 / 响应可并行传输,显著减少需要创建 / 关闭的 TCP 连接数量。
  1. .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
  2. {
  3.     EnableHttp2 = true // 显式启用 HTTP/2(默认自动协商)
  4. });
复制代码

  • 禁用 Nagle 算法(用户态)
  1. .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
  2. {
  3.     ConnectCallback = async (context, cancellationToken) =>
  4.     {
  5.         var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
  6.         socket.NoDelay = true; // 禁用 Nagle 算法(减少延迟)
  7.         await socket.ConnectAsync(context.DnsEndPoint, cancellationToken);
  8.         return new NetworkStream(socket, ownsSocket: true);
  9.     }
  10. });
复制代码

  • 延长连接空闲时间(用户态)
  1. new SocketsHttpHandler
  2. {
  3.     PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), // 空闲 2 分钟后关闭(而非立即关闭)
  4.     PooledConnectionLifetime = TimeSpan.FromMinutes(10)    // 连接最长存活 10 分钟(避免频繁轮换)
  5. };
复制代码
需要注意一点,服务器通常作为被动关闭方,一般不会产生大量TIME_WAIT连接,但在微服务大行其道的今天,服务器与服务器之间的通信,服务器也会作为发起方,导致出现大量的TIME_WAIT。
IHttpClientFactory的优势?

为了解决HttpClient端口耗尽与DNS缓存问题。.NET提供了IHttpClientFactory。

  • 连接池复用与端口管理
    IHttpClientFactory 负责管理 HttpClient 实例的生命周期,共享底层 SocketsHttpHandler 和连接池,避免重复创建连接池和端口.
有一点需要注意,如果应用需要使用Cookie,要考虑禁用自动Cookie处理。因为IHttpClientFactory共享底层的SocketsHttpHandler,所以会导致CookieContainer也会被共享,从而导致网站A的Cookie被劫持到了网站B。

  • 统一配置与扩展
    通过AddHttpClient注册客户端,集中配置,统一管理
  1. serviceCollection
  2. .AddHttpClient<BaiduAPIService>(configure =>
  3. {
  4.         configure.BaseAddress = new Uri("https://www.baidu.com");
  5. })
  6. .AddHttpMessageHandler<CustomerHandler>();
复制代码

  • DNS动态更新
    通过 SetHandlerLifetime 配置 HttpMessageHandler 的生命周期(默认 2 分钟),到期后自动重建 Handler 并重新解析 DNS,避免缓存旧 IP。
  1. serviceCollection
  2.         .AddHttpClient()
  3.     .SetHandlerLifetime(TimeSpan.FromMinutes(5));
复制代码
IHttpClientFactory的生命周期?

IHttpClientFactory的生命周期独立于DI。
IHttpClientFactory在DI容器中以单例(Singleton)形式存在,通过IHttpClientFactory.CreateClient()获取的HttpClient实例是瞬态(Transient),每次调用CreateClient都会返回一个新的HttpClient实例,但这些实例共享HttpMessageHandler管道。
5.png

IHttpClientFactory的生命周期独立于DI?

DI 的生命周期(Transient/Scoped/Singleton)主要用于管理服务实例的创建与销毁,而IHttpClientFactory的核心目标是高效管理HTTP连接,这两者的理念存在严重冲突。

  • HTTP连接的复用,需要长生命周期的HttpMessageHandler
    TCP的连接的创建成本极高,如果遵循DI的Scoped 生命周期,将会导致TCP连接无法复用,增加延迟与TIME_WAIT 状态堆积
  • DNS动态更新需要独立于 DI 的生命周期控制
    DNS解析结果会随着时间变化(服务器扩容/缩容),若 HttpMessageHandler 生命周期与DI的Scoped绑定,若过长,会导致连接长期绑定旧IP,若过短,DNS解析过于频繁而限流。
  • HttpClient轻量级,频繁创建不会导致资源浪费
因此,IHttpClientFactory的生命周期独立于DI是一种优化的选择,而不是一种缺点
请勿重复注册HttpClientService
  1.             var serviceCollection = new ServiceCollection();
  2.             serviceCollection
  3.                 .AddHttpClient<BaiduAPIService>(configure =>
  4.                 {
  5.                     configure.BaseAddress = new Uri("https://www.baidu.com");
  6.                 });
  7.             serviceCollection.AddSingleton<BaiduAPIService>();//重复注册BaiduAPIService,因为没有指定HttpClient的name,所以系统会提供default HttpClient,导致与上面的代码失效。可以使用TryAddSingleton or 删除此重复注册的代码。
复制代码
高并发情况下,首次启动很慢?

HttpClient底层也用Task获取HttpConnection,当流量瞬增时,线程池创建新线程需要时间。等线程池预热后,便会恢复正常。
我需要访问API Endpoint,配置HttpClient太繁琐了,有没有更加简便的?

Refit是.NET生态中一款轻量级,声明式的HTTP客户端库,旨在通过定义接口的方式快速生成API Endpoint,大幅度减少手动编写的样板代码。
https://github.com/reactiveui/refit
  1. // Program.cs 中注册
  2. serviceCollection.AddRefitClient<BaiduAPIService>()
  3.         .ConfigureHttpClient(client =>
  4.         {
  5.                 client.BaseAddress = new Uri("https://api.github.com");
  6.                 client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
  7.         })
  8.         .AddPolicyHandler(wrappedPolicy); // 添加弹性策略
  9. public interface IGitHubApi
  10. {
  11.     // GET https://api.github.com/users/{username}
  12.     [Get("/users/{username}")]
  13.     Task<User> GetUserAsync(string username);
  14.     // POST https://api.github.com/repos/{owner}/{repo}/issues
  15.     [Post("/repos/{owner}/{repo}/issues")]
  16.     Task<Issue> CreateIssueAsync(
  17.         string owner,
  18.         string repo,
  19.         [Body] CreateIssueRequest request,
  20.         CancellationToken cancellationToken = default
  21.     );
  22. }
复制代码
分享一个曾经踩过的坑,.NET 6之前默认序列化/反序列化类库是Newtonsoft.Json,.NET 6之后切换成了System.Text.Json。Refit使用的是.NET默认配置,导致.NET 版本升级后,大量接口报错,惨遭挨锤!
需要引用Refit.Newtonsoft.Json包,并修改配置ContentSerializer= new NewtonsoftJsonContentSerializer()。
再分享一个国内的声明式API框架,号称性能是Refit的两倍。https://github.com/dotnetcore/WebApiClient
最佳实践

方法一,个人推荐。
  1.     internal class Program
  2.     {
  3.         static async Task Main(string[] args)
  4.         {
  5.             var serviceCollection = new ServiceCollection();
  6.                         //强类型,简单省事。
  7.             serviceCollection
  8.                 .AddHttpClient<BaiduAPIService>(configure =>
  9.                 {
  10.                     configure.BaseAddress = new Uri("https://www.baidu.com");
  11.                 });  
  12.             var services = serviceCollection.BuildServiceProvider();
  13.             var httpClient= services.GetRequiredService<BaiduAPIService>();
  14.             var result= await httpClient.GetStringAsync();
  15.             Console.WriteLine(result);
  16.         }
  17.     }
  18.         internal class BaiduAPIService
  19.     {
  20.         private readonly HttpClient _httpClient;
  21.         public BaiduAPIService(HttpClient httpClient)
  22.         {
  23.             _httpClient = httpClient;
  24.         }
  25.         public async Task<string?> GetStringAsync(string? url=null)
  26.         {
  27.             var response= await _httpClient.GetAsync(url);
  28.             if (!response.IsSuccessStatusCode)
  29.                 return null;
  30.             var result= await response.Content.ReadAsStringAsync();
  31.             
  32.             return result;
  33.         }
  34.     }
复制代码
方法二
  1.     internal class Program
  2.     {
  3.         static async Task Main(string[] args)
  4.         {
  5.             var serviceCollection = new ServiceCollection();
  6.                         //自定义Name
  7.             serviceCollection
  8.                 .AddHttpClient(nameof(BaiduAPIService), configure =>
  9.                 {
  10.                     configure.BaseAddress = new Uri("https://www.baidu.com");
  11.                 });
  12.             serviceCollection.AddScoped<BaiduAPIService>();//需要主动注册DI
  13.             var services = serviceCollection.BuildServiceProvider();
  14.             var httpClient= services.GetRequiredService<BaiduAPIService>();
  15.             var result= await httpClient.GetStringAsync();
  16.             Console.WriteLine(result);
  17.         }
  18.     }
  19.         internal class BaiduAPIService
  20.     {
  21.         private readonly HttpClient _httpClient;
  22.         public BaiduAPIService(IHttpClientFactory httpClientFactory)
  23.         {
  24.             //因为DI不知道你要用哪个HttpClient,所以需要httpClientFactory来寻找name
  25.             _httpClient = httpClientFactory.CreateClient(nameof(BaiduAPIService));
  26.         }
  27.         public async Task<string?> GetStringAsync(string? url=null)
  28.         {
  29.             var response= await _httpClient.GetAsync(url);
  30.             if (!response.IsSuccessStatusCode)
  31.                 return null;
  32.             var result= await response.Content.ReadAsStringAsync();
  33.             
  34.             return result;
  35.         }
  36.     }
复制代码
方法三,.NET 9后新增
  1.     internal class Program
  2.     {
  3.         static async Task Main(string[] args)
  4.         {
  5.             var serviceCollection = new ServiceCollection();
  6.             serviceCollection
  7.                 .AddHttpClient(nameof(BaiduAPIService), configure =>
  8.                 {
  9.                     configure.BaseAddress = new Uri("https://www.baidu.com");
  10.                 })
  11.                 .AddAsKeyed();//keyed DI 支持
  12.             serviceCollection.AddScoped<BaiduAPIService>();
  13.             var services = serviceCollection.BuildServiceProvider();
  14.             var httpClient= services.GetRequiredService<BaiduAPIService>();
  15.             var result= await httpClient.GetStringAsync();
  16.             Console.WriteLine(result);
  17.         }
  18.     }
  19.         internal class BaiduAPIService
  20.     {
  21.         private readonly HttpClient _httpClient;
  22.         //从DI中选择你需要的HttpClient
  23.         public BaiduAPIService([FromKeyedServices(nameof(BaiduAPIService))]HttpClient httpClient)
  24.         {
  25.             _httpClient = httpClient;
  26.         }
  27.         public async Task<string?> GetStringAsync(string? url=null)
  28.         {
  29.             var response= await _httpClient.GetAsync(url);
  30.             if (!response.IsSuccessStatusCode)
  31.                 return null;
  32.             var result= await response.Content.ReadAsStringAsync();
  33.             
  34.             return result;
  35.         }
  36.     }
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册