简介

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请求库

发送请求

//GET
HttpResponseMessage response = await _httpClient.GetAsync("https://www.baidu.com");
response.EnsureSuccessStatusCode(); // 检查状态码是否为 200-299,否则抛异常
string json = await response.Content.ReadAsStringAsync(); // 读取响应内容
Console.WriteLine($"响应内容:{json}"); //POST
var 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");

结构解析

  1. HttpClient

    用户入口,封装SendAsync()方法,提供GET/POST/PUT/DELETE等友好API,协调请求发送于响应接收。

  2. HttpMessageInvoker

    HttpClient基类,持有SendAsync()的具体类。并实现IDisposable,实现资源自动清理。

  3. HttpMessageHandler/HttpClientHandler

    真正发起请求的底层抽象与实现,定义了处理HTTP请求的核心逻辑,由其派生类实现跨平台

  4. SocketsHttpHandler

    .NET Core 2.1之后默认的HTTP处理器,直接基于System.Net.Sockets,性能高且跨平台。

  5. DelegatingHandler

    DelegatingHandler是一个抽象类,以责任链模式拓展请求处理逻辑,可以通过继承DelegatingHandler,以中间件的方式实现自定义逻辑(日志,重试,退让,缓存等)

  6. HttpRequestMessage/HttpResponseMessage

    HttpRequestMessage:表示 HTTP 请求,包含 Method(如 HttpMethod.Get)、RequestUri、Headers、Content(请求体)等属性。

    HttpResponseMessage:表示 HTTP 响应,包含 StatusCode(状态码)、Headers、Content(响应体)、ReasonPhrase(状态描述)等属性。

  7. HttpContent

    请求体与响应体的基类,定义了内容的通用操作,再根据不同的数据,派生出不同的子类。

    StringContent:文本内容(如 JSON、HTML)。

    ByteArrayContent:二进制字节数组内容(如图像、文件)。

    StreamContent:流式内容(如大文件上传)。

    MultipartFormDataContent:表单内容(支持文件上传)

请求处理流程

.NET 9中优化

https://devblogs.microsoft.com/dotnet/dotnet-9-networking-improvements/#community-contributions

弹性处理(Polly)

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

Polly 提供了 6 大核心策略,覆盖常见的弹性需求:

  1. 重试(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()}");
}
);
  1. 断路(Circuit Breaker)

    当故障频率超过阈值时,主动 “断开” 电路(拒绝后续请求),防止级联故障(如服务已崩溃,继续重试会加重负载)。
// 定义断路策略:10 秒内 5 次失败则断路 30 秒
var circuitBreaker = Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.CircuitBreakerAsync(
5,
TimeSpan.FromSeconds(30)
);
  1. 回退(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;
}
);
  1. 超时(Timeout)

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

    Polly支持将多个策略组合,按照组合顺序执行,用以应对复杂场景。

    比如先重试,重试失败触发断路,断路期间执行回退。
// 组合策略,先执行最外层。
var wrappedPolicy = Policy.WrapAsync(
fallbackPolicy,//最外层:失败时回退
circuitBreakerkPolicy,//中层:断路保护
retryPolicy //内层:重试
);

FAQ

为什么不推荐New HttpClient()的方式?

上面讲到,HttpClient是线程安全的,那为什么不推荐使用New HttpClient()呢?

