注:本文隸屬於《理解ASP.NET Core》系列文章,請查看置頂博客或點擊此處查看全文目錄
前言
在.NET中,我們有很多發送Http請求的手段,如HttpWebRequest
、WebClient
以及HttpClient
。
在進入正文之前,先簡單瞭解一下前2個:
HttpWebRequest
namespace System.Net
{
public class HttpWebRequest : WebRequest, ISerializable { }
}
HttpWebRequest
位於System.Net
命名空間下,繼承自抽象類WebRequest
,是.NET中最早、最原始地用於操作Http請求的類。相對來說,該類提供的方法更接近於底層,所以它的使用較爲繁瑣,對於開發者的水平要求是比較高的。
WebClient
namespace System.Net
{
public class WebClient : Component { }
}
同樣的,WebClient
也位於System.Net
命名空間下,它主要是對WebRequest
進行了一層封裝,簡化了常用任務場景的使用,如文件上傳、文件下載、數據上傳、數據下載等,並提供了一系列事件。
不過,雖然HttpWebRequest
和WebClient
仍然可用,但官方建議,若沒有特殊要求,不要使用他倆,而應該使用HttpClient
。那HttpClient
是什麼呢?
HttpClient
namespace System.Net.Http
{
public class HttpClient : HttpMessageInvoker { }
}
HttpClient
位於System.Net.Http
命名空間下,它提供了GetAsync
、PostAsync
、PutAsync
、DeleteAsync
、PatchAsync
等方法,更適合操作當下流行的Rest風格的Http Api。而且,它提供的方法幾乎都是異步的,非常適合當下的異步編程模型。
而且,HttpClient
旨在實例化一次,並在應用程序的整個生命週期內重複使用,也就是說,可以使用一個HttpClient
實例可以發送多次以及多個不同的請求。
不過需要注意的是,如果每次請求反而都實例化一個HttpClient
,由於Dispose
並不會立即釋放套接字,那麼當短時間內有大量請求時,就會導致服務器的套接字數被耗盡,從而引發SocketException
異常。
我們一起來看一個錯誤的示例:
public class ValuesController : ControllerBase
{
[HttpGet("WrongUsage")]
public async Task<string> WrongUsage()
{
try
{
// 模擬10次請求,每次請求都創建一個新的 HttpClient
var i = 0;
while (i++ < 10)
{
using var client = new HttpClient();
await client.GetAsync("https://jsonplaceholder.typicode.com/posts/1");
}
return "Success";
}
catch (Exception ex)
{
return ex.ToString();
}
}
}
jsonplaceholder.typicode.com 是一個免費提供虛假API的網站,我們可以使用它來方便測試。
在Windows中,當你請求WrongUsage接口之後,可以通過 netstat 命令查看套接字連接(jsonplaceholder的IP爲172.67.131.170:443),你會發現程序雖然已經退出了,但是連接並沒有像我們所預期的那樣立即關閉:
> netstat -n | find "172.67.131.170"
TCP 172.16.161.10:1057 172.67.131.170:443 TIME_WAIT
TCP 172.16.161.10:1058 172.67.131.170:443 TIME_WAIT
TCP 172.16.161.10:1061 172.67.131.170:443 TIME_WAIT
TCP 172.16.161.10:1065 172.67.131.170:443 TIME_WAIT
TCP 172.16.161.10:1070 172.67.131.170:443 TIME_WAIT
TCP 172.16.161.10:1073 172.67.131.170:443 TIME_WAIT
TCP 172.16.161.10:10005 172.67.131.170:443 TIME_WAIT
下面是一個較爲合理的示例:
public class ValuesController : ControllerBase
{
private static readonly HttpClient _httpClient;
static ValuesController()
{
// 複用同一個實例
_httpClient = new HttpClient();
}
}
可以看出,HttpClient
很容易被錯誤使用,並且,即使是上面的正確示例,仍然有很多待優化的地方。因此,爲了解決這個問題,IHttpClientFactory
誕生了。
IHttpClientFactory
看名字就知道了,IHttpClientFactory
可以幫我們創建所需要的HttpClient
實例,我們無須關心實例的創建過程。與HttpClient
一樣,位於System.Net.Http
命名空間下。
下面先了解一下它的一些用法。
基礎用法
首先,註冊HttpClientFactory相關的服務
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient();
然後,構造函數注入IHttpClientFactory
,通過CreateClient()
創建Client實例。
public class ValuesController : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory;
public ValuesController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
[HttpGet]
public async Task<string> Get()
{
// 通過 _httpClientFactory 創建 Client 實例
var client = _httpClientFactory.CreateClient();
var response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts/1");
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsStringAsync();
}
return $"{response.StatusCode}: {response.ReasonPhrase}";
}
}
輸出:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
命名客戶端
類似於命名選項,我們也可以添加命名的HttpClient,並添加一些全局默認配置。下面我們添加一個名爲jsonplaceholder
的客戶端:
// jsonplaceholder client
builder.Services.AddHttpClient("jsonplaceholder", (sp, client) =>
{
// 基址
client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
// 請求頭
client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
client.DefaultRequestHeaders.Add(HeaderNames.UserAgent, "HttpClientFactory-Sample-Named");
});
[HttpGet("named")]
public async Task<dynamic> GetNamed()
{
// 獲取指定名稱的 Client
var client = _httpClientFactory.CreateClient("jsonplaceholder");
var response = await client.GetAsync("posts/1");
if (response.IsSuccessStatusCode)
{
return new
{
Content = await response.Content.ReadAsStringAsync(),
AcceptHeader = response.RequestMessage!.Headers.Accept.ToString(),
UserAgentHeader = response.RequestMessage.Headers.UserAgent.ToString()
};
}
return $"{response.StatusCode}: {response.ReasonPhrase}";
}
輸出:
{
"content": "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"sunt aut facere repellat provident occaecati excepturi optio reprehenderit\",\n \"body\": \"quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto\"\n}",
"acceptHeader": "application/json",
"userAgentHeader": "HttpClientFactory-Sample-Named"
}
實際上,在創建HttpClient實例時,也可以指定未在服務中註冊的HttpClient名字。讀完文章後面,你就知道爲什麼了。
類型化客戶端
客戶端也可以被類型化,這樣做的好處有:
- 無需像命名客戶端那樣通過傳遞字符串獲取客戶端實例
- 可以將同一類別的調用接口進行歸類、封裝
- 有智能提示
下面看個簡單地例子,首先,創建一個類型化客戶端JsonPlaceholderClient
,用於封裝對jsonplaceholder接口的調用:
public class JsonPlaceholderClient
{
private readonly HttpClient _httpClient;
// 直接注入 HttpClient
public JsonPlaceholderClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<dynamic> GetPost(int id) =>
await _httpClient.GetFromJsonAsync<dynamic>($"/posts/{id}");
}
爲了讓DI容器知道要將哪個HttpClient實例注入到JsonPlaceholderClient
的構造函數,我們需要配置一下服務:
builder.Services.AddHttpClient<JsonPlaceholderClient>(client =>
{
client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
client.DefaultRequestHeaders.Add(HeaderNames.UserAgent, "HttpClientFactory-Sample-Typed");
});
最後,我們直接注入JsonPlaceholderClient
,而不再是IHttpClientFactory
,使用起來就好像在調用本地服務似的:
public class ValuesController : ControllerBase
{
private readonly JsonPlaceholderClient _jsonPlaceholderClient;
public ValuesController(JsonPlaceholderClient jsonPlaceholderClient)
{
_jsonPlaceholderClient = jsonPlaceholderClient;
}
[HttpGet("typed")]
public async Task<dynamic> GetTyped()
{
var post = await _jsonPlaceholderClient.GetPost(1);
return post;
}
}
藉助第三方庫生成的客戶端
一般來說,類型化的客戶端已經大大簡化了我們使用HttpClient的步驟和難度,不過,我們還可以藉助第三方庫再次簡化我們的代碼:我們只需要定義要調用的服務接口,第三方庫會生成代理類。
常用的第三方庫有以下兩個:
這兩個第三方庫的使用方式非常類似,由於我比較熟悉WebApiClientCore,所以後面的示例均使用它進行演示。
首先,安裝Nuget包:
Install-Package WebApiClientCore
接着,創建一個接口IJsonPlaceholderApi
:
[Header("User-Agent", "HttpClientFactory-Sample-Api")]
[Header("Custom-Header", "Custom-Value")]
public interface IJsonPlaceholderApi
{
[HttpGet("/posts/{id}")]
Task<dynamic> GetPost(int id);
}
怎麼樣,看起來是不是很像在寫Web Api?
對了,別忘了進行服務註冊:
builder.Services.AddHttpApi<IJsonPlaceholderApi>(
o =>
{
o.HttpHost = new Uri("https://jsonplaceholder.typicode.com/");
o.UseDefaultUserAgent = false;
});
最後,我們就可以更方便地用它了:
public class ValuesController : ControllerBase
{
private readonly IJsonPlaceholderApi _jsonPlaceholderApi;
public ValuesController(IJsonPlaceholderApi jsonPlaceholderApi)
{
_jsonPlaceholderApi = jsonPlaceholderApi;
}
[HttpGet("api")]
public async Task<dynamic> GetApi()
{
var post = await _jsonPlaceholderApi.GetPost(1);
return post;
}
}
HttpClient設計原理
上面我們提到過:HttpClient
旨在實例化一次,並在應用程序的整個生命週期內重複使用。如果每次請求都實例化一個HttpClient
,由於Dispose
並不會立即釋放套接字,那麼當短時間內有大量請求時,服務器的套接字數就會被耗盡,從而引發SocketException
異常。
爲了能夠真正理解這句話,我們一起看一下HttpClient的是如何發送請求並處理響應結果的。
下面,我們先看下HttpClient的基本結構:
按照慣例,爲了方便理解,後續列出的源碼中我已經刪除了一些不是那麼重要的代碼。
public class HttpMessageInvoker : IDisposable
{
private volatile bool _disposed;
private readonly bool _disposeHandler;
private readonly HttpMessageHandler _handler;
public HttpMessageInvoker(HttpMessageHandler handler) : this(handler, true) { }
public HttpMessageInvoker(HttpMessageHandler handler, bool disposeHandler)
{
_handler = handler;
_disposeHandler = disposeHandler;
}
[UnsupportedOSPlatformAttribute("browser")]
public virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) =>
_handler.Send(request, cancellationToken);
public virtual Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
_handler.SendAsync(request, cancellationToken);
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
_disposed = true;
if (_disposeHandler)
{
_handler.Dispose();
}
}
}
}
public class HttpClient : HttpMessageInvoker
{
private const HttpCompletionOption DefaultCompletionOption = HttpCompletionOption.ResponseContentRead;
private volatile bool _disposed;
private int _maxResponseContentBufferSize;
public HttpClient() : this(new HttpClientHandler()) { }
public HttpClient(HttpMessageHandler handler) : this(handler, true) { }
public HttpClient(HttpMessageHandler handler, bool disposeHandler) : base(handler, disposeHandler) =>
_maxResponseContentBufferSize = HttpContent.MaxBufferSize;
// 中間的Rest方法就略過了,因爲它們的內部都是通過調用 SendAsync 實現的
// 同步的 Send 方法與異步的 SendAsync 實現類似
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request) =>
SendAsync(request, DefaultCompletionOption, CancellationToken.None);
public override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
SendAsync(request, DefaultCompletionOption, cancellationToken);
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption) =>
SendAsync(request, completionOption, CancellationToken.None);
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cts.Token).ConfigureAwait(false);
ThrowForNullResponse(response);
if (ShouldBufferResponse(completionOption, request))
{
await response.Content.LoadIntoBufferAsync(_maxResponseContentBufferSize, cts.Token).ConfigureAwait(false);
}
return response;
}
private static void ThrowForNullResponse(HttpResponseMessage? response)
{
if (response is null) throw new InvalidOperationException(...);
}
private static bool ShouldBufferResponse(HttpCompletionOption completionOption, HttpRequestMessage request) =>
completionOption == HttpCompletionOption.ResponseContentRead
&& !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase);
protected override void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
_disposed = true;
// ...
}
base.Dispose(disposing);
}
}
看過之後,我們對HttpClient
的基本結構可以有一個清晰的認識:
HttpClient
繼承自HttpMessageInvoker
,“調用者”,很形象的一個名字。Send/SendAsync
方法是整個類的核心方法,所有的請求都是通過調用它們來實現的HttpClient
只是對HttpMessageHandler
的包裝,實際上,所有的請求都是通過這個Handler來發送的。
如果你足夠細心,你會發現其中的一個構造函數接收了一個名爲disposeHandler
的參數,用於指示是否要釋放HttpMessageHandler
。爲什麼要這麼設計呢?我們知道,HttpClient
旨在實例化一次,並在應用程序的整個生命週期內重複使用,實際上指的是HttpMessageHandler
,爲了在多個地方複用它,該參數允許我們創建多個HttpClient
實例,但使用的都是同一個HttpMessageHandler
實例(參見下方的IHttpClientFactory設計方式)。
下面看一下HttpMessageHandler
及其子類HttpClientHandler
:
public abstract class HttpMessageHandler : IDisposable
{
protected HttpMessageHandler() { }
// 這個方法是後加的,爲了不影響它的已存在的子類,所以將其設置爲了virtual(而不是abstract),並默認拋NSE
protected internal virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
throw new NotSupportedException(...);
}
protected internal abstract Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
protected virtual void Dispose(bool disposing)
{
// 基類中啥都沒幹
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
// 這裏我們不討論作爲WASM運行在瀏覽器中的情況
public class HttpClientHandler : HttpMessageHandler
{
// Socket
private readonly SocketsHttpHandler _underlyingHandler;
private volatile bool _disposed;
public HttpClientHandler()
{
_underlyingHandler = new HttpHandlerType();
ClientCertificateOptions = ClientCertificateOption.Manual;
}
private HttpMessageHandler Handler => _underlyingHandler;
// Send 與 SendAsync 類似
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
Handler.SendAsync(request, cancellationToken);
protected override void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
_disposed = true;
_underlyingHandler.Dispose();
}
base.Dispose(disposing);
}
}
實際上,在.NET Core 2.1(不包含)之前,HttpClient
默認使用的HttpMessageHandler
在各個平臺上的實現各不相同,直到.NET Core 2.1開始,HttpClient
才統一默認使用SocketsHttpHandler
,這帶來了很多好處:
- 更高的性能
- 消除了平臺依賴,簡化了部署和服務
- 在所有的.NET平臺上行爲一致
[UnsupportedOSPlatform("browser")]
public sealed class SocketsHttpHandler : HttpMessageHandler
{
private readonly HttpConnectionSettings _settings = new HttpConnectionSettings();
private HttpMessageHandlerStage? _handler;
private bool _disposed;
// Send 與 SendAsync 類似
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpMessageHandler handler = _handler ?? SetupHandlerChain();
return handler.SendAsync(request, cancellationToken);
}
private HttpMessageHandlerStage SetupHandlerChain()
{
HttpConnectionSettings settings = _settings.CloneAndNormalize();
HttpConnectionPoolManager poolManager = new HttpConnectionPoolManager(settings);
HttpMessageHandlerStage handler;
if (settings._credentials == null)
{
handler = new HttpConnectionHandler(poolManager);
}
else
{
handler = new HttpAuthenticatedConnectionHandler(poolManager);
}
// 省略了一些Handlers管道的組裝,與中間件管道類似
// 釋放舊的 _handler
if (Interlocked.CompareExchange(ref _handler, handler, null) != null)
{
handler.Dispose();
}
return _handler;
}
protected override void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
_disposed = true;
_handler?.Dispose();
}
base.Dispose(disposing);
}
}
// HttpAuthenticatedConnectionHandler 結構類似
internal sealed class HttpConnectionHandler : HttpMessageHandlerStage
{
// Http連接池管理器
private readonly HttpConnectionPoolManager _poolManager;
public HttpConnectionHandler(HttpConnectionPoolManager poolManager) =>
_poolManager = poolManager;
internal override ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) =>
_poolManager.SendAsync(request, async, doRequestAuth: false, cancellationToken);
protected override void Dispose(bool disposing)
{
if (disposing)
{
_poolManager.Dispose();
}
base.Dispose(disposing);
}
}
後面的就比較底層了,今天咱們就看到這裏吧。下面我們看一下IHttpClientFactory
。
IHttpClientFactory設計方式
我們先從服務註冊看起:
public static class HttpClientFactoryServiceCollectionExtensions
{
public static IServiceCollection AddHttpClient(this IServiceCollection services)
{
services.AddLogging();
services.AddOptions();
// 核心服務
services.TryAddTransient<HttpMessageHandlerBuilder, DefaultHttpMessageHandlerBuilder>();
services.TryAddSingleton<DefaultHttpClientFactory>();
services.TryAddSingleton<IHttpClientFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>());
services.TryAddSingleton<IHttpMessageHandlerFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>());
// 類型化客戶端服務
services.TryAdd(ServiceDescriptor.Transient(typeof(ITypedHttpClientFactory<>), typeof(DefaultTypedHttpClientFactory<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(DefaultTypedHttpClientFactory<>.Cache), typeof(DefaultTypedHttpClientFactory<>.Cache)));
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, LoggingHttpMessageHandlerBuilderFilter>());
services.TryAddSingleton(new HttpClientMappingRegistry());
// 默認註冊一個名字爲空字符串的 HttpClient 實例
services.TryAddTransient(s => s.GetRequiredService<IHttpClientFactory>().CreateClient(string.Empty));
return services;
}
public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, string name)
{
AddHttpClient(services);
// 返回一個Builder,以允許繼續針對HttpClient進行配置
return new DefaultHttpClientBuilder(services, name);
}
public static IHttpClientBuilder AddHttpClient<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TClient>(
this IServiceCollection services)
where TClient : class
{
AddHttpClient(services);
// 獲取類型名作爲客戶端名
string name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false);
var builder = new DefaultHttpClientBuilder(services, name);
// 目的是通過 ActivatorUtilities 動態創建 TClient 實例,並通過構造函數注入 HttpClient
builder.AddTypedClientCore<TClient>(validateSingleType: true);
return builder;
}
}
很顯然,HttpMessageHandlerBuilder
的作用就是創建HttpMessageHandler
實例,默認實現爲DefaultHttpMessageHandlerBuilder
IHttpMessageHandlerBuilderFilter
會在DefaultHttpClientFactory
中用到,它可以在HttpMessageHandlerBuilder.Build
調用之前對HttpMessageHandlerBuilder
進行一些初始化操作。
IHttpClientFactory
接口的默認實現是DefaultHttpClientFactory
:
internal class DefaultHttpClientFactory : IHttpClientFactory, IHttpMessageHandlerFactory
{
private readonly IServiceProvider _services;
private readonly Func<string, Lazy<ActiveHandlerTrackingEntry>> _entryFactory;
// 有效的Handler對象池,使用Lazy來保證每個命名客戶端具有唯一的 HttpMessageHandler 實例
internal readonly ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>> _activeHandlers;
// 過期的Handler集合
internal readonly ConcurrentQueue<ExpiredHandlerTrackingEntry> _expiredHandlers;
public DefaultHttpClientFactory(
IServiceProvider services,
IServiceScopeFactory scopeFactory,
ILoggerFactory loggerFactory,
IOptionsMonitor<HttpClientFactoryOptions> optionsMonitor,
IEnumerable<IHttpMessageHandlerBuilderFilter> filters)
{
_services = services;
_activeHandlers = new ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>>(StringComparer.Ordinal);
_entryFactory = (name) =>
{
return new Lazy<ActiveHandlerTrackingEntry>(() =>
{
return CreateHandlerEntry(name);
}, LazyThreadSafetyMode.ExecutionAndPublication);
};
}
public HttpClient CreateClient(string name)
{
HttpMessageHandler handler = CreateHandler(name);
return new HttpClient(handler, disposeHandler: false);
}
public HttpMessageHandler CreateHandler(string name)
{
// 若存在指定的命名客戶端的活躍的Handler,則直接使用,若不存在,則新建一個
ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
return entry.Handler;
}
internal ActiveHandlerTrackingEntry CreateHandlerEntry(string name)
{
HttpMessageHandlerBuilder builder = _services.GetRequiredService<HttpMessageHandlerBuilder>();
builder.Name = name;
var handler = new LifetimeTrackingHttpMessageHandler(builder.Build());
// options.HandlerLifetime 默認2分鐘
return new ActiveHandlerTrackingEntry(name, handler, scope, options.HandlerLifetime);
}
}
public static class HttpClientFactoryExtensions
{
public static HttpClient CreateClient(this IHttpClientFactory factory) =>
factory.CreateClient(Options.DefaultName); // 名字爲 string.Empty
}
可以發現,我們每次調用CreateClient
,都是新創建一個HttpClient實例,但是,當這些HttpClient實例同名時,所使用的HttpMessageHandler在一定條件下,其實都是同一個。
另外,你也可以發現,所有通過IHttpClientFactory創建的HttpClient,都是命名客戶端:
- 未指定名字的,則默認使用空字符串作爲客戶端的名字
- 類型客戶端使用類型名作爲客戶端的名字
Handler的創建是通過DefaultHttpMessageHandlerBuilder
調用Build
來實現的,不同的是,Factory並非是簡單地創建一個Handler,而是建立了一個Handler管道,這是通過抽象類DelegatingHandler
實現的。其中,管道最底層的Handler默認是HttpClientHandler
,與我們直接new HttpClient()
時所創建的Handler是一樣的。
與中間件管道類似,DelegatingHandler
的作用就是將Http請求的發送和處理委託給內部的另一個Handler處理,而它可以在這個Handler處理之前和之後加一些自己的特定邏輯。
public abstract class DelegatingHandler : HttpMessageHandler
{
private HttpMessageHandler? _innerHandler;
private volatile bool _disposed;
[DisallowNull]
public HttpMessageHandler? InnerHandler
{
get => _innerHandler;
set => _innerHandler = value;
}
protected DelegatingHandler() { }
// 這裏接收的innerHandler就是負責發送和處理Http請求的
protected DelegatingHandler(HttpMessageHandler innerHandler) =>
InnerHandler = innerHandler;
protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) =>
_innerHandler!.Send(request, cancellationToken);
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
_innerHandler!.SendAsync(request, cancellationToken);
protected override void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
_disposed = true;
if (_innerHandler != null)
{
_innerHandler.Dispose();
}
}
base.Dispose(disposing);
}
}
這裏我們看到的LifetimeTrackingHttpMessageHandler
,以及源碼中我刪除掉的LoggingHttpMessageHandler
都是DelegatingHandler
的子類。
你有沒有想過,爲啥最後要包裝成LifetimeTrackingHttpMessageHandler
呢?其實很簡單,它就是一個標識,標誌着它內部的Handler在超出生命週期後,需要被釋放。
另外,實際上,創建好的HttpMessageHandler
並非能夠一直重用,默認可重用的生命週期爲2分鐘,我們會將可重用的放在_activeHandlers
中,而過期的放在了_expiredHandlers
,並在合適的時候釋放銷燬。注意,過期不意味着要立即銷燬,只是不再重用,即不再分配給新的HttpClient實例了。
那爲什麼不讓創建好的HttpMessageHandler
一直重用,幹嘛要銷燬呢?它的原理與各種池(如數據庫連接池、線程池)類似,就是爲了保證套接字連接在空閒的時候能夠被及時關閉,而不是長時間保持打開的狀態,白白佔用資源。
總結
現在,我們已經對HttpClient
和IHttpClientFactory
有了一個清晰的認識,我們簡單總結一下:
HttpClient
是當前.NET版本中發送Http請求的首選HttpClient
提供了很多異步Rest方法,非常適合當下的異步編程模型HttpClient
旨在實例化一次,並在應用程序的整個生命週期內重複使用。- 直接創建
HttpClient
實例,很容易被錯誤使用,建議通過IHttpClientFactory
來創建 HttpClient
是對HttpMessageHandler
的包裝,默認使用HttpMessageHandler
的子類HttpClientHandler
,而HttpClientHandler
也只是對SocketsHttpHandler
的簡單包裝(不討論WASM)- 通過
IHttpClientFactory
,我們可以方便地創建命名客戶端、類型化客戶端等 IHttpClientFactory
通過創建多個HttpClient
實例,但多個實例重用同一個HttpMessageHandler
來優化HttpClient
的創建