如何使用 HttpClient 和 HttpClientFactory / SocketsHttpHandler

HttpClient 原本存在的問題,現在通過 HttpClientFactory / SocketsHttpHandler 的 Connection Pool 就可以解決,不過 HttpClientFactory 得搭配 Microsoft.Extensions.DependencyInjection 才能使用,接下來,我將分享實作步驟

 

開發環境

  • Rider 2020.3
  • WinForm .NET 4.8
  • Microsoft.Extensions.DependencyInjection 5.0.1
  • Microsoft.Extensions.Http 5.0.0
  • Microsoft.Extensions.Http.Polly 5.0.1

使用 HttpClient

在 .NET Fx 4+

1. using(var client = new HttpClient()) 调用的 Dispose() 方法,不會立即釋放底層的 Socket,最後導致通訊端耗盡(sockets exhaustion),
通訊耗盡的實測,請參考 https://blog.yowko.com/httpclient-issue/

解法:
官方建議
HttpClient 的目的是要具現化一次,並在應用程式的整個生命週期中重複使用。 具現化每個要求的 HttpClient 類別,將會耗盡繁重負載下可用的通訊端數目。 這會導致 socketexception 錯誤。 以下是正確使用 HttpClient 的範例。

public class GoodController : ApiController
{
    private static readonly HttpClient HttpClient;

    static GoodController()
    {
        HttpClient = new HttpClient();
    }
}

出自:https://docs.microsoft.com/zh-tw/dotnet/api/system.net.http.httpclient

2.共用 HttpClient,可能會無法及時反應 DNS 的異動
無法及時反應 DNS 的異動實測,請參考 https://blog.yowko.com/httpclient-issue/

解法:定期更新 DNS 
//設定 10 分鐘沒有活動即關閉連線,預設 -1 (永不關閉)
ServicePointManager.FindServicePoint(baseUri).ConnectionLeaseTimeout = (int)TimeSpan.FromMinutes(10).TotalMilliseconds;

//設定 10 分鐘更新 DNS,預設 120000 (2 分鐘)
ServicePointManager.DnsRefreshTimeout = (int)TimeSpan.FromMinutes(10).TotalMilliseconds;

 

使用 HttpClientFactory

從 .NET 4.6.1/.NET Core 2.1 開始增加 HttpClientFactory

  • 實作連接池 (Connection pool),統一管理 HttpMessageHandler (HttpClientHandler 的基底類別) 的生命周期與連線,避免通訊 (Socket)耗盡。
  • 使用具名管理連接池,當名稱不存在時建立新的,存在則使用已存在的
  • HttpClient 命令發送前後需要執行的動作,可以設定委派處理常式 DelegatingHandlers,用法如下:
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

}

 

  • 在 Connection Pool,HttpClientHandler 預設的存活時間為 2 分鐘,通過 SetHandlerLifetime 修改時間,到期後不會被 Dispose,而是會被移到過期的 Pool,解決了 DNS 更新問題,範例如下
services.AddHttpClient("lab",
                       p => { p.BaseAddress = new Uri(BaseAddress); })
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

 

由於他並沒有公開實作物件給調用端 new 實例或是靜態方法建立實例,只能從 Microsoft.Extensions.DependencyInjection (DI Container),取得實例

本篇以 .NET Framework 4.8 為例,為了不佔篇幅,有關 MS DI Container 的用法可以參考以下:
https://www.dotblogs.com.tw/yc421206/2020/10/29/standard_di_container_for_microsoft_extensions_dependencyInjection

 

接下來就針對 IHttpClientFactory 來實作,主要有兩種注入

https://docs.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient?view=dotnet-plat-ext-5.0

開始之前先安裝套件,ASP.NET Core

Install-Package Microsoft.Extensions.Http

 

注入 IHttpClientFactory

先注入 IHttpClientFactory,再呼叫 IHttpClientFactory.CreateClient() 建立 HttpClient;注入時,可指定名稱。

//注入 HttpClientFactory
services.AddHttpClient("lab",
                       p => { p.BaseAddress = new Uri(BaseAddress); });
services.AddSingleton<LabService2>();
services.AddSingleton<Form2>();


LabService2 建構函數依賴 IHttpClientFactory

public class LabService2 : ILabService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public LabService2(IHttpClientFactory factory)
    {
        this._httpClientFactory = factory;
    }

    public IEnumerable<string> Get()
    {
        var url      = "api/default";
        var client   = this._httpClientFactory.CreateClient("lab");
        var response = client.GetAsync(url).GetAwaiter().GetResult();
        response.EnsureSuccessStatusCode();

        var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
        var result  = JsonConvert.DeserializeObject<string[]>(content);

        return result;
    }
}

 

Form2 建構函數依賴 LabService2

public partial class Form2 : Form
{
    private readonly ILabService _service;

    public Form2(LabService2 service)
    {
        this.InitializeComponent();

        this._service = service;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        var data = this._service.Get();
        MessageBox.Show(JsonConvert.SerializeObject(data));
    }
}

 

注入 HttpClient

當使用 IServiceCollection.AddHttpClient<T> 則是注入 HttpClient

//LabService 注入 HttpClient
services.AddHttpClient<LabService>(client =>
                                   {
                                       client.BaseAddress = new Uri(BaseAddress);
                                   });
services.AddSingleton<Form1>();

 

LabService 建構函數依賴 HttpClient

