使用 HttpClientFactory 實作復原性 HTTP 要求

提示

本內容節錄自《容器化 .NET 應用程式的 .NET 微服務架構》(.NET Microservices Architecture for Containerized .NET Applications) 電子書,可以在 .NET Docs 上取得,或免費下載可供離線閱讀的 PDF。

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

IHttpClientFactory 是由作為意向明確的中心 DefaultHttpClientFactory 實作的合約,自 .NET Core 2.1 開始提供,可用來建立 HttpClient 執行個體以用於您的應用程式。

.NET 中所提供之原始 HttpClient 類別的問題

原始且已知的 HttpClient 類別的使用方式很簡單,但在某些情況下,許多開發人員都沒有正確地加以使用。

雖然這個類別會實作 IDisposable,但不建議在 using 陳述式內加以宣告及具現化,因為處置 HttpClient 物件時,底層通訊端並不會立即釋放,而這可能會導致「通訊端耗盡」問題。 如需有關此問題的詳細資訊,請參閱您使用 HttpClient 的方法錯誤而導致軟體不穩定 (英文) 部落格文章。

因此,您應該將 HttpClient 具現化一次,然後在整個應用程式生命週期中重複使用。 為每個要求具現化 HttpClient 類別將會在負載過重時耗盡可用的通訊端數目。 該問題會導致 SocketException 錯誤。 解決該問題的可能方法為建立 HttpClient 物件做為單一物件或靜態物件,如 Microsoft 關於 HttpClient 用法的文章中所述。 這可能是每天只會執行數次的短期主控台應用程式或類似項目的良好解決方案。

開發人員遇到的另一個問題是在長時間執行的處理序中使用 HttpClient 的共用執行個體時。 在 HttpClient 具現化為 singleton 或靜態物件的情況下,其並無法處理 DNS 變更,如 dotnet/runtime GitHub 存放庫的此問題 (英文) 中所述。

不過,真正的問題並不在於 HttpClient,而是在於 HttpClient 的預設建構函式,因為其會建立 HttpMessageHandler 的新具體執行個體,其就是具有上述「通訊端耗盡」與 DNS 變更問題的執行個體。

為了解決上述問題,並讓 HttpClient 執行個體可供管理,.NET Core 2.1 引進了兩種方法,其中一種方法是 IHttpClientFactory。 其是用來透過相依性插入 (DI) 在應用程式中設定及建立 HttpClient 執行個體的介面。 其也提供適用於 Polly 型中介軟體的延伸模組,以利用 HttpClient 中的委派處理常式。

替代方法是搭配已設定的 PooledConnectionLifetime 使用 SocketsHttpHandler。 此方法會套用至長期執行、static 或 singleton HttpClient 執行個體。 若要深入了解不同的策略,請參閱適用於 .NET 的 HttpClient 指導方針

Polly (英文) 是暫時性錯誤處理程式庫,可協助開發人員使用一些預先定義的原則,以流暢且安全執行緒的方式,將復原功能新增至其應用程式。

使用 IHttpClientFactory 的優點

目前 IHttpClientFactory 的實作 (其也會實作 IHttpMessageHandlerFactory) 能提供下列優點:

  • 提供一個集中位置以便命名及設定邏輯 HttpClient 物件。 例如,您可以設定一個已預先設定為存取特定微服務的用戶端 (服務代理程式)。
  • 透過委派 HttpClient 中的處理常式及實作 Polly 型中介軟體以利用 Polly 適用於復原的原則,來撰寫連出中介軟體的概念。
  • HttpClient 已經有委派可針對傳出 HTTP 要求連結在一起之處理常式的概念。 您可以將 HTTP 用戶端註冊到中心,而且您可以使用 Polly 處理常式使用適用於 Retry、CircuitBreakers 等的 Polly 原則。
  • 管理 HttpMessageHandler 的存留期,以避免自行管理 HttpClient 存留期時可能發生的上述問題。

提示

DI 插入的 HttpClient 執行個體可以安全地處置,因為相關聯的 HttpMessageHandler 是由中心管理。 插入的 HttpClient 執行個體從 DI 的觀點來看是「暫時性」的,而 HttpMessageHandler 執行個體可以視為「具範圍」的。 HttpMessageHandler 執行個體有自己的 DI 範圍,與應用程式範圍分開 (例如 ASP.NET 連入要求範圍)。 如需詳細資訊,請參閱在 .NET 中使用 HttpClientFactory

