HTTPClient 踩坑记

  1. 1. HttpClient的坑
  2. 2. HttpClient的优点
  3. 3. Further reading

HttpClient是随着.Net framework 4.5一起发布的现代Http库。比起WebClient,HttpClient最大的优点就是
加入了C#5中的async/await异步方法的支持。async/await的坑暂且不表,今天就来说一说这个HttpClient

HttpClient的坑

HttpClient实现了IDisposable接口,很多小伙伴一看到IDisposeable接口就纷纷把HttpClient套在了using里边

1
2
3
4
5
//bad httpclient usage
using(var client = new HttpClient())
{
//do stuffs
}

这种用法是错误的.HttpClient在设计之初被设计为一个可重用的对象,它的生命周期应该与应用程序相一致.上述错误的用法每发起一个请求就会创建一个新的HttpClient,并且在收到回复之后立即把HttpClient dispose掉。众所周知TCP连接在真正断开之前会有几分钟处于CLOSE_WAIT状态。这个状态下TCP链接并没有真正断开。短时间内大量发出Http请求会使系统可用的端口急剧消耗。

MS的人推荐重用HttpClient以使其生命周期与应用相同

1
2
3
4
5
6
7
8
9
10
11
//good httpclient usage
class GoodHttpClientSample
{
private static readonly HttpClient client = new HttpClient();
public Task<string> GetStringAsync(string url)
{
var resposne = await client.GetAsync(url).ConfigureAwait(false);
return response.Content.ReadAsStringAsync();
}
}

HttpClient的优点

踩过了坑我们再来说说他的好处。去掉async/await支持这个最大的有点,HttpClient的一个构造函数的重载接受一个HttpMessageHandler。这个重载很有意思。HttpMessageHandler可以在发出Http请求和接受Http回复时做出一些回应。.net framework里有一个类叫做DelegatingHandler,它继承了HttpMessageHandler。叫做DelegatingHandler是因为它有个类型为HttpMessageHandler的InnerHandler属性,因而可以把请求delegate给InnerHandler。通过这个DelegatingHandler我们可以请以实现像Java web里的filter chain一样的逻辑。

今天重构了公司的代码。公司现有的HttpManager提供了GET和POST两种Http动词的异步方法。在这些方法中还进行了日志记录和失败重试。日志记录和失败重试相关的代码非常重复,但是又无法写成一个函数。因此我把这部分逻辑抽出来做成了两个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
class LogHandler:DelegatingHandler
{
private readonly ILog _log;
public LogHandler(ILog log,HttpMessageHandler handler):base(handler)
{
_log = log;
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
try
{
var begin = DateTime.Now;
_log.I($"{request.Method} ->{request.RequestUri}");
var response = await base.SendAsync(request,cancellationToken);
var end = DateTime.Now;
var diff = (end-begin).TotalMillseconds;
_log.I($"{request.Method} <- {response.Content.ReadAsStringAsync()} cost{diff}ms");
}
catch(Exception e)
{
_log.E($"{request.Method} ->{request.RequestUri},{e}");
throw;
}
}
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
33
34
35
class RetryHandler:DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken cancellationToken)
{
HttpResponseMessage response = null;
for(int i=0;i<=_retryTimes,i++)
{
try
{
response = await base.SendAsync(request,cancellationToken);
return response;
}
catch(Exception)
{
if(i==_retryTimes)
{
throw;
}
}
//make compiler happy.
return response;
}
}
private readonly int _retryTimes = 0;
public RetryHandler(int retryTimes,HttpMessageHandler handler):base(handler)
{
if(retryTimes<0)
{
throw new ArgumentOutOfRangeException(nameof(retryTimes));
}
_retryTimes = retryTimes;
}
}

HttpClientHandler是个真正实现HttpClient逻辑的HttpMessageHandler。因此,我们只要保证最内部的Handler时HttpClientHandler就OK了。

1
2
3
4
private static readonly ILog _log;
private static readonly HttpClient _client = new
HttpClient(new RetryHandler(3,new LogHandler(_log,new HttpClientHandler())));

需要注意的是,如果HttpHandler没有返回HttpResponseMessage,对应的异步方法会在运行时抛出InvalidOperationException

重构之后整个Http封装类的代码行数从400行减少到了120行左右,可读性和可维护性提升显著。

Further reading

FUN WITH THE HTTPCLIENT PIPELINE

YOU’RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE

Do HttpClient and HttpClientHandler have to be disposed?