使用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请求互相造成干扰。