注意

IHttpClientFactory 的實作 (DefaultHttpClientFactory) 與 Microsoft.Extensions.DependencyInjection NuGet 套件中的 DI 實作緊密繫結。 如果您需要在沒有 DI 或搭配其他 DI 實作的情況下使用 HttpClient,請考慮使用以設定 PooledConnectionLifetimestatic 或 singleton HttpClient。 如需詳細資訊,請參閱適用於 .NET 的 HttpClient 指導方針

使用 IHttpClientFactory 的多種方式

有多種方式可在您的應用程式中使用 IHttpClientFactory

  • 基本使用方式
  • 使用具名用戶端
  • 使用具型別用戶端
  • 使用產生的用戶端

為了簡潔起見,此指導方針會示範以結構化程度最高的方式使用 IHttpClientFactory,也就是使用型別用戶端 (服務代理程式模式)。 但是我們已記錄所有選項,而且目前已詳列在這篇涵蓋 IHttpClientFactory 用法的文章中。

注意

如果您的應用程式需要 Cookie,最好避免在應用程式中使用 IHttpClientFactory。 如需管理用戶端的替代方式,請參閱使用 HTTP 用戶端的指導方針

如何搭配 IHttpClientFactory 使用型別用戶端

那麼,什麼是「具型別用戶端」? 其只是針對某些特定用途預先設定的 HttpClient。 此設定可以包括特定值,例如基底伺服器、HTTP 標頭或逾時。

下圖顯示具型別用戶端如何搭配 IHttpClientFactory 使用:

Diagram showing how typed clients are used with IHttpClientFactory.

圖 8-4。 搭配型別用戶端類別使用 IHttpClientFactory

在上圖中,ClientService (由控制器或用戶端程式碼所使用) 會使用由已註冊的 IHttpClientFactory 所建立的 HttpClient。 此中心會將 HttpMessageHandler 從集區指派給 HttpClient。 使用擴充方法 AddHttpClient 在 DI 容器中註冊 IHttpClientFactory 時,可以使用 Polly 的原則設定 HttpClient

若要設定上述結構,請透過安裝包括適用於 IServiceCollectionAddHttpClient 擴充方法的 Microsoft.Extensions.Http NuGet 套件,將 IHttpClientFactory 新增至您的應用程式。 這個擴充方法會註冊將用來作為介面 IHttpClientFactory 之 singleton 的內部 DefaultHttpClientFactory 類別。 它為 HttpMessageHandlerBuilder 定義暫時性設定。 此訊息處理常式 (HttpMessageHandler 物件) 取自集區,供處理站傳回的 HttpClient 使用。

在下一個程式碼片段中,您可以看到如何使用 AddHttpClient() 來註冊需要使用 HttpClient 的型別用戶端 (服務代理程式)。

// Program.cs
//Add http client services at ConfigureServices(IServiceCollection services)
builder.Services.AddHttpClient<ICatalogService, CatalogService>();
builder.Services.AddHttpClient<IBasketService, BasketService>();
builder.Services.AddHttpClient<IOrderingService, OrderingService>();

如上一個程式碼片段所示註冊用戶端服務之後,會使 DefaultClientFactory 針對每個服務建立標準的 HttpClient。 型別用戶端會向 DI 容器註冊為暫時性。 在上述程式碼中,AddHttpClient() 會將 CatalogServiceBasketServiceOrderingService 註冊為暫時性服務,以便直接加以插入及取用,而不需要額外的註冊。

您也可以在註冊中新增執行個體專屬設定以進行設定基底位址之類的動作,並新增一些復原原則,如下所示:

builder.Services.AddHttpClient<ICatalogService, CatalogService>(client =>
{
    client.BaseAddress = new Uri(builder.Configuration["BaseUrl"]);
})
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

在下一個範例中,您可以看到上述其中一個原則的設定:

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
        .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

您可以在下一篇文章中找到有關使用 Polly 的更多詳細資料。

HttpClient 存留期

