HTTPClient 踩坑记

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?

使用 Hugo 构建
主题 StackJimmy 设计