主要是因为每一个HttpClient都会有一个独立的连接池造成的。

  1. 端口耗尽

    每个 HttpClient 实例默认使用独立的 SocketsHttpHandler,在其内部按照Authority(https://xxxx.com:443)进行分组管理TCP连接。如果是同一个Authority,则会复用连接。

    但如果是通过New HttpClient()的方式创建,即时是同一个URL,也会为分配新的端口号,无法实现复用,导致端口耗尽

  2. TIME_WAIT 状态堆积

    当TCP连接关闭时,发起端会进入TIME_WAIT状态,并等待2MSL(约60s)。以确保接收端收到最终的ACK包。如若HttpClient被频繁创建和销毁,其底层的TCP连接会大量处于TIME_WAIT 状态,进一步加剧端口耗尽。

  3. 无法统一配置与拓展

    直接new HttpClient 难以统一管理公共配置,如代理、超时、证书验证等,每个实例都要配置一遍,代码非常冗余

    且无法便捷集成扩展功能(如重试策略、断路机制、日志记录),这些需要通过 DelegatingHandler 实现,但 new 的方式难以统一添加中间件

眼见为实

图出处

为什么不推荐单例/静态的HttpClient?

既然不推荐New HttpClient(),那我将HttpClient设单例或者静态的行不行?

答案是也不行,因为HttpClient在首次解析域名后,会缓存DNS结果,默认缓存时间取决于操作系统和 DNS 服务器配置。

如果HttpClient实例被长期保留,当DNS记录更新时(比如服务器IP变更),会导致请求失败或者连接到旧服务器

眼见为实



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

源码

TIME_WAIT的优化几个思路

TCP的四次挥手是在内核态中进行处理的,我们难以在用户态层面进行大刀阔斧的优化

我们可以通过以下几种方式来进行小幅度优化:

  1. 开启端口复用(内核态)

    调整操作系统配置,允许系统重用处于 TIME_WAIT 状态的端口。
  2. 缩短TIME_WAIT时间(内核态)

    TIME_WAIT 状态的默认持续时间是 60 秒,缩短此值可减少状态堆积。但会增加旧数据包干扰新连接的风险。
  3. 开启HTTP2(用户态)

    HTTP/2 支持 单 TCP 连接上的多路复用,多个请求 / 响应可并行传输,显著减少需要创建 / 关闭的 TCP 连接数量。
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
EnableHttp2 = true // 显式启用 HTTP/2(默认自动协商)
});
  1. 禁用 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);
}
});
  1. 延长连接空闲时间(用户态)
new SocketsHttpHandler
{
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), // 空闲 2 分钟后关闭(而非立即关闭)
PooledConnectionLifetime = TimeSpan.FromMinutes(10) // 连接最长存活 10 分钟(避免频繁轮换)
};

需要注意一点,服务器通常作为被动关闭方,一般不会产生大量TIME_WAIT连接,但在微服务大行其道的今天,服务器与服务器之间的通信,服务器也会作为发起方,导致出现大量的TIME_WAIT。

IHttpClientFactory的优势?

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

  1. 连接池复用与端口管理

    IHttpClientFactory 负责管理 HttpClient 实例的生命周期共享底层 SocketsHttpHandler 和连接池,避免重复创建连接池和端口.

  2. 统一配置与扩展

    通过AddHttpClient注册客户端,集中配置,统一管理

serviceCollection
.AddHttpClient<BaiduAPIService>(configure =>
{
configure.BaseAddress = new Uri("https://www.baidu.com");
})
.AddHttpMessageHandler<CustomerHandler>();
  1. 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;
}
}

