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 來實作,主要有兩種注入
開始之前先安裝套件,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));
}
}
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)))
);
參考
使用 SocketsHttpHandler
開始之前要安裝 Install-Package Microsoft.Extensions.Http
.NET Core 2.1 新增了一個 SocketsHttpHandler 用來管理 Connection Pool 的物件,跟以往的實作相比效能更好,消除平臺相依性,可簡化部署和服務。
從 .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