在微服务架构中,或与外部API通信时,HTTP客户端是必不可少的组件。然而,许多开发人员在实现HTTP客户端时未能充分考虑性能和可用性。
本文将介绍使用C#中的HttpClient
类的最佳实践,并探讨HTTP通信的一些重要方面。
初学者最常犯的错误是在每次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
),因为操作系统需要几分钟来回收套接字。
另一个常见做法是将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
是为长期使用而设计的,但将其作为单例使用有其问题:
HttpClient
不会自动响应DNS变更.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
public class GithubService
{
private readonly 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();
return await response.Content.ReadAsStringAsync();
}
}
HttpClientFactory
提供以下优势:
HttpClientMessageHandler
的生命周期可以通过使用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
public interface IGithubClient
{
Task<IEnumerable<GithubIssue>> GetAspNetDocsIssues();
}
public class GithubClient : IGithubClient
{
private readonly HttpClient _httpClient;
public GithubClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async 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);
}
}
这种方法的主要优势在于:
HttpClient
的默认超时是100秒,这可能过长。建议设置更合理的超时值。
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
// 将超时设置为10秒
c.Timeout = TimeSpan.FromSeconds(10);
});
对于HttpClient
设置了Timeout,所有请求默认都会使用此值。也可以在单个请求基础上设置不同的超时。
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秒。
虽然HttpClient
实现了IDisposable
,但使用HttpClientFactory
时不需要显式销毁由工厂创建的客户端。工厂负责管理客户端生命周期。
// 不需要using语句
public async Task<string> GetDataFromApi()
{
var client = _clientFactory.CreateClient("named-client");
return await client.GetStringAsync("/api/data");
}
使用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); // 客户端关闭请求
}
}
为HTTP调用添加日志记录,以帮助调试和监控:
services.AddHttpClient("github")
.AddHttpMessageHandler(() => new LoggingHandler(_loggerFactory));
public class LoggingHandler : DelegatingHandler
{
private readonly 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 = await base.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;
}
}
}
为了提高性能,尤其是在处理大型响应时,启用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();
}
常见的认证方法包括:
services.AddHttpClient("authenticated-client")
.ConfigureHttpClient(client =>
{
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes("username:password"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
});
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>();
public class AuthenticationHandler : DelegatingHandler
{
private readonly 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);
return await base.SendAsync(request, cancellationToken);
}
}
要并行发送多个请求:
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个并发请求
using var semaphore = new SemaphoreSlim(5);
var tasks = productIds.Select(async id =>
{
await semaphore.WaitAsync();
try
{
return await client.GetFromJsonAsync<Product>($"/api/products/{id}");
}
finally
{
semaphore.Release();
}
});
return await Task.WhenAll(tasks);
}
正确使用HttpClient
对于创建高性能、可靠和可维护的应用程序至关重要。通过采用HttpClientFactory
和遵循本文中的最佳实践,您可以避免常见的陷阱并构建能够有效处理HTTP通信的强大应用程序。
记住这些关键点:
HttpClientFactory
而不是直接实例化HttpClient
通过这些实践,您的应用程序将更好地处理网络通信的挑战,并为用户提供更好的体验。