C#网络编程(四)----HttpClient的更多相关文章

  1. Java高并发网络编程(四)Netty

    在网络应用开发的过程中,直接使用JDK提供的NIO的API,比较繁琐,而且想要进行性能提升,还需要结合多线程技术. 由于网络编程本身的复杂性,以及JDK API开发的使用难度较高,所以在开源社区中,涌 ...

  2. 网络编程采用HttpClient类更好

    一般人网络编程普遍用HttpWebRequest,类似下面的实现.我也一般都这样实现 string result = string.Empty; HttpWebRequest request = (H ...

  3. Linux 网络编程四(socket多线程升级版)

    //网络编程--客户端 #include <stdio.h> #include <stdlib.h> #include <string.h> #include &l ...

  4. Linux网络编程(四)

    在linux网络编程[1-3]中,我们编写的网络程序仅仅是为了了解网络编程的基本步骤,实际应用当中的网络程序并不会用那样的.首先,如果服务器需要处理高并发访问,通常不会使用linux网络编程(三)中那 ...

  5. Android 网络编程之---HttpClient 与 HttpURLConnection 共用cookie

    HttpClient 与 HttpURLConnection 共用 SessionId HttpClient 与 HttpUrlConnection 是Android 中HTTP操作最常见的訪问方式. ...

  6. [Python网络编程]gevent httpclient以及网页编码

    之前看到geventhttpclient这个项目,https://github.com/gwik/geventhttpclient,官方文档说非常快,因为响应使用了C的解析,所以我一直想把这玩意用到项 ...

  7. Linux网络编程四、UDP,广播和组播

    一.UDP UDP:是一个支持无连接的传输协议,全称是用户数据包协议(User Datagram Protocol).UDP协议无需像TCP一样要建立连接后才能发送封装的IP数据报,也是因此UDP相较 ...

  8. Python for Infomatics 第12章 网络编程四(译)

    注:文章原文为Dr. Charles Severance 的 <Python for Informatics>.文中代码用3.4版改写,并在本机测试通过. 12.7 用BeautifulS ...

  9. 脑残式网络编程入门(四):快速理解HTTP/2的服务器推送(Server Push)

    本文原作者阮一峰,作者博客:ruanyifeng.com. 1.前言 新一代HTTP/2 协议的主要目的是为了提高网页性能(有关HTTP/2的介绍,请见<从HTTP/0.9到HTTP/2:一文读 ...

  10. 脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手

    .引言 网络编程中TCP协议的三次握手和四次挥手的问题,在面试中是最为常见的知识点之一.很多读者都知道“三次”和“四次”,但是如果问深入一点,他们往往都无法作出准确回答. 本篇文章尝试使用动画图片的方 ...

随机推荐

  1. 开源一款串口舵机驱动扩展板-FreakStudio多米诺系列

    原文链接: FreakStudio的博客 摘要 总线舵机扩展板通过UART接口控制多个舵机,支持堆叠级联,最多连接4个扩展板.具备小尺寸设计.供电保护.全双工转半双工通信.稳定供电等特点,适用于多舵机 ...

  2. TensorFlow 的基本概念和使用场景

    TensorFlow是一个开源的机器学习框架,由Google开发并维护.它提供了一个灵活的编程环境,用于构建和训练各种机器学习模型.TensorFlow是基于图计算的模型,其中节点表示数学操作,而边表 ...

  3. Java字节码增强实际应用在哪些方面?

    Java字节码增强由于与业务应用耦合性较低,且可任意修改程序代码,所以在许多方面都有应用.也是许多公司产品实现的基础.下面大概分类一下: 1.在可观测和监控方面的应用 如果一个应用的架构服务之间的依赖 ...

  4. 寻找旋转排序数组中的最小值 II

    地址:https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/ <?php /** 154. 寻找旋转排 ...

  5. 三分钟构建高性能WebSocket服务 | 超优雅的Springboot整合Netty方案

    前言 每当使用SpringBoot进行Weboscket开发时,最容易想到的就是spring-boot-starter-websocket(或spring-websocket).它可以让我们使用注解, ...

  6. AXUI前端框架v3版本已经发布,底层完全改写,基于原生技术标准,想走得更远!

    AXUI的v3版本已经发布! AXUI框架已经经历了第一代和第二代的迭代,充分认识到纯CSS和HTML的局限性,也意识到过多手动编写代码会影响用户体验.因此,AXUI的目标是:既满足原生前端的标准,又 ...

  7. 池化层 Pooling Layer

    写在前面:人生就是努力.搞不懂.躺平,循环. 文章结构 池化层的相对位置 在多通道任务中,池化层和卷积层的不同 重要的参数stride 与 kernel_size 大小的相对关系决定3种池化层 参数 ...

  8. idea 导入普通的项目后,无法发布

    之前一直都是在eclipse开发,现在改idea,但是很多隐藏的功能,都不晓得在哪里找到. 问题: 新导入一个spring 项目(没有maven),在界面上看是没有问题,但是使用tomcat部署项目的 ...

  9. 原生JS实现虚拟列表(不使用Vue,React等前端框架)

    好家伙,   1. 什么是虚拟列表 虚拟列表(Virtual List)是一种优化长列表渲染性能的技术.当我们需要展示成千上万条数据时,如果一次性将所有数据渲染到DOM中,会导致页面卡顿甚至崩溃.虚拟 ...

  10. 队列的实现方式(先进先出 FIFO)--环形队列

    博客地址:https://www.cnblogs.com/zylyehuo/ # -*- coding: utf-8 -*- class Queue: def __init__(self, size= ...