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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM