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
| using(var client = new HttpClient()) { }
|
这种用法是错误的.HttpClient在设计之初被设计为一个可重用的对象,它的生命周期应该与应用程序相一致.上述错误的用法每发起一个请求就会创建一个新的HttpClient,并且在收到回复之后立即把HttpClient dispose掉。众所周知TCP连接在真正断开之前会有几分钟处于CLOSE_WAIT状态。这个状态下TCP链接并没有真正断开。短时间内大量发出Http请求会使系统可用的端口急剧消耗。
MS的人推荐重用HttpClient以使其生命周期与应用相同
1 2 3 4 5 6 7 8 9 10 11
| 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; } } 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?