在 Asp.Net Core 1.0 時代,由於設計上的問題, HttpClient 給開發者帶來了無盡的困擾,用 Asp.Net Core 開發團隊的話來說就是:我們註意到,HttpClient 被很多開發人員不正確的使用。得益於 .Net Core 不斷的版本快速升級;解決方案也一一浮出水面,本... ...
前言
在 Asp.Net Core 1.0 時代,由於設計上的問題, HttpClient 給開發者帶來了無盡的困擾,用 Asp.Net Core 開發團隊的話來說就是:我們註意到,HttpClient 被很多開發人員不正確的使用。得益於 .Net Core 不斷的版本快速升級;解決方案也一一浮出水面,本文嘗試從各個業務場景去剖析 HttpClient 的各種使用方式,從而在開發中正確的使用 HttpClient 進行網路請求。
1.0時代發生的事情
1.1 在 1.0 時代,部署在 Linux 上的 Asp.Net Core 應用程式進程出現 “套接字資源耗盡” 的異常,該異常通常是由於不停的創建 HttpClient 的實例後產生,因為每創建一個新的 HttpClient 對象,將會消耗一個套接字資源,而在對象使用完成後,由於設計上的問題,即使手動釋放 HttpClient,也極有可能無法釋放套接字資源。
1.2 思考下麵的代碼,在遠古時代,下麵的代碼將會造成 “套接字資源耗盡” 的異常
public HttpClient CreateHttpClient()
{
return new HttpClient();
}
// 或者
public async Task<string> GetData()
{
using (var client = new HttpClient())
{
var data = await client.GetAsync("https://www.cnblogs.com");
}
return null;
}
1.3 繼而引出了下麵的使用方法,利用靜態對象進行網路請求
private static HttpClient httpClient = null;
public HttpClient CreateHttpClient()
{
if (httpClient == null)
httpClient = new HttpClient();
return httpClient;
}
1.4 上面使用靜態對象的方式可以避免 “套接字資源耗盡” 的異常,但是,有一個致命的問題,當主機 DNS 更新時,你可能會收到另外一個異常
An error occurred while sending the request. Couldn't resolve host name An error occurred while sending the request. Couldn't resolve host name
1.5 該異常指示無法解析主機名稱,其實就是因為靜態 HttpClient 對象不會隨著主機 DNS 更新而更新,這個時候,你通常需要做的就是:重啟服務!
2. 正確的使用 HttpClient
2.1 時間來到了 .Net Core 的 2.2 時代(其實2.1就可以),官方推薦我們應該使用依賴註入的方式去使用 HttpClient,比如在 Startup.cs 的 ConfigureServices 方法中加入下麵的代碼
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHttpClient();
}
2.2 然後再控制器中通過構造方法註入 HttpClient 對象進行使用
public class ValuesController : ControllerBase
{
private HttpClient httpClient;
public ValuesController(HttpClient httpClient)
{
this.httpClient = httpClient;
}
...
}
2.3 在新版本的 Asp.Net Core 中,Asp.Net Core 開發團隊引入了 HttpClientFactory
public HttpClient CreateHttpClient()
{
return HttpClientFactory.Create();
}
2.4 HttpClientFactory 的主要工作就是創建 HttpClient 對象,但是在創建過程中,通過為每個 HttpClient 對象創建一個單獨的清理句柄來對 HttpClient 進行跟蹤和管理,以確保在對象使用完成後能夠及時的釋放網路請求的資源,也就是套接字,具體 HttpClientFactory 內部原理可參考 李志章-DotNetCore深入瞭解之三HttpClientFactory類.
2.5 更重要的是,HttpClientFactory 內部管理著一個連接句柄池,一旦高併發的到來,HttpClientFactory 內句柄池內使用完成但是未被釋放的句柄將被重新使用,雖然使用 HttpClientFactory.Create() 每次都是返回一個新的 HttpClient 對象,但是其背後的管理句柄是可以復用的,換句話說就是 “套接字復用”,而且還不會有 DNS 無法同步更新的問題
2.6 所以現在我們明白了為什麼要使用 HttpClientFactory 來創建 HttpClient 對象
3. 使用類型化的 HttpClient 客戶端
3.1 在常規應用和微服務的應用場景中,都可以使用類型化的客戶端,類型化客戶端這個詞如果不太好理解,那麼你可以理解為為每個業務單獨的使用一個 HttpClient 客戶端,比如獲取天氣預報,思考下麵的代碼
public class WeatherService
{
private HttpClient httpClient;
public WeatherService(HttpClient httpClient)
{
this.httpClient = httpClient;
this.httpClient.BaseAddress = new Uri("http://www.weather.com.cn");
this.httpClient.Timeout = TimeSpan.FromSeconds(30);
}
public async Task<string> GetData()
{
var data = await this.httpClient.GetAsync("/data/sk/101010100.html");
var result = await data.Content.ReadAsStringAsync();
return result;
}
}
3.2 為了在控制器中更好的使用 WeatherService,我們需要把 WeatherService 註入到服務中
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHttpClient();
}
// 然後,在控制器中使用如下代碼
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private WeatherService weatherService;
public ValuesController(WeatherService weatherService)
{
this.weatherService = weatherService;
}
[HttpGet]
public async Task<ActionResult> Get()
{
string result = string.Empty;
try
{
result = await weatherService.GetData();
}
catch { }
return new JsonResult(new { result });
}
}
3.3 運行程式,你將得到 北京市 的天氣
{
result: "{"weatherinfo":{"city":"北京","cityid":"101010100","temp":"27.9","WD":"南風","WS":"小於3級","SD":"28%","AP":"1002hPa","njd":"暫無實況","WSE":"<3","time":"17:55","sm":"2.1","isRadar":"1","Radar":"JC_RADAR_AZ9010_JB"}}"
}
3.4 在微服務中,這種做法很常見,而且非常有用,通過使用類型化的客戶端,除了在構造方法中註入 HttpClient 外,我們還可以註入任何需要的東西到 WeatherService 中,更重要的是,可以對業務應用擴展策略,還方便了管理
3.5 在 WeatherService 類型化客戶端中,雖然每次都是創建了一個新的 HttpClient 對象,但是其內部的句柄和其它 HttpClient 是共用同一個句柄池,無需擔心
4.對 HttpClient 應用策略
4.1 下麵說到的策略組件是業內大名鼎鼎的 Polly (波莉),GitHub 地址:https://github.com/App-vNext/Polly
4.2 使用重試策略,參考 Polly 的 Wiki 示例代碼,使用起來非常簡單
首先需要從 NuGet 中引用包
Polly
Polly.Extensions.Http
4.3 接著在 Startup.cs ConfigureServices 方法中應用策略
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHttpClient<WeatherService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddPolicyHandler(policy =>
{
return HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(3,
retryAttempt => TimeSpan.FromSeconds(2),
(exception, timeSpan, retryCount, context) =>
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("請求出錯了:{0} | {1} ", timeSpan, retryCount);
Console.ForegroundColor = ConsoleColor.Gray;
});
});
}
4.4 以上代碼表示在請求發生錯誤的情況下,重試 3 次,每次 2 秒,針對高併發的請求,重試請求間隔建議使用隨機值
結語
- 本章著重介紹了 HttpClient 在 Asp.Net Core 中的前世今生,簡單介紹了使用原理,介紹了各種使用 HttpClient 的方式
- 介紹了使用了 Polly 對在類型化的客戶端上使用 HttpClient 重試策略,因為對 Polly 理解不夠,其它的策略就不再介紹,大家可以到 Polly 的 Wiki 上深入瞭解
- 最後通過一個簡單的獲取天氣預報的小實例來演示類型化的客戶端使用場景
示例代碼下載
https://files.cnblogs.com/files/viter/Ron.HttpClientDemo.zip