public class LabService : ILabService
{
    private readonly HttpClient _client;

    public LabService(HttpClient client)
    {
        this._client = client;
    }

    public IEnumerable<string> Get()
    {
        var url      = "api/default";
        var response = this._client.GetAsync(url).GetAwaiter().GetResult();
        response.EnsureSuccessStatusCode();

        var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
        var result  = JsonConvert.DeserializeObject<string[]>(content);

        return result;
    }
}

 

Form1 建構函數依賴 LabService

public partial class Form1 : Form
{
    private readonly ILabService _service;

    public Form1(LabService service)
    {
        this.InitializeComponent();

        this._service = service;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        var data = this._service.Get();
        MessageBox.Show(JsonConvert.SerializeObject(data));
    }
}

 

參考:https://docs.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient

 

Retry

微軟在 IHttpClientFactory 引用了 Polly,只要一點點設定就能夠建立 Retry 機制

安裝

Install-Package Microsoft.Extensions.Http.Polly

 

定義重試策略

services.AddHttpClient<LabService>(client => { client.BaseAddress = new Uri(BaseAddress); })
        //Polly Retory
        .AddPolicyHandler(HttpPolicyExtensions
                          //HandleTransientHttpError 包含 5xx 及 408 錯誤
                          .HandleTransientHttpError()
                          //404錯誤
                          .OrResult(p => p.StatusCode == HttpStatusCode.NotFound)
                          .WaitAndRetryAsync(6, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)))
                         );

 

參考

https://docs.microsoft.com/zh-tw/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly

https://docs.microsoft.com/zh-tw/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests

 

使用 SocketsHttpHandler

開始之前要安裝 Install-Package Microsoft.Extensions.Http

.NET Core 2.1 新增了一個  SocketsHttpHandler 用來管理 Connection Pool 的物件,跟以往的實作相比效能更好,消除平臺相依性,可簡化部署和服務。 

 

下段內文出自 https://docs.microsoft.com/zh-tw/dotnet/api/system.net.http.httpclienthandler?view=net-5.0#httpclienthandler-in-net-core

從 .NET Core 2.1 開始, HttpClientHandler 會根據類別所使用的跨平臺 HTTP 通訊協定堆疊,將類別的實作為變更 System.Net.Http.SocketsHttpHandler 

在 .NET Core 2.1 之前, HttpClientHandler 類別會在 Windows 上使用較舊的 HTTP 通訊協定堆疊 (WinHttpHandler ,並在 CurlHandler linux) 上以 linux 原生 libcurl 元件上的內部類別來執行。

下圖出自:https://www.stevejgordon.co.uk/httpclient-connection-pooling-in-dotnet-core

連接池

SocketsHttpHandler 為每個唯一端點 (Endpoint) 建立連接池,應用程程式通過 HttpClient 向端點發出 Http Request 時,當連接池不存在時連接物件時,將建立一個新的 HTTP 連接並將其用於該請求。該請求完成後,連接將保持打開狀態並返回到池中。對同一端點的後續的請求,將嘗試從池中找到可用的連接。

如何控制連接池

  • 應用程式起動時建立實例,應用程式消關閉才消滅,共用 SocketsHttpHandler 避免通訊耗盡
  • PooledConnectionLifetime:連接池中保持活動狀態的時間,預設無限。此生存期到期後,將不再為將來的請求而合併或發出連接。可根據 DNS 重新整理時間設定
  • PooledConnectionIdleTimeout閒置連接在不使用時在池中保留的時間,預設 2 分鐘。一旦生存期到期,空閒連接將被清除並從池中刪除。
  • MaxConnectionsPerServer每個端點將建立的最大出站連接數,預設 int.MaxValue,每個端點的連接分別池化。例如,如果最大連接數為 2,並且您的應用程序將請求同時發送到 www.github.com 和 www.google.com,則總共可能有多達 4 個連接。
var socketsHandler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(10),
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
    MaxConnectionsPerServer = 10
};

var client = new HttpClient(socketsHandler);

 

2024/01/11 補充:IHttpClientFactory + SocketsHttpHandler

builder.Services.AddHttpClient("lab",
        p => { p.BaseAddress = new Uri(serverUrl); })
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler()
    {
        UseCookies = false
    });

 

有關連線測試可參考: https://www.stevejgordon.co.uk/httpclient-connection-pooling-in-dotnet-core

參考:https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0
使用 HttpClientFactory 實作復原 HTTP 要求 - .NET | Microsoft Learn
Make HTTP requests using IHttpClientFactory in ASP.NET Core | Microsoft Learn

結論

.NET Core 2.1 之後一下子就提供了兩種 Connection Pool,該選用哪一種??

  • IHttpClientFactory 除了管理連接生存期之外,可以使用具名方法、DelegatingHandlers、Polly retry 配置 HttpClient;可以做的事似乎比 SocketsHttpHandler 多。
  • SocketsHttpHandler 可以將 HttpClient 封裝在物件內部,調用端不需要知道 HttpClient 的存在,不需要管理 HttpClient 的生命週期。
  • 但是 .NET Core 2.1,內建就使用 SocketsHttpHandler,是不是代表 IHttpClientFactory 未來的走向會被取代?或者是兩者並存?我不知道,這只能靜觀其變了。

 

範例位置

https://github.com/yaochangyu/sample.dotblog/tree/master/Http%20Client/Lab.HttpClientFactory

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo