使用HttpClient实现并发请求

在.Net 4.0之前,一直是依靠HttpWebRequest实现Http操作的。它默认有一个非常保守的同一站点下最大2并发数限制,导致默认情况下HttpWebRequest往往得不到理想的速度,必须修改App.config或ServicePointManager.DefaultConnectionLimit的值。所以对于需要高并发请求的场景HttpWebRequest不是一个理想的选择。

MS在.Net 4.5中引入了一个HttpClient类专门处理Http操作,HttpClient不受HttpWebRequest并发策略控制,也没有系统级的并发限制。

关于HttpClient和HttpWebRequest的一些区别参考(https://stackoverflow.com/questions/22214930/httpclient-vs-httpwebrequest)

下面开始进入重点,在本文之前对于HttpClient有过很多错误的使用,最开始使用的如下代码:

写一个静态方法,每次调用该方法都会new一个新的HttpClient对象,使用完成后释放HttpClient对象

        public static HttpResponseMessage GetRequest(string requestUri, string accessToken)
        {
            using (HttpClient client = new HttpClient())
            {
                if (!string.IsNullOrEmpty(accessToken))
                {
                    client.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
                }
                client.DefaultRequestHeaders.Add("Accept", "application/json");
                try
                {
                    var response = client.GetAsync(requestUri).Result;
                    if (response.IsSuccessStatusCode)
                    {
                        return response;
                    }
                    else
                    {
                        throw new Exception(response.Content.ReadAsStringAsync().Result);
                    }
                }
                catch (Exception ex)
                {

                    throw ex;
                }
            }
        }

那这就完成没有发挥出HttpClient的一大优势(同一HttpClient可以发送多个请求),而以上代码一个请求就会创建一个HttpClient对象,同时我们调用HttpClient的Dispose()方法销毁它时,它就启动一个进程,关闭在它控制之下的套接字。也就是说,你下次请求连接时,必须重复整个连接新建过程。如果网络延迟很高,或者连接是受保护的(需要新一轮的SSL/TLS协商),就会非常痛苦。

所以HttpClient关闭套接字的过程并不快。当“关闭”套接字时,你真正做的是将TCP连接状态置为TIME_WAIT。在一个预先配置好的时间窗口内,Windows将保持该套接字的状态不变,默认情况下是4分钟。这是为了防止有任何剩余的数据包仍在传输。

所以以上代码是一个错误的使用方式,如果在一个高并发场景,服务器资源也会迅速耗尽。

所以我们的正确做法应该是:HttpClient应该只初始化一次,并在应用程序的整个生存期内重用。在负载很高的情况下,为每个请求初始化一个HttpClient类会耗尽可用的套接字数量。

以下展示正确的做法,就是对HttpClient做单例处理,使得应用生命周期内只有一个HttpClient对象,并可以重用,并且不调用Dispose()方法。

    internal class HttpClientUtils
    {
        private static HttpClient _httpClient;
        private readonly static object _lock = new object();
        private readonly static HttpClientUtils _httpClientUtils = new HttpClientUtils();

        static HttpClientUtils()
        {
            _httpClient = GetHttpClient();
        }

        public static HttpClientUtils GetHttpClientUtils()
        {
            return _httpClientUtils;
        }

        private HttpClientUtils()
        {
        }

        private static HttpClient GetHttpClient()
        {
            lock (_lock)
            {
                if (_httpClient == null)
                {
                    _httpClient = new HttpClient();
                    //_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
                    _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    _httpClient.Timeout = TimeSpan.FromSeconds(1800);
                }
            }
            return _httpClient;
        }
}

以上代码解决了HttpClient重用问题,但是竟然所有Http请求都使用同一HttpClient对象,那么新的问题也随之而来,可能我们会遇到不同的HttpClient请求需要带不同的Http请求头,

那么如下所示代码,就会造成很大问题,因为并不是所有的Http请求都需要此请求头。

client.DefaultRequestHeaders.Add("Accept", "application/json");
//....

那么问题的关键就在于,我们不能将请求头直接赋值到HttpClient对象的DefaultRequestHeaders属性上。

那么应该如何讲请求头和HttpClient对象分离。我们可以使用SendAsync(HttpRequestMessage request); ,在HttpRequestMessage中添加请求头。

以Get请求为例,示例如下:

        public async Task<HttpResponseMessage> GetRequestAsync(string requestUri, string accessToken)
        {
            HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, requestUri);
            if (!string.IsNullOrEmpty(accessToken))
            {
                message.Headers.Add("Authorization", "Bearer " + accessToken);
            }
            try
            {
                var response = await _httpClient.SendAsync(message);
                //var response = await this._client.GetAsync(requestUri);
                if (response.IsSuccessStatusCode)
                {
                    return response;
                }
                else
                {
                    throw new Exception(response.Content.ReadAsStringAsync().Result);
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

可以看到我们将Http请求方法,Http请求Url以及请求头信息,封装到HttpRequestMessage对象中,使用 SendAsync(HttpRequestMessage request); 发送请求

而这正好解决高并发下Http请求发送的问题,避免不同Http请求互相造成干扰。

 

posted @ 2018-12-18 15:01  公羊天纵  阅读(6032)  评论(0编辑  收藏  举报