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”。

Picture1
圖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%成功。

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次重試足以確保最終的調用是成功的,我們提供的調試斷言證實了這一點。


免責聲明!

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



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