1 前言
Microsoft.Extensions.Http是一個設計非常優異的客戶端工廠庫,其提供了IHttpClientFactory
用於創建HttpClient
和IHttpMessageHandlerFactory
用於創建HttpMessageHandler
。
遺憾的是這個庫目前僅非常試用於客戶端,而不太適用於轉發端。我們對客戶端的定義是一個軟件在某種業務下使用單賬號請求遠程服務器的客戶端行爲,此軟件不再充當其它軟件的服務端;對轉發端的定義是一個軟件運行時,幫它的的多個客戶端請求遠程服務器,同時一般對遠程服務器的響應內容做一些包裝或修改的軟件。
有時哪怕是做客戶端軟件,當遇到下面需求時,HttpClient和Microsoft.Extensions.Http的者難以解決:
- 可以臨時申請很多代理服務器
- 每個代理服務器能使用3分鐘
- 使用這些代理服務器源源不斷的請求到某站
如果我們使用Microsoft.Extensions.Http,則無法使用動態的代理服務器;如果我們使用動態創建和維護多個HttpClient實例,我們又回到造第二個Microsoft.Extensions.Http的需求。
2 HttpMessageHandlerFactory
HttpMessageHandlerFactory就是上面要造第二Microsoft.Extensions.Http的需求
的產物,其它核心接口定義如下:
/// <summary>
/// Http消息處理者工廠
/// </summary>
public interface IHttpMessageHandlerFactory
{
/// <summary>
/// 創建用於請求的HttpMessageHandler
/// </summary>
/// <param name="name">別名</param>
/// <param name="proxyUri">支持攜帶UserInfo的代理地址</param>
/// <returns></returns>
HttpMessageHandler CreateHandler(string name, Uri? proxyUri);
}
當然HttpMessageHandlerFactory
也提供了和Microsoft.Extensions.Http
相似的Builder能力,在使用服務註冊時沒有額外的學習成本。
2.1 可選的ProxyUri參數
接口多了一個可選的Uri參數,有值的時候,代表要使用這個參數做代理。別小看這個Uri參數,它是可繼承的類型,傳入Uri的子類型還可以實現很多意想不到的騷操作。
2.2 支持創建HttpClient
IHttpMessageHandlerFactory
提供創建HttpClient
的擴展,用於做客戶端模式,且支持傳入與用戶實例綁定的CookieContainer
,然後Cookie就完全自動化處理。
/// <summary>
/// 創建Http客戶端
/// </summary>
/// <param name="factory"></param>
/// <param name="name">別名</param>
/// <param name="proxyUri">支持攜帶UserInfo的代理地址</param>
/// <param name="cookieContainer">cookie容器</param>
/// <returns></returns>
public static HttpClient CreateClient(this IHttpMessageHandlerFactory factory, string name, Uri? proxyUri = null, CookieContainer? cookieContainer = null)
{
var httpHandler = factory.CreateHandler(name, proxyUri, cookieContainer);
return new HttpClient(httpHandler, disposeHandler: false);
}
2.3 支持創建HttpMessageInvoker
IHttpMessageHandlerFactory
提供創建HttpMessageInvoker
的擴展,用於轉發端模式,且支持傳入與用戶實例綁定的CookieContainer,然後Cookie就完全自動化處理。
/// <summary>
/// 創建Http執行器
/// </summary>
/// <param name="factory"></param>
/// <param name="name">別名</param>
/// <param name="proxyUri">支持攜帶UserInfo的代理地址</param>
/// <param name="cookieContainer">cookie容器</param>
/// <returns></returns>
public static HttpMessageInvoker CreateInvoker(this IHttpMessageHandlerFactory factory, string name, Uri? proxyUri = null, CookieContainer? cookieContainer = null)
{
var httpHandler = factory.CreateHandler(name, proxyUri, cookieContainer);
return new HttpMessageInvoker(httpHandler, disposeHandler: false);
}
3 生態與擴展
如果說HttpMessageHandlerFactory
只解決了Microsoft.Extensions.Http
的Proxy痛點,但丟了Microsoft.Extensions.Http
的生態又不能擴展的話,那無疑HttpMessageHandlerFactory
是非常侷限和失敗的。
實際上Microsoft.Extensions.Http
上層的很多組件,移植到HttpMessageHandlerFactory
是非常簡單的,簡單說是DI註冊擴展的IHttpClientBuilder
改爲IHttpMessageHandlerBuilder
就行。
3.1 HttpMessageHandlerFactory.Polly
爲HttpMessageHandlerFactory提供Polly策略擴展,使得IHttpMessageHandlerBuilder
擁有與IHttpClientFactory
完全一致的Polly能力。
3.1.1 AddPolicyHandler能力
var retryPolicy = Policy.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(response =>
{
return response.IsSuccessStatusCode == false;
}).WaitAndRetryAsync(3, t => TimeSpan.FromSeconds(3d));
services
.AddHttpMessageHandlerFactory("App")
.AddPolicyHandler(retryPolicy);
3.1.2 AddPolicyHandlerFromRegistry能力
var retryPolicy = Policy.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(response =>
{
return response.IsSuccessStatusCode == false;
}).WaitAndRetryAsync(3, t => TimeSpan.FromSeconds(3d));
var registry = services.AddPolicyRegistry();
registry.Add("registry1", retryPolicy);
services
.AddHttpMessageHandlerFactory("App")
.AddPolicyHandlerFromRegistry("registry1");
3.1.3 AddTransientHttpErrorPolicy能力
當以下任意條件成立時,觸發TransientHttpErrorPolicy
- HttpRequestException的網絡故障
- 服務端響應5XX的狀態碼
- 408的狀態碼(request timeout)
services
.AddHttpMessageHandlerFactory("App")
.AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[] {
TimeSpan.FromSeconds(1d),
TimeSpan.FromSeconds(5d),
TimeSpan.FromSeconds(10d)
}));
3.2 HttpMessageHandlerFactory.Connection
爲HttpMessageHandlerFactory提供自定義連接的功能。
注意此擴展項目不是免費項目,有如下限制:
- 不開放和提供源代碼
- nuget包的程序集在應用程序運行2分鐘後適用期結束
- 適用期結束後所有的http請求響應爲423 Locked
- 需要license文件授權方可完全使用
3.2.1 自定義域名解析
- 當無代理連接時,連接到自定義解析得到的IP
- 當使用http代理時,讓代理服務器連接到自定義解析得到的IP
- 當使用socks代理時,讓代理服務器連接到自定義解析得到的IP
services
.AddHttpMessageHandlerFactory("App")
.AddHostResolver<CustomHostResolver>();
sealed class CustomHostResolver : HostResolver
{
public override ValueTask<HostPort> ResolveAsync(DnsEndPoint endpoint, CancellationToken cancellationToken)
{
if (endpoint.Host == "www.baidu.com")
{
return ValueTask.FromResult(new HostPort("14.119.104.189", endpoint.Port));
}
return ValueTask.FromResult(new HostPort(endpoint.Host, endpoint.Port));
}
}
3.2.2 自定義ssl的sni
Server Name Indication (SNI) 是 TLS 協議(以前稱爲 SSL 協議)的擴展,該協議在 HTTPS 中使用。它包含在 TLS/SSL 握手流程中,以確保客戶端設備能夠看到他們嘗試訪問的網站的正確 SSL 證書。該擴展使得可以在 TLS 握手期間指定網站的主機名或域名 ,而不是在握手之後打開 HTTP 連接時指定。
services
.AddHttpMessageHandlerFactory("App")
.AddSslSniProvider<CustomSslSniProvider>();
sealed class CustomSslSniProvider : SslSniProvider
{
public override ValueTask<string> GetSslSniAsync(string host, CancellationToken cancellationToken)
{
return ValueTask.FromResult(string.Empty);
}
public override bool RemoteCertificateValidationCallback(string host, X509Certificate? cert, X509Chain? chain, SslPolicyErrors errors)
{
return true;
}
}
4 兩個庫的場景選擇
庫 | 無/固定代理 | 動態代理 | 客戶端 | 轉發端 |
---|---|---|---|---|
Microsoft.Extensions.Http | 適合 | 不適合 | 非常適合 | 功能弱 |
HttpMessageHandlerFactory | 適合 | 適合 | 功能弱 | 非常適合 |
HttpMessageHandlerFactory的源代碼在https://github.com/xljiulang/HttpMessageHandlerFactory,其功能單一代碼量相對Microsoft.Extensions.Http要少一些,閱讀其代碼之後去理解Microsoft.Extensions.Http會更容易很多。