在现代应用开发中,HTTP 通信是连接不同服务、获取远程数据、调用 RESTful API 的核心手段。C# 提供了强大的 HttpClient 类来满足这些需求。本教程将系统讲解 HttpClient 的使用方法、核心机制、常见陷阱与最佳实践,帮助你在项目中游刃有余地处理 HTTP 请求。
一、初识 HttpClient
HttpClient 位于 System.Net.Http 命名空间下,是 .NET Framework 4.5 及 .NET Core/.NET 5+ 中用于发送 HTTP 请求和接收 HTTP 响应的现代化 API。它取代了老旧的 WebClient 和 HttpWebRequest,提供了更简洁、更灵活、更强大的异步编程模型。
与旧方案相比,HttpClient 具备以下核心优势:
- 原生异步支持:所有 I/O 方法均为异步,避免阻塞线程,提升并发能力。
- 连接池复用:底层自动管理 TCP 连接,同一目标主机可复用连接,减少握手开销。
- 可扩展管道 :通过
DelegatingHandler可以轻松插入日志、重试、认证等中间件逻辑。 - 丰富的配置项:支持超时、自定义请求头、多种认证方式等。
二、基础用法
2.1 发送 GET 请求
GET 请求用于从服务器获取数据。最简单的用法是调用 GetStringAsync 直接获取响应字符串。
cs
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using var client = new HttpClient();
string content = await client.GetStringAsync("https://api.github.com/repos/dotnet/runtime");
Console.WriteLine(content);
}
}
如果需要更精细的控制,比如检查状态码或读取响应头,可以使用 GetAsync 方法获取 HttpResponseMessage 对象。
cs
HttpResponseMessage response = await client.GetAsync("https://api.example.com/data");
response.EnsureSuccessStatusCode(); // 非 200-299 状态码时抛出异常
string json = await response.Content.ReadAsStringAsync();
2.2 发送 POST 请求
POST 请求用于向服务器提交数据,通常需要指定请求体内容和 Content-Type。
cs
using System.Text;
using System.Text.Json;
var payload = new { Name = "Alice", Age = 25 };
string json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.PostAsync("https://httpbin.org/post", content);
string result = await response.Content.ReadAsStringAsync();
2.3 其他 HTTP 方法
HttpClient 同样支持 PUT、DELETE 等标准方法,使用方式与 POST 类似。
cs
// PUT 请求
await client.PutAsync("https://api.example.com/items/1", content);
// DELETE 请求
await client.DeleteAsync("https://api.example.com/items/1");
三、高级配置
3.1 自定义请求头
可以通过 DefaultRequestHeaders 为所有请求设置默认头,也可以在单个 HttpRequestMessage 中设置。
cs
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
client.DefaultRequestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
3.2 设置超时时间
Timeout 属性控制从发送请求到接收完整响应的时间上限,超时将取消请求并抛出 TaskCanceledException。
cs
client.Timeout = TimeSpan.FromSeconds(30);
3.3 使用 SendAsync 自定义请求
SendAsync 是底层方法,允许你构建完整的 HttpRequestMessage 对象,实现最大程度的定制。
cs
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/data");
request.Headers.Add("Authorization", "Bearer your_token_here");
HttpResponseMessage response = await client.SendAsync(request);
四、常见陷阱与核心原则
4.1 不要每次请求都 new HttpClient
许多初学者习惯在 using 块中创建 HttpClient 实例,认为这样可以自动释放资源。然而,这种做法在高并发场景下会引发严重问题。
每次创建新实例都会分配新的 socket 连接,即使调用 Dispose,底层 socket 也不会立即释放,而是进入 TIME_WAIT 状态,持续数十秒甚至数分钟。短时间内大量请求会迅速耗尽可用端口,导致 SocketException(通常提示"通常每个套接字地址只允许使用一次")。
**错误示范:**
cs
for (int i = 0; i < 1000; i++)
{
using var client = new HttpClient();
await client.GetAsync("https://api.example.com");
}
4.2 不要将 HttpClient 简单当作静态单例
虽然复用 HttpClient 实例可以解决端口耗尽问题,但直接将其定义为静态单例也存在隐患:
- DNS 变更不生效 :
HttpClient仅在创建连接时解析 DNS,之后不会跟踪 TTL。在容器化或动态伸缩环境中,IP 变更可能导致请求失败。 - 全局配置耦合 :修改
Timeout或DefaultRequestHeaders会影响所有调用,缺乏隔离性。
4.3 正确做法:使用 IHttpClientFactory
从 .NET Core 2.1 开始,微软引入了 IHttpClientFactory,它通过管理底层 HttpMessageHandler 的生命周期,完美解决了上述问题。
**核心机制:**
- 工厂内部维护一个
HttpMessageHandler池,按命名分组复用。 - 定期轮换过期的 handler,确保 DNS 更新生效。
- 返回的
HttpClient实例是轻量对象,可安全释放,不占用 socket 资源。
**注册与使用:**
cs
// 在 Startup.cs 或 Program.cs 中注册
services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp");
client.Timeout = TimeSpan.FromSeconds(30);
});
// 在业务类中注入使用
public class GitHubService
{
private readonly IHttpClientFactory _clientFactory;
public GitHubService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<string> GetRepos()
{
var client = _clientFactory.CreateClient("GitHub");
return await client.GetStringAsync("/repos/dotnet/runtime");
}
}
4.4 类型化客户端
对于封装特定 API 调用的场景,推荐使用类型化客户端,将 HTTP 调用逻辑与业务类绑定,更利于单元测试和依赖注入。
cs
// 定义服务类
public class WeatherService
{
private readonly HttpClient _client;
public WeatherService(HttpClient client)
{
_client = client;
}
public async Task<string> GetForecastAsync(string city)
{
return await _client.GetStringAsync($"/weather?city={city}");
}
}
// 注册
services.AddHttpClient<WeatherService>(client =>
{
client.BaseAddress = new Uri("https://api.weather.com/");
});
五、弹性策略与 Polly 集成
在生产环境中,网络波动、服务暂时不可用等情况时有发生。结合 Polly 库可以轻松实现重试、断路器、超时等弹性策略。
cs
services.AddHttpClient("ResilientClient")
.AddTransientHttpErrorPolicy(policy =>
policy.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));
上述配置会在遇到 HttpRequestException 或 5xx 状态码时,自动重试 3 次,每次间隔时间按指数退避增长。
六、进阶技巧
6.1 处理响应流
下载大文件时,不应将全部内容加载到内存,而应使用流式处理。
cs
using var response = await client.GetAsync("https://example.com/largefile.zip");
using var fileStream = File.Create("largefile.zip");
await response.Content.CopyToAsync(fileStream);
6.2 配置底层 SocketsHttpHandler
在 .NET Core 2.1+ 中,可以通过 SocketsHttpHandler 精细控制连接池行为。
cs
var handler = new SocketsHttpHandler
{
MaxConnectionsPerServer = 50, // 每台服务器最大并发连接数
PooledConnectionLifetime = TimeSpan.FromMinutes(2), // 连接最大存活时间,到期后重建以刷新 DNS
ConnectTimeout = TimeSpan.FromSeconds(10) // 建立 TCP 连接的超时
};
var client = new HttpClient(handler);
6.3 取消请求
使用 CancellationToken 可以优雅地取消长时间运行的请求,避免资源浪费。
cs
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
var response = await client.GetAsync("https://slow-api.com", cts.Token);
}
catch (TaskCanceledException)
{
Console.WriteLine("请求被取消或超时");
}
七、总结
| 场景 | 推荐方案 |
|---|---|
| 简单脚本或低频调用 | 使用 using 创建 HttpClient 实例 |
| Web 应用、后台服务 | 使用 IHttpClientFactory 或类型化客户端 |
| 需要重试、熔断 | 结合 IHttpClientFactory + Polly |
| 需要精细控制连接池 | 配置 SocketsHttpHandler 并传入构造函数 |
| 下载大文件 | 使用 CopyToAsync 流式写入 |
掌握 HttpClient 的正确使用方式,是构建健壮、高性能 .NET 应用的关键一步。牢记"复用 handler 而非 HttpClient 实例"这一核心原则,并善用 IHttpClientFactory 管理生命周期,你就能避开绝大多数网络编程中的常见陷阱,写出稳定可靠的代码。