在微服务架构中,或与外部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 编辑过