1. 程式人生 > 其它 >ASP.NET Core 6框架揭祕例項演示[17]:利用IHttpClientFactory工廠來建立HttpClient

ASP.NET Core 6框架揭祕例項演示[17]:利用IHttpClientFactory工廠來建立HttpClient

在一個採用依賴注入框架的應用中,我們一般不太推薦利用手工建立的HttpClient物件來進行HTTP呼叫,使用的HttpClient物件最好利用注入的IHttpClientFactory工廠來建立。前者引起的問題,以及後者帶來的好處,將通過如下這幾個演示程式展現出來。IHttpClientFactory型別由“Microsoft.Extensions.Http”這個NuGet包提供,“Microsoft.NET.Sdk.Web”SDK具有該包的預設引用。如果採用“Microsoft.NET.Sdk”這個SDK,需要新增該包的引用。(本篇提供的例項已經彙總到《ASP.NET Core 6框架揭祕-例項演示版

》)

[S1201]頻繁建立HttpClient物件呼叫API(原始碼
[S1202]以單例方式使用HttpClient(原始碼
[S1203]利用IHttpClientFactory工廠建立HttpClient物件(原始碼
[S1204]直接注入HttpClient物件(原始碼
[S1205]定製HttpClient物件(原始碼
[S1206]強型別客戶端(原始碼
[S1207]基於Polly的失敗重試(原始碼

[S1201]頻繁建立HttpClient物件呼叫API

HttpClient型別實現了IDisposable介面,如果採用在每次呼叫時建立新的物件,那麼按照我們理解的程式設計規範,呼叫結束之後就應該主動呼叫Dispose方法及時地將其釋放。如下的演示程式就採用了這種程式設計方式,我們啟動了一個ASP.NET應用,它提供了一個返回“Hello World”的終結點。

using System.Diagnostics;

var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
await app.StartAsync();

while (true)
{
    using (var httpClient = new HttpClient())
    {
        try
        {
            var reply = await httpClient.GetStringAsync("http://localhost:5000");
            Debug.Assert(reply == "Hello World!
"); } catch (Exception ex) { Console.WriteLine(ex.Message); } } }

ASP.NET應用啟動之後,我們在一個無限迴圈中對它發起呼叫。每次迭代的建立的HttpClient物件會在完成呼叫之後被釋放。當我們的程式執行之後,初始階段都沒有問題。當呼叫次數累積到一定規模之後,程式會大量地丟擲HttpRequestExcetion異常,並提示“Only one usage of each socket address (protocol/network address/port) is normally permitted”。


圖1 頻繁建立HttpClient導致的異常

[S1202]以單例方式使用HttpClient

這個演示例項表明頻繁建立HttpClient物件是不可取的。如果我們需要自行建立HttpClient物件並頻繁地使用它們,應該儘可能地複用這個物件。如果將演示程式改寫成如下的形式使用單例的HttpClient物件就不會丟擲上面這個異常,但是這又會帶來一些額外的問題。HttpRequestExcetion異常在前面的例項中為何會出現,後面的例項究竟又有哪些問題,我們將在後面回答這個問題。

using System.Diagnostics;
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
await app.StartAsync();

var httpClient = new HttpClient();
while (true)
{
    try
    {
        var reply = await httpClient.GetStringAsync("http://localhost:5000");
        Debug.Assert(reply == "Hello World!");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

[S1203]利用IHttpClientFactory工廠建立HttpClient物件

引入IHttpClientFactory工廠將會使一切變得簡單,我們只需要在需要進行HTTP呼叫的時候利用這個工廠創建出對應的HttpClient物件就可以了。雖然HttpClient型別實現了IDisposable介面,我們在完成了呼叫之後根本不需要去呼叫它的Dispose方法。在下面的演示程式中,我們呼叫ServiceCollection物件的AddHttpClient擴充套件方法對IHttpClientFactory工廠進行了註冊,並利用構建出來的IServiceProvider物件得到了這個物件。在每次進行HTTP呼叫的時候,我們利用這個IHttpClientFactory工廠實時地將HttpClient物件創建出來。

using System.Diagnostics;

var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
await app.StartAsync();

var httpClientFactory = new ServiceCollection()
    .AddHttpClient()
    .BuildServiceProvider()
    .GetRequiredService<IHttpClientFactory>();

while (true)
{
    try
    {
        var reply = await httpClientFactory.CreateClient().GetStringAsync("http://localhost:5000");
        Debug.Assert(reply == "Hello World!");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

[S1204]直接注入HttpClient物件

上面介紹的CreateClient擴充套件方法還註冊加針對HttpClient型別的服務,所以HttpClient物件可以直接作為注入的服務來使用。在如下所示的演示程式中,我們直接利用IServiceProvider物件來創提供HttpClient物件,它與上面演示的程式是等效的(S1204)。

using System.Diagnostics;

var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
await app.StartAsync();

var serviceProvider = new ServiceCollection()
    .AddHttpClient()
    .BuildServiceProvider();
while (true)
{
    try
    {
        var reply = await serviceProvider.GetRequiredService<HttpClient>().GetStringAsync("http://localhost:5000");
        Debug.Assert(reply == "Hello World!");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

[S1205]定製HttpClient物件

呼叫IServiceCollection介面的AddHttpClient擴充套件方法進行服務註冊的時候可以對HttpClient作相應的定製,比如可以設定超時時間、預設請求報頭和網路代理等。如果應用會涉及針對眾多不同型別API的呼叫,呼叫不同的API可能需要採用不同的設定,比如區域網內部呼叫就比外部呼叫需要更小的超時設定。為了解決這個問題,我們對提供的設定賦予一個唯一的名稱,在使用的時候針對這個標識提取對應的設定來建立HttpClient物件,為了方便描述,我們將這個唯一標識HttpClient設定的名稱就稱為HttpClient的名稱。在接下來演示的例項中,我們將設定兩個HttpClient來呼叫指向“www.foo.com”和“www.bar.com”這兩個域名的API。為此我們需要在host檔案中添加了如下的對映關係

127.0.0.1 www.foo.com
127.0.0.1 www.bar.com

在如下所示的演示例項中,我們為ASP.NET應用註冊的終結點會返回包含請求的域名和路徑。我們呼叫IServiceCollection介面的AddHttpClient方法註冊了兩個名稱分別為“foo”和“bar”的HttpClient,並對它們的基礎地址進行鍼對性的設定(S1205)。

using System.Diagnostics;

var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:80");
app.MapGet("/{path}" , (HttpRequest resquest, HttpResponse response) =>response.WriteAsync($"{resquest.Host}{resquest.Path}"));
await app.StartAsync();

var services = new ServiceCollection();
services.AddHttpClient("foo", httpClient => httpClient.BaseAddress = new Uri("http://www.foo.com"));
services.AddHttpClient("bar", httpClient => httpClient.BaseAddress = new Uri("http://www.bar.com"));
var httpClientFactory = services
    .BuildServiceProvider()
    .GetRequiredService<IHttpClientFactory>();

var reply = await httpClientFactory.CreateClient("foo").GetStringAsync("abc");
Debug.Assert(reply == "www.foo.com/abc");
reply = await httpClientFactory.CreateClient("bar").GetStringAsync("xyz");
Debug.Assert(reply == "www.bar.com/xyz");

我們將HttpClient的註冊名稱作為引數呼叫IHttpClientFactory工廠的Create方法得到對應的HttpClient物件。由於基礎地址已經設定好了,所以在進行HTTP呼叫時只需要指定相對地址(“abc”和“xyz”)就可以了。

[S1206]強型別客戶端

所謂“強型別客戶端”指的針對具體場景自定義的用於呼叫指定API的型別,強型別客戶端直接使用注入的HttpClient進行HTTP呼叫。對於上一個例項的應用場景,我們就可以定義如下兩個客戶端型別FooClient和BarClient,並使用它們分別呼叫指向不同域名的API。如程式碼片段所示,我們直接在其建構函式中注入了HttpClient物件,並在GetStringAsync方法中使用它來完成最終的HTTP呼叫。

public class FooClient
{
    private readonly HttpClient _httpClient;
    public FooClient(HttpClient httpClient) => _httpClient = httpClient;
    public Task<string> GetStringAsync(string path) => _httpClient.GetStringAsync(path);
}

public class BarClient
{
    private readonly HttpClient _httpClient;
    public BarClient(HttpClient httpClient) => _httpClient = httpClient;
    public Task<string> GetStringAsync(string path) => _httpClient.GetStringAsync(path);
}

由於FooClient和BarClient對使用的HttpClient具有不同的要求,所以我們採用如下的方式呼叫IServiceCollection介面的AddHttpClient<TClient>針對客戶端型別對HttpClient進行鍼對設定,具體設定的依然是基礎地址。由於AddHttpClient<TClient>擴充套件方法會將作為泛型引數的TClient型別註冊為服務,所以我們可以直接利用IServiceProvider物件提取對應的客戶端例項(S1206)。

using App;
using System.Diagnostics;

var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:80");
app.MapGet("/{path}", (HttpRequest resquest, HttpResponse response)=> response.WriteAsync($"{resquest.Host}{resquest.Path}"));
await app.StartAsync();

var services = new ServiceCollection();
services.AddHttpClient<FooClient>("foo", httpClient=> httpClient.BaseAddress = new Uri("http://www.foo.com"));
services.AddHttpClient<BarClient>("bar", httpClient=> httpClient.BaseAddress = new Uri("http://www.bar.com"));
var serviceProvider = services.BuildServiceProvider();
var foo = serviceProvider.GetRequiredService<FooClient>();
var bar = serviceProvider.GetRequiredService<BarClient>();

var reply = await foo.GetStringAsync("abc");
Debug.Assert(reply == "www.foo.com/abc");
reply = await bar.GetStringAsync("xyz");
Debug.Assert(reply == "www.bar.com/xyz");

[S1207]基於Polly的失敗重試

在任何環境下都不可能確保次HTTP呼叫都能成功,所以在失敗重試是很有必要的。失敗重試是要講究策略的,返回何種響應狀態才需要重試?重試多少次?時間間隔多長?一提到策略化自動重試,大多數人會想到Polly這個開源框架,“Microsoft.Extensions.Http.Polly”這個NuGet包提供了IHttpClientFactory工廠和Polly的整合。在添加了這個包引用之後,我們將演示程式做了如下的修改。如程式碼片段所示,我們註冊的終結點接收到的每三個請求只有一個會返回狀態碼為200的響應,其餘兩個響應碼均為500。如果客戶端能夠確保失敗後至少進行兩次重試,那麼就能保證客戶端呼叫100%成功(S1207)。

using Polly;
using Polly.Extensions.Http;
using System.Diagnostics;

var app = WebApplication.Create(args);
var counter = 0;
app.MapGet("/", (HttpResponse response) => response.StatusCode = counter++ % 3 == 0 ? 200 : 500);
await app.StartAsync();

var services = new ServiceCollection();
services
    .AddHttpClient(string.Empty)
    .AddPolicyHandler(HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(2, _ => TimeSpan.FromSeconds(1)));
var httpClientFactory = services
    .BuildServiceProvider()
    .GetRequiredService<IHttpClientFactory>();

while (true)
{
    var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000");
    var response = await httpClientFactory.CreateClient().SendAsync(request);
    Debug.Assert(response.IsSuccessStatusCode);
}

如上面的程式碼片段所示,呼叫AddHttpClient擴充套件方法註冊了一個預設匿名HttpClient(名稱採用空字串)之後,我們接著呼叫返回的IHttpClientBuilder物件的AddPolicyHandler擴充套件方法設定了失敗重試策略。AddPolicyHandler方法的引數型別為IAsyncPolicy<HttpResponseMessage>的引數,我們利用HttpPolicyExtensions型別的HandleTransientHttpError靜態方法建立一個用來處理偶發錯誤(比如HttpRequestException異常和5XX/408響應)的PolicyBuilder<HttpResponseMessage>物件。我們最終呼叫該物件的WaitAndRetryAsync方法返回所需的IAsyncPolicy<HttpResponseMessage>物件,並通過引數設定了重試次數(兩次)和每次重試時間間隔(1秒)。

在利用代表依賴注入容器的IServiceProvider物件得到IHttpClientFactory之後,我們在一個無限迴圈中利用它建立的HttpClient對本地承載的API發起呼叫,雖然服務端每三次呼叫只有一次是成功的,但是2次重試足以確保最終的呼叫是成功的,我們提供的除錯斷言證實了這一點。