HttpClient 实践

开始

在 .NetCore2.1 之前只能使用 HttpClient 发起 HTTP 的各种操作,但是 HttpClient 使用中会有一些问题。

  1. 如果过于频繁的创建 HttpClient 实例 ,虽然 HttpClient 实现了 IDisposable ,但是在 HttpClient 释放后,由于 TCP 生命周期的问题的。会导致套接字资源耗尽。抛出 SocketException 异常。
  2. 如果使用单例去处理所有的请求就无法处理DNS变动的问题。

上面两个问题解决起来也不是很复杂,通过封装不同的 HttpClient 实例去处理不同类型的请求就可以。但是在 .Net Core2.1 开始微软提供了一种更加优雅的方式去发起 HTTP 操作。

IHttpClientFactory使用

IHttpClientFactory 以模块化、可配置、弹性的方式提供 HttpClient 的使用方式

使用方式

注入方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 基本注入
services.AddHttpClient();

// 通过别名注入
services.AddHttpClient("api", m =>
{
m.BaseAddress = new Uri("http://api/xxx/xxxx");
m.Timeout = TimeSpan.FromSeconds(3);
m.DefaultRequestHeaders.Add("xxx","xxx");
});

//通过强类型
services.AddHttpClient<ApiHttpClient>();
public class ApiHttpClient
{
private readonly HttpClient _httpClient;

public ApiHttpClient(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("http://api/xxx/xxxx");
_httpClient.Timeout = TimeSpan.FromSeconds(3);
_httpClient.DefaultRequestHeaders.Add("xxx", "xxx");
}

public async Task<bool> SetNameAsync()
{
var response = await _httpClient.GetAsync("/name");
return response.IsSuccessStatusCode;
}
}

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

private readonly ApiHttpClient _apiHttpclient;

private readonly IHttpClientFactory _factory;

public WeatherForecastController(ApiHttpClient apiHttpclient, IHttpClientFactory factory)
{
_apiHttpclient = apiHttpclient;
_factory = factory;
}

[HttpGet]
public async Task Get()
{
var client = _factory.CreateClient();
await client.GetAsync("xxx");


var client1 = _factory.CreateClient("api");
await client.GetAsync("xxx");

await _apiHttpclient.SetNameAsync();
}

HttpClient创建过程

来看看 CreateClient() 的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

public static HttpClient CreateClient(this IHttpClientFactory factory)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}

return factory.CreateClient(Options.DefaultName);
}

private readonly IOptionsMonitor<HttpClientFactoryOptions> _optionsMonitor;
public HttpClient CreateClient(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}

HttpMessageHandler handler = CreateHandler(name);
var client = new HttpClient(handler, disposeHandler: false);

HttpClientFactoryOptions options = _optionsMonitor.Get(name);
for (int i = 0; i < options.HttpClientActions.Count; i++)
{
options.HttpClientActions[i](client);
}

return client;
}

CreateClient() 接受一个 name, 如果 name 为 null 会被给一个默认的值 ”“, 之后创建一个 HttpMessageHandler 对象。那这个 HttpMessageHandler 是什么呢。可以看一张微软官方描述 HTTP 整个过程的流程图。

通过这个图可以看到整个 HTTP 请求就是一组管道组织起来的,而 HttpMessageHandler 处于管道的头部,所有的请求都会经过这里,且 HttpMessageHandler 可以直接构造 Http 响应并且返回跳过剩余的管道流程。其次 HttpMessageHandler 可以针对某一种路由或者全局配置。当请求与路由匹配时才会调用。 所以 HttpMessageHandler 是一个处理请求,响应回复的管道,这个管道由多个 HttpMessageHandler 组织而成。

创建消息处理管道 HttpMessageHandler 后,会将它传递给 HttpClient 实例用于处理响应消息。disposeHandler: false 表示 HttpClient 释放后并不会释放它使用的HttpMessageHandler 对象,最后通过 _optionsMonitor 获取对于 HttpClient 配置,并返回 HttpClient 对象。

每次从 IHttpClientFactory 获取 HttpClient 对象时,返回的都是一个新的实例。 但每个 HttpClient 使用 HttpMessageHandler 是会重用的。HttpMessageHandler 由对象池管理,默认生命周期是两分钟。也可以通过配置来延长它的生命周期来提高复用率。

1
2
3
4
5
6
services.AddHttpClient("api", m =>
{
m.BaseAddress = new Uri("http://api/xxx/xxxx");
m.Timeout = TimeSpan.FromSeconds(3);
m.DefaultRequestHeaders.Add("xxx", "xxx");
}).SetHandlerLifetime(TimeSpan.FromMinutes(5));

自定义HTTP请求管道

既然 HttpMessageHandler 是一个处理请求,响应回复的管道,那么就可以自定义自己的管道。只需要继承 DelegatingHandler 并重写 SendAsync 方法就可以了。DelegatingHandler 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public abstract class DelegatingHandler : HttpMessageHandler
{

private HttpMessageHandler _innerHandler;

public HttpMessageHandler? InnerHandler
{
get => this._innerHandler;
[param: DisallowNull] set
{
if (value == null)
throw new ArgumentNullException(nameof (value));
this.CheckDisposedOrStarted();
if (NetEventSource.Log.IsEnabled())
NetEventSource.Associate((object) this, (object) value, nameof (InnerHandler));
this._innerHandler = value;
}
}

protected DelegatingHandler(HttpMessageHandler innerHandler) => this.InnerHandler = innerHandler;

protected internal override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request == null)
throw new ArgumentNullException(nameof (request), SR.net_http_handler_norequest);
this.SetOperationStarted();
return this._innerHandler.SendAsync(request, cancellationToken);
}
}

DelegatingHandler 里面有一个 HttpMessageHandler 的成员,它是组织请求管道的关键。类似 RequestDelegate

实现自定义管道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class HttpHandlerA : DelegatingHandler
{
private readonly ILogger<HttpHandlerA> _logger;

public HttpHandlerA(ILogger<HttpHandlerA> logger)
{
_logger = logger;
}

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_logger.LogInformation("请求开始");
var res = base.SendAsync(request, cancellationToken);
_logger.LogInformation("请求结束");
return res;
}
}

services.AddHttpClient("api", m =>
{
m.BaseAddress = new Uri("http://api/xxx/xxxx");
m.Timeout = TimeSpan.FromSeconds(3);
m.DefaultRequestHeaders.Add("xxx", "xxx");
}).SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddHttpMessageHandler<HttpHandlerA>();

重试机制

Microsoft.Extensions.Http.Polly 是一个处理 HttpClient 弹性和瞬态故障处理的扩展库,通过 AddPolicyHandler 把策略加入消息处理管道中。

重试流程图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 状态码重试
services.AddHttpClient<ApiHttpClient>().AddPolicyHandler(GetRetryPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
// 自定义条件
.OrResult(msg => (int)msg.StatusCode != 200)
// 指数退避等待重试,这里会重试6+1次。
.WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}


// 超时重试
services.AddHttpClient<ApiHttpClient>()
.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(4))
.AddPolicyHandler(TimeOutRetryPolicy());

static IAsyncPolicy<HttpResponseMessage> TimeOutRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TimeoutRejectedException>()
.WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) + TimeSpan.FromMilliseconds(new Random().Next(0, 100)));
}

HttpClient 实践
http://example.com/posts/1817.html
作者
她微笑的脸y
发布于
2022年8月17日
许可协议