(6)ASP.NET Core 中使用IHttpClientFactory發出HTTP請求
1.HttpClient類使用存在的問題
HttpClient類的使用所存在的問題,百度搜索的文章一大堆,好多都是單純文字描述,讓人感覺不太好理解,為了更好理解HttpClient使用存在的問題,下面讓我們通過程式碼跟示例來描述。
using(var client = new HttpClient())
傳統關閉連線方法如上述程式碼所示,但當使用using語句釋放HttpClient物件的時候,套接字(socket)也不會立即釋放,下面我們通過請求aspnetmonsters站點的示例來驗證下:
class Program { static void Main(string[] args) { Console.WriteLine("Starting connections"); var g = GetAsync(); g.Wait(); Console.WriteLine("Connections done"); Console.ReadKey(); } static async Task GetAsync() { for (int i = 0; i < 5; i++) { using (var client = new HttpClient()) { var result = await client.GetAsync("http://aspnetmonsters.com/"); Console.WriteLine(result.StatusCode); } } } }
輸出結果:
控制檯打印出五條請求站點返回狀態的資訊,下面我們通過netstat工具打印出五個請求連線套接字狀態:
應用程式已經執行結束了(結束連線),但是列印結果顯示連線狀態仍然是TIME_WAIT,也就是說在此狀態期間仍然在觀察是否有資料包進入連線(如果連線等待中有任何資料包仍然會通過),因為它們可能在某個地方被網路延遲,這是我從tcpstate竊取的TCP / IP狀態圖。
Windows將在此狀態下保持連線240秒(由其設定[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay])。Windows可以快速開啟新套接字的速度有限,因此如果您耗盡連線池,那麼您可能會看到如下錯誤:
而怎麼做才可以減少套接字的浪費呢?我們在上述程式碼中把每次迴圈中建立的HttpClient物件拉到Main外定義為一個共享的靜態例項:
class Program { private static HttpClient client = new HttpClient(); static void Main(string[] args) { Console.WriteLine("Starting connections"); var g = GetAsync(); g.Wait(); Console.WriteLine("Connections done"); Console.ReadKey(); } static async Task GetAsync() { for (int i = 0; i < 5; i++) { var result = await client.GetAsync("http://aspnetmonsters.com/"); Console.WriteLine(result.StatusCode); } } }
應用程式運動完畢之後,我們再通過netstat工具打印出五個請求連線套接字狀態,這時候會看到資訊如下:
通過共享一個例項,減少了套接字的浪費,實際上由於套接字重用而傳輸快一點。
總結:
●在建立HttpClient例項的時候,最好是靜態(static )例項。
●不要用using包裝HttpClient物件。
在.NET Core 2.1版本之後引入的 HttpClientFactory解決了HttpClient的所有痛點。有了 HttpClientFactory,我們不需要關心如何建立HttpClient,又如何釋放它。通過它可以建立具有特定業務的HttpClient,而且可以很友好的和 DI 容器結合使用,更為靈活。下面以 ASP.NET Core為例介紹HttpClientFactory的四種使用方式。
2.HttpClientFactory 的多種使用方式
可以通過多種使用方式在應用程式中使用HttpClientFactory。
2.1直接使用HttpClientFactory
在Startup.ConfigureServices方法中,通過在IServiceCollection上呼叫AddHttpClient擴充套件方法可以註冊IHttpClientFactory服務。
services.AddHttpClient();
註冊服務後,我們新建BasicUsageModel類使用IHttpClientFactory建立HttpClient例項:
public class BasicUsageModel { private readonly IHttpClientFactory _clientFactory; public IEnumerable<GitHubBranch> Branches { get; private set; } public bool GetBranchesError { get; private set; } public BasicUsageModel(IHttpClientFactory clientFactory) { _clientFactory = clientFactory; } public async Task OnGet() { var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/aspnet/AspNetCore.Docs/branches"); request.Headers.Add("Accept", "application/vnd.github.v3+json"); request.Headers.Add("User-Agent", "HttpClientFactory-Sample"); var client = _clientFactory.CreateClient(); var response = await client.SendAsync(request); if (response.IsSuccessStatusCode) { Branches = await response.Content .ReadAsAsync<IEnumerable<GitHubBranch>>(); } else { GetBranchesError = true; Branches = Array.Empty<GitHubBranch>(); } } } public class GitHubBranch { public string name { get; set; } }
以這種方式直接在使用IHttpClientFactory的類中呼叫CreateClient方法建立HttpClient例項。然後在Controller中呼叫BasicUsageModel類:
public class HomeController : Controller { private readonly IHttpClientFactory _clientFactory; public HomeController(IHttpClientFactory clientFactory) { _clientFactory = clientFactory; } public IActionResult Index() { BasicUsageModel model = new BasicUsageModel(_clientFactory); var task = model.OnGet(); task.Wait(); List<GitHubBranch> list = model.Branches.ToList(); return View(list); } }
2.2使用命名客戶端
如果應用程式需要有許多不同的HttpClient用法(每種用法的服務配置都不同),可以視情況使用命名客戶端。可以在HttpClient中註冊時指定命名Startup.ConfigureServices的配置。
services.AddHttpClient("github", c => { c.BaseAddress = new Uri("https://api.github.com/"); // Github API versioning c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // Github requires a user-agent c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); });
上面的程式碼呼叫AddHttpClient,同時提供名稱“github”。此客戶端應用了一些預設配置,也就是需要基址和兩個標頭來使用GitHub API。每次呼叫CreateClient時,都會建立HttpClient 的新例項,並呼叫配置操作。要使用命名客戶端,可將字串引數傳遞到CreateClient。指定要建立的客戶端的名稱:
public class NamedClientModel : PageModel { private readonly IHttpClientFactory _clientFactory; public IEnumerable<GitHubPullRequest> PullRequests { get; private set; } public bool GetPullRequestsError { get; private set; } public bool HasPullRequests => PullRequests.Any(); public NamedClientModel(IHttpClientFactory clientFactory) { _clientFactory = clientFactory; } public async Task OnGet() { var request = new HttpRequestMessage(HttpMethod.Get, "repos/aspnet/AspNetCore.Docs/pulls"); var client = _clientFactory.CreateClient("github"); var response = await client.SendAsync(request); if (response.IsSuccessStatusCode) { PullRequests = await response.Content .ReadAsAsync<IEnumerable<GitHubPullRequest>>(); } else { GetPullRequestsError = true; PullRequests = Array.Empty<GitHubPullRequest>(); } } } public class GitHubPullRequest { public string url { get; set; } public int? id { get; set; } public string node_id { get; set; } }
在上述程式碼中,請求不需要指定主機名。可以僅傳遞路徑,因為採用了為客戶端配置的基址。在Controller中呼叫方法如上個示例。
2.3使用型別化客戶端
什麼是“型別化客戶端”?它只是DefaultHttpClientFactory注入時配置的HttpClient。
下圖顯示瞭如何將型別化客戶端與HttpClientFactory結合使用:
型別化客戶端提供與命名客戶端一樣的功能,不需要將字串用作金鑰。它們提供單個地址來配置特定HttpClient並與其進行互動。例如,單個型別化客戶端可能用於單個後端終結點,並封裝此終結點的所有處理邏輯。另一個優勢是它們使用 DI 且可以被注入到應用中需要的位置。
型別化客戶端在建構函式中接收HttpClient引數:
public class GitHubService { public HttpClient Client { get; } public GitHubService(HttpClient client) { client.BaseAddress = new Uri("https://api.github.com/"); // GitHub API versioning client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // GitHub requires a user-agent client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); Client = client; } public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues() { var response = await Client.GetAsync( "/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc"); response.EnsureSuccessStatusCode(); var result = await response.Content .ReadAsAsync<IEnumerable<GitHubIssue>>(); return result; } } public class GitHubIssue { public string url { get; set; } public int? id { get; set; } public string node_id { get; set; } }
在上述程式碼中,配置轉移到了型別化客戶端中。HttpClient物件公開為公共屬性。可以定義公開HttpClient功能的特定於API的方法。GetAspNetDocsIssues方法從GitHub儲存庫封裝查詢和分析最新待解決問題所需的程式碼。
要註冊型別化客戶端,可在Startup.ConfigureServices中使用通用的AddHttpClient擴充套件方法,指定型別化客戶端類:
services.AddHttpClient<GitHubService>();
使用DI將型別客戶端註冊為暫時客戶端。可以直接插入或使用型別化客戶端:
public class TypedClientModel : PageModel { private readonly GitHubService _gitHubService; public IEnumerable<GitHubIssue> LatestIssues { get; private set; } public bool HasIssue => LatestIssues.Any(); public bool GetIssuesError { get; private set; } public TypedClientModel(GitHubService gitHubService) { _gitHubService = gitHubService; } public async Task OnGet() { try { LatestIssues = await _gitHubService.GetAspNetDocsIssues(); } catch (HttpRequestException) { GetIssuesError = true; LatestIssues = Array.Empty<GitHubIssue>(); } } }
參考文獻:
在ASP.NET Core中使用IHttpClientFactory發出HTTP請求
你正在以錯誤方式使用 HttpClient,這將導致軟體受損