每次您從 IHttpClientFactory 取得 HttpClient 物件時,都會傳回一個新的執行個體。 但只要 HttpMessageHandler 的存留期間尚未過期,每個 HttpClient 就都會使用 HttpMessageHandler,它會由 IHttpClientFactory 組成集區並重複使用以減少資源耗用量。

處理常式的集合是需要的做法,因為每個處理常式通常會管理自己的基礎 HTTP 連線;建立比所需數目更多的處理常式可能會導致連線延遲。 有些處理常式也會保持連線無限期地開啟,這可能導致處理常式無法對 DNS 變更回應。

集區中的 HttpMessageHandler 物件有存留期,也就是集區中該 HttpMessageHandler 執行個體可重複使用的時間長度。 預設值為 2 分鐘,但是可針對各個具型別用戶端分別覆寫。 若要覆寫它,請在建立用戶端時所傳回的 IHttpClientBuilder 上呼叫 SetHandlerLifetime(),如下列程式碼所示:

//Set 5 min as the lifetime for the HttpMessageHandler objects in the pool used for the Catalog Typed Client
builder.Services.AddHttpClient<ICatalogService, CatalogService>()
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

每個具型別用戶端都可以設定自己的處理常式存留期值。 將存留期設定成 InfiniteTimeSpan 以停用處理常式到期時間。

實作使用已插入和已設定之 HttpClient 的具型別用戶端類別

如同上一個步驟,您必須先定義型別用戶端類別,例如範例程式碼中的類別,像是 'BasketService'、'CatalogService'、'OrderingService' 等等。型別用戶端是接受 HttpClient 物件 (透過其建構函式插入) 並使用該物件來呼叫某些遠端 HTTP 服務的類別。 例如:

public class CatalogService : ICatalogService
{
    private readonly HttpClient _httpClient;
    private readonly string _remoteServiceBaseUrl;

    public CatalogService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Catalog> GetCatalogItems(int page, int take,
                                               int? brand, int? type)
    {
        var uri = API.Catalog.GetAllCatalogItems(_remoteServiceBaseUrl,
                                                 page, take, brand, type);

        var responseString = await _httpClient.GetStringAsync(uri);

        var catalog = JsonConvert.DeserializeObject<Catalog>(responseString);
        return catalog;
    }
}

型別用戶端 (範例中的 CatalogService) 是由 DI (相依性插入) 啟用,這表示除了 HttpClient 之外,其可以接受其建構函式中任何已註冊的服務。

型別用戶端實際上是暫時性物件,也就是說只要需要時就會建立一個新的執行個體。 每次加以建構時,其都會收到新的 HttpClient 執行個體。 不過,集區中的 HttpMessageHandler 物件是可由多個 HttpClient 執行個體重複使用的物件。

使用具型別用戶端類別

最後,一旦您實作型別類別之後,就可以使用 AddHttpClient() 加以註冊及設定。 之後,您就可以在由 DI 插入服務的位置加以使用,例如在 Razor 頁面代碼或 MVC Web 應用程式控制器中,如下列來自 eShopOnContainers 的程式碼所示:

namespace Microsoft.eShopOnContainers.WebMVC.Controllers
{
    public class CatalogController : Controller
    {
        private ICatalogService _catalogSvc;

        public CatalogController(ICatalogService catalogSvc) =>
                                                           _catalogSvc = catalogSvc;

        public async Task<IActionResult> Index(int? BrandFilterApplied,
                                               int? TypesFilterApplied,
                                               int? page,
                                               [FromQuery]string errorMsg)
        {
            var itemsPage = 10;
            var catalog = await _catalogSvc.GetCatalogItems(page ?? 0,
                                                            itemsPage,
                                                            BrandFilterApplied,
                                                            TypesFilterApplied);
            //… Additional code
        }

        }
}

目前為止,上述程式碼片段只會顯示執行一般 HTTP 要求的範例。 但是最精彩的內容則位於接下來的小節中,其中會示範如何讓 HttpClient 進行的所有 HTTP 要求都具有復原性原則,例如搭配指數輪詢進行重試、斷路器、使用驗證權杖的安全性功能,或甚至是任何其他自訂功能。 而只要將原則和委派處理常式新增至您已註冊的型別用戶端,便能達成上述所有作業。

其他資源