在微服务架构中,或与外部API通信时,HTTP客户端是必不可少的组件。然而,许多开发人员在实现HTTP客户端时未能充分考虑性能和可用性。
本文将介绍使用C#中的HttpClient类的最佳实践,并探讨HTTP通信的一些重要方面。
1. 不要在每次请求时创建和销毁HttpClient
初学者最常犯的错误是在每次HTTP请求时创建和销毁HttpClient实例。
public async Task<string> GetStringFromApi()
{
    using (var client = new HttpClient())
    {
        return await client.GetStringAsync("https://api.example.com/data");
    }
}
为什么这是错误的?
每次创建HttpClient的新实例时,都会分配新的socket连接。当客户端进入using语句块的末尾时,socket连接并不会立即释放,而是会进入TIME_WAIT状态,这可能会持续数十秒。
在高负载下,这会导致socket耗尽(SocketException: Address already in use),因为操作系统需要几分钟来回收套接字。
2. 不要将HttpClient保留为单例
另一个常见做法是将HttpClient对象创建为单例。
public class ApiClient
{
    private static readonly HttpClient _client = new HttpClient();
    public async Task<string> GetStringFromApi()
    {
        return await _client.GetStringAsync("https://api.example.com/data");
    }
}
为什么这不是最佳选择?
HttpClient是为长期使用而设计的,但将其作为单例使用有其问题:
- • 在某些情况下,之前设置的默认请求头可能会带到后续请求
3. 使用HttpClientFactory(.NET Core 2.1及以上)
.NET Core 2.1引入了HttpClientFactory,解决了上述问题。
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("github", c =>
    {
        c.BaseAddress = new Uri("https://api.github.com/");
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
    });
}
// GithubService.cs
publicclassGithubService
{
    privatereadonly IHttpClientFactory _clientFactory;
    public GithubService(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }
    public async Task<string> GetAspNetDocsIssues()
    {
        var client = _clientFactory.CreateClient("github");
        var response = await client.GetAsync("/repos/aspnet/AspNetCore.Docs/issues");
        response.EnsureSuccessStatusCode();
        returnawait response.Content.ReadAsStringAsync();
    }
}
HttpClientFactory提供以下优势:
- • 管理底层HttpClientMessageHandler的生命周期
- • 应用轮询策略防止连接拥塞(轮流使用连接,而不是一次全部使用)
- • 内置对Polly集成的支持,便于添加断路器、超时、重试等弹性政策
4. 使用强类型客户端
可以通过使用AddHttpClient<TClient>()方法注册强类型客户端,进一步改进HttpClientFactory方法。
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient<IGithubClient, GithubClient>(c =>
    {
        c.BaseAddress = new Uri("https://api.github.com/");
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
    });
}
// GithubClient.cs
publicinterfaceIGithubClient
{
    Task<IEnumerable<GithubIssue>> GetAspNetDocsIssues();
}
publicclassGithubClient : IGithubClient
{
    privatereadonly HttpClient _httpClient;
    public GithubClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    publicasync Task<IEnumerable<GithubIssue>> GetAspNetDocsIssues()
    {
        var response = await _httpClient.GetAsync("/repos/aspnet/AspNetCore.Docs/issues");
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<IEnumerable<GithubIssue>>(content);
    }
}
这种方法的主要优势在于:
5. 设置超时
HttpClient的默认超时是100秒,这可能过长。建议设置更合理的超时值。
services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // 将超时设置为10秒
    c.Timeout = TimeSpan.FromSeconds(10);
});
对于HttpClient设置了Timeout,所有请求默认都会使用此值。也可以在单个请求基础上设置不同的超时。
6. 实现弹性模式
HTTP通信受多种因素影响,可能出现间歇性故障。使用弹性模式来处理这些问题:
重试策略
services.AddHttpClient("github")
    .AddTransientHttpErrorPolicy(p => 
        p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));
