C# HttpClient 使用全指南

在现代应用开发中,HTTP 通信是连接不同服务、获取远程数据、调用 RESTful API 的核心手段。C# 提供了强大的 HttpClient 类来满足这些需求。本教程将系统讲解 HttpClient 的使用方法、核心机制、常见陷阱与最佳实践,帮助你在项目中游刃有余地处理 HTTP 请求。


一、初识 HttpClient

HttpClient 位于 System.Net.Http 命名空间下,是 .NET Framework 4.5 及 .NET Core/.NET 5+ 中用于发送 HTTP 请求和接收 HTTP 响应的现代化 API。它取代了老旧的 WebClientHttpWebRequest,提供了更简洁、更灵活、更强大的异步编程模型。

与旧方案相比,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 变更可能导致请求失败。
  • 全局配置耦合 ‌:修改 TimeoutDefaultRequestHeaders 会影响所有调用,缺乏隔离性。

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 管理生命周期,你就能避开绝大多数网络编程中的常见陷阱,写出稳定可靠的代码。