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

简介

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 包含响应的状态码、头部、内容等信息,关键属性如下:

属性/方法说明
StatusCodeHTTP 状态码(如 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");

结构解析

image

  1. HttpClient/HttpMessageInvoker
    用户入口,提供GET/POST/PUT/DELETE等友好API,协调请求发送于响应接收。
    HttpClient本身是轻量级对象,仅包含了配置信息和对HttpMessageHandler的引用。

  2. HttpMessageHandler
    HttpMessageHandler是 核心抽象类,定义了处理 HTTP 请求的基本行为。它通过抽象方法 SendAsync 接收 HttpRequestMessage,并返回 HttpResponseMessage。

  3. HttpClientHandler
    HttpClientHandler是HttpMessageHandler 的具体子类,
    是早期 .NET中默认的 HTTP 处理程序。在.NET Core 2.1后,它演变为兼容层,底层通过 SocketsHttpHandler 实现网络通信,保持 API 兼容性。

  4. SocketsHttpHandler
    这才是持有网络资源(TCP 连接池、TLS 会话)的核心组件,.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:表单内容(支持文件上传)

请求处理流程

image

.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 //内层:重试
	);

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<T>基于 PolicyPolicy<TResult>
动态调整支持运行时修改策略参数(如重试次数)需重建策略实例
长期支持微软官方维护,长期演进方向社区维护(Polly 7+ 支持 .NET Standard)

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 的方式难以统一添加中间件

眼见为实

image

图出处

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

既然不推荐New HttpClient(),那我将HttpClient设单例或者静态的行不行?
答案是也不行,因为HttpClient在首次解析域名后,会缓存DNS结果,默认缓存时间取决于操作系统和 DNS 服务器配置。
如果HttpClient实例被长期保留,当DNS记录更新时(比如服务器IP变更),会导致请求失败或者连接到旧服务器

仅针对 .NET Framework,在.NET Core之后,可以设置PooledConnectionLifetime来解决DNS缓存问题,从而变相解决端口耗尽问题。

眼见为实

image
连接池缓存的地址,是以传入的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 和连接池,避免重复创建连接池和端口.

有一点需要注意,如果应用需要使用Cookie,要考虑禁用自动Cookie处理。因为IHttpClientFactory共享底层的SocketsHttpHandler,所以会导致CookieContainer也会被共享,从而导致网站A的Cookie被劫持到了网站B。

  1. 统一配置与扩展
    通过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));

IHttpClientFactory的生命周期?

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

image

IHttpClientFactory的生命周期独立于DI?

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

  1. HTTP连接的复用,需要长生命周期的HttpMessageHandler
    TCP的连接的创建成本极高,如果遵循DI的Scoped 生命周期,将会导致TCP连接无法复用,增加延迟与TIME_WAIT 状态堆积
  2. DNS动态更新需要独立于 DI 的生命周期控制
    DNS解析结果会随着时间变化(服务器扩容/缩容),若 HttpMessageHandler 生命周期与DI的Scoped绑定,若过长,会导致连接长期绑定旧IP,若过短,DNS解析过于频繁而限流。
  3. HttpClient轻量级,频繁创建不会导致资源浪费

因此,IHttpClientFactory的生命周期独立于DI是一种优化的选择,而不是一种缺点

请勿重复注册HttpClientService

            var serviceCollection = new ServiceCollection();
            serviceCollection
                .AddHttpClient<BaiduAPIService>(configure =>
                {
                    configure.BaseAddress = new Uri("https://www.baidu.com");

                });

            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

// Program.cs 中注册
serviceCollection.AddRefitClient<BaiduAPIService>()
	.ConfigureHttpClient(client =>
	{
		client.BaseAddress = new Uri("https://api.github.com");
		client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
	})
	.AddPolicyHandler(wrappedPolicy); // 添加弹性策略
public interface IGitHubApi
{
    // GET https://api.github.com/users/{username}
    [Get("/users/{username}")]
    Task<User> GetUserAsync(string username);

    // POST https://api.github.com/repos/{owner}/{repo}/issues
    [Post("/repos/{owner}/{repo}/issues")]
    Task<Issue> CreateIssueAsync(
        string owner, 
        string repo, 
        [Body] CreateIssueRequest request, 
        CancellationToken cancellationToken = default
    );
}

分享一个曾经踩过的坑,.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

最佳实践

方法一,个人推荐。

    internal class Program
    {
        static async Task Main(string[] args)
        {
            var serviceCollection = new ServiceCollection();
			//强类型,简单省事。
            serviceCollection
                .AddHttpClient<BaiduAPIService>(configure =>
                {
                    configure.BaseAddress = new Uri("https://www.baidu.com");
                });  

            var services = serviceCollection.BuildServiceProvider();

            var httpClient= services.GetRequiredService<BaiduAPIService>();
            var result= await httpClient.GetStringAsync();
            Console.WriteLine(result);
        }
    }
	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;
        }
    }

方法二

    internal class Program
    {
        static async Task Main(string[] args)
        {
            var serviceCollection = new ServiceCollection();
			//自定义Name
            serviceCollection
                .AddHttpClient(nameof(BaiduAPIService), configure =>
                {
                    configure.BaseAddress = new Uri("https://www.baidu.com");
                });

            serviceCollection.AddScoped<BaiduAPIService>();//需要主动注册DI
            var services = serviceCollection.BuildServiceProvider();

            var httpClient= services.GetRequiredService<BaiduAPIService>();
            var result= await httpClient.GetStringAsync();
            Console.WriteLine(result);
        }
    }
	internal class BaiduAPIService
    {
        private readonly HttpClient _httpClient;

        public BaiduAPIService(IHttpClientFactory httpClientFactory)
        {
            //因为DI不知道你要用哪个HttpClient,所以需要httpClientFactory来寻找name
            _httpClient = httpClientFactory.CreateClient(nameof(BaiduAPIService));
        }

        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;
        }
    }

方法三,.NET 9后新增

    internal class Program
    {
        static async Task Main(string[] args)
        {
            var serviceCollection = new ServiceCollection();

            serviceCollection
                .AddHttpClient(nameof(BaiduAPIService), configure =>
                {
                    configure.BaseAddress = new Uri("https://www.baidu.com");
                })
                .AddAsKeyed();//keyed DI 支持

            serviceCollection.AddScoped<BaiduAPIService>();
            var services = serviceCollection.BuildServiceProvider();

            var httpClient= services.GetRequiredService<BaiduAPIService>();
            var result= await httpClient.GetStringAsync();
            Console.WriteLine(result);
        }
    }
	internal class BaiduAPIService
    {
        private readonly HttpClient _httpClient;

        //从DI中选择你需要的HttpClient
        public BaiduAPIService([FromKeyedServices(nameof(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;
        }
    }
原创作者: lmy5215006 转载于: https://www.cnblogs.com/lmy5215006/p/18849726
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值