正確的使用 HttpClient (遠端 SSL 憑證無效問題)
為工作紀錄。解決遠端 SSL 憑證無效的錯誤問題。IHttpClientFactory, AddHttpClient
引言
.NET Core 的 HttpClient
的資源釋放做的不完美,在 Dispose 後還會有殘留,官方解法是另設計了一個 IHttpClientFactory
來統一管理。
總之,就是不要直接用 HttpClient 建構,改透過 IHttpClientFactory 間接建構 httpClient 物件。
HttpClient 的連線集區會連結至基礎 SocketsHttpHandler。 當您處置 HttpClient 執行個體時,其會處置集區內的所有現有連線。 如果您稍後將要求傳送至相同的伺服器,則必須重新建立新的連線。 因此,不必要的連線建立會產生效能損失。 此外,連線關閉之後,不會立即釋放 TCP 連接埠。 (如需詳細資訊,請參閱 RFC 9293 中的 TCP
TIME-WAIT
。)如果要求率很高,則可能會耗盡可用連接埠的作業系統限制。 若要避免連接埠耗盡問題,建議您盡可能重複使用多個 HTTP 要求的 HttpClient 執行個體。
參考文件
環境
IDE:Visual Studio 2022
平台:.NET6
相依套件
程式碼紀錄:法一
為每個存取目標網站,撰寫相應的存取服務。
using System;
var builder = WebApplication.CreateBuilder(args);
...略...
// 註冊 HttpClient 工廠使可注入:IHttpClientFactory 以建構較低秏的 HttpClient 資源池。
builder.Services.AddHttpClient("FooWebApiUrl", c =>
{
c.BaseAddress = new Uri(Configuration["FooWebApiUrl"]);
// headers...
//c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
//c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
}).ConfigurePrimaryHttpMessageHandler(() =>
{
//## 解決遠端 SSL 憑證無效的錯誤問題
//※ 注意:跳過憑證檢查,沒問題不要加這段碼。
var handler = new System.Net.Http.HttpClientHandler();
handler.ClientCertificateOptions = System.Net.Http.ClientCertificateOption.Manual;
handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true;
return handler;
});
...略...
var app = builder.Build();
...略...
app.Run();
using System.Net.Http;
class YourService : IDisposable
{
//# Injection Member
readonly HttpClient _http;
void IDisposable.Dispose()
{
_http?.Dispose();
}
public YourService(IHttpClientFactory httpFactory)
{
//※ 其中 IHttpClientFactory 介面來自 NuGet 套件:Microsoft.Extensions.Http。
_http= httpFactory.CreateClient("FooWebApiUrl");
}
void CallFooWebApi(string id)
{
...略...
using var ret = _http.PostAsync($"api/foo/{id}", null);
...略...
}
}
程式碼紀錄:法二
var builder = WebApplication.CreateBuilder(args);
...略...
// 註冊 HttpClient 工廠使可注入:IHttpClientFactory 以建構較低秏的 HttpClient 資源池。
builder.Services.AddHttpClient();
/// 註冊:以 HttpClient 為基底的客製服務
builder.Services.AddScoped<FooApiService>();
...略...
var app = builder.Build();
...略...
app.Run();
注入 IHttpClientFactory 建構 HttpClient 等相關資料實作高階的、商業邏輯等級的通訊指令。
using System.Net.Http;
using System.Net.Http.Json;
internal class FooApiService
{
//# Injection Member
readonly IHttpClientFactory _httpFactory;
//# State
public YourAccessToken AccessToken { get; private set; } = null;
public FooApiService(IHttpClientFactory httpFactory)
{
_httpFactory = httpFactory;
}
public async Task<FooBarResp> CallFooBarApiAsync()
{
try
{
if(this.AccessToken == null)
await GetTokenAsync();
using var _http = _httpFactory.CreateClient();
_http.DefaultRequestHeaders.Add("authorization", $"Bearer {this.AccessToken.bearer_token}");
_http.BaseAddress = new Uri($"https://target.webapi.url");
using var resp = await _http.GetAsync(@$"api/FooBar?$format=JSON");
var json = await resp.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<FooBarResp>(json);
return result;
}
catch(Exception ex)
{
throw new ApplicationException("出現例外!" + ex.Message, ex);
}
}
private async Task GetTokenAsync()
{
try
{
HttpContent formData = new FormUrlEncodedContent(
new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("client_id", "xxxxxx"),
new KeyValuePair<string, string>("client_secret", "xxxxxx"),
}
);
using var _http = _httpFactory.CreateClient();
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
_http.BaseAddress = new Uri($"https://target.webapi.url");
using var resp = await _http.PostAsync("auth/token", formData);
var token = await resp.Content.ReadFromJsonAsync<YourAccessToken>();
//# 存放 access token
this.AccessToken = token;
}
catch ( Exception ex )
{
throw new HttpRequestException("授權要求失敗!", ex, HttpStatusCode.Unauthorized);
}
}
}
Last updated