flag
mode_edit
description 字数:1367 字
schedule 阅读:3 分钟

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

HttpClient的坑

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

//bad httpclient usage
using(var client = new HttpClient())
{
    //do stuffs
}

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

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

//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

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;
        }
    }
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了。

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?