断路器
services.AddHttpClient("github")
    .AddTransientHttpErrorPolicy(p => 
        p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
组合策略
services.AddHttpClient("github")
    .AddTransientHttpErrorPolicy(p => 
        p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)))
    .AddTransientHttpErrorPolicy(p => 
        p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
这种组合应用了重试策略和断路器模式:请求可能在失败后重试3次,但如果持续失败,断路器会触发并阻止进一步请求30秒。
7. 正确处理销毁
虽然HttpClient实现了IDisposable,但使用HttpClientFactory时不需要显式销毁由工厂创建的客户端。工厂负责管理客户端生命周期。
// 不需要using语句
public async Task<string> GetDataFromApi()
{
    var client = _clientFactory.CreateClient("named-client");
    return await client.GetStringAsync("/api/data");
}
8. 处理取消请求
使用CancellationToken来允许取消长时间运行的请求:
public async Task<string> GetLongRunningDataAsync(CancellationToken cancellationToken = default)
{
    var client = _clientFactory.CreateClient("named-client");
    var response = await client.GetAsync("/api/longrunning", cancellationToken);
    response.EnsureSuccessStatusCode();
    
    return await response.Content.ReadAsStringAsync();
}
允许从控制器取消请求:
[HttpGet]
public async Task<IActionResult> Get(CancellationToken cancellationToken)
{
    try
    {
        var data = await _apiClient.GetLongRunningDataAsync(cancellationToken);
        return Ok(data);
    }
    catch (OperationCanceledException)
    {
        // 请求已取消,无需进一步处理
        return StatusCode(499); // 客户端关闭请求
    }
}
9. 添加请求和响应记录
为HTTP调用添加日志记录,以帮助调试和监控:
services.AddHttpClient("github")
    .AddHttpMessageHandler(() => new LoggingHandler(_loggerFactory));
publicclassLoggingHandler : DelegatingHandler
{
    privatereadonly ILogger _logger;
    public LoggingHandler(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<LoggingHandler>();
    }
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Making request to {Url}", request.RequestUri);
        
        try
        {
            // 测量请求时间
            var stopwatch = Stopwatch.StartNew();
            var response = awaitbase.SendAsync(request, cancellationToken);
            stopwatch.Stop();
            
            _logger.LogInformation("Received response from {Url} with status code {StatusCode} in {ElapsedMilliseconds}ms",
                request.RequestUri, response.StatusCode, stopwatch.ElapsedMilliseconds);
                
            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error making HTTP request to {Url}", request.RequestUri);
            throw;
        }
    }
}
10. 压缩
为了提高性能,尤其是在处理大型响应时,启用HTTP压缩:
services.AddHttpClient("github")
    .ConfigureHttpClient(client =>
    {
        client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
        client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
    });
要处理压缩的响应:
public async Task<string> GetCompressedDataAsync()
{
    var client = _clientFactory.CreateClient("github");
    var response = await client.GetAsync("/api/largedata");
    response.EnsureSuccessStatusCode();
    
    // HttpClient自动处理解压缩
    return await response.Content.ReadAsStringAsync();
}
11. 处理认证
常见的认证方法包括:
基本认证
services.AddHttpClient("authenticated-client")
    .ConfigureHttpClient(client =>
    {
        var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes("username:password"));
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
    });
Bearer令牌
services.AddHttpClient("authenticated-client")
    .ConfigureHttpClient(client =>
    {
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "your-access-token");
    });
动态认证处理
使用HttpClientFactory的AddHttpMessageHandler方法来添加认证处理:
services.AddTransient<AuthenticationHandler>();
services.AddHttpClient("authenticated-client")
    .AddHttpMessageHandler<AuthenticationHandler>();
publicclassAuthenticationHandler : DelegatingHandler
{
    privatereadonly ITokenService _tokenService;
    public AuthenticationHandler(ITokenService tokenService)
    {
        _tokenService = tokenService;
    }
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // 动态获取令牌
        var token = await _tokenService.GetTokenAsync();
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        
        returnawaitbase.SendAsync(request, cancellationToken);
    }
}
12. 处理并发请求
要并行发送多个请求:
public async Task<IEnumerable<Product>> GetProductsAsync(IEnumerable<int> productIds)
{
    var client = _clientFactory.CreateClient("product-api");
    
    var tasks = productIds.Select(id => 
        client.GetFromJsonAsync<Product>($"/api/products/{id}"));
    
    return await Task.WhenAll(tasks);
}
然而,要小心避免启动太多并行请求。考虑批处理或使用信号量来限制并发请求数:
public async Task<IEnumerable<Product>> GetProductsWithSemaphoreAsync(IEnumerable<int> productIds)
{
    var client = _clientFactory.CreateClient("product-api");
    var results = new List<Product>();
    
    // 限制最多5个并发请求
    usingvar semaphore = new SemaphoreSlim(5);
    var tasks = productIds.Select(async id => 
    {
        await semaphore.WaitAsync();
        try
        {
            returnawait client.GetFromJsonAsync<Product>($"/api/products/{id}");
        }
        finally
        {
            semaphore.Release();
        }
    });
    
    returnawait Task.WhenAll(tasks);
}
正确使用HttpClient对于创建高性能、可靠和可维护的应用程序至关重要。通过采用HttpClientFactory和遵循本文中的最佳实践,您可以避免常见的陷阱并构建能够有效处理HTTP通信的强大应用程序。
记住这些关键点:
- • 使用HttpClientFactory而不是直接实例化HttpClient
通过这些实践,您的应用程序将更好地处理网络通信的挑战,并为用户提供更好的体验。
阅读原文:https://mp.weixin.qq.com/s/YOTpp0llYylmAeMGZHcDsQ
该文章在 2025/3/14 10:50:18 编辑过