理解ASP.NET Core - 傳送Http請求(HttpClient)

xiaoxiaotank 發表於 2022-05-16
.Net

注:本文隸屬於《理解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄

前言

在.NET中,我們有很多傳送Http請求的手段,如HttpWebRequestWebClient以及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進行了一層封裝,簡化了常用任務場景的使用,如檔案上傳、檔案下載、資料上傳、資料下載等,並提供了一系列事件。

不過,雖然HttpWebRequestWebClient仍然可用,但官方建議,若沒有特殊要求,不要使用他倆,而應該使用HttpClient。那HttpClient是什麼呢?

HttpClient

namespace System.Net.Http
{
    public class HttpClient : HttpMessageInvoker { }
}

HttpClient位於System.Net.Http名稱空間下,它提供了GetAsyncPostAsyncPutAsyncDeleteAsyncPatchAsync等方法,更適合操作當下流行的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一直重用,幹嘛要銷燬呢?它的原理與各種池(如資料庫連線池、執行緒池)類似,就是為了保證套接字連線在空閒的時候能夠被及時關閉,而不是長時間保持開啟的狀態,白白佔用資源。

總結

現在,我們已經對HttpClientIHttpClientFactory有了一個清晰的認識,我們簡單總結一下:

  • HttpClient是當前.NET版本中傳送Http請求的首選
  • HttpClient提供了很多非同步Rest方法,非常適合當下的非同步程式設計模型
  • HttpClient旨在例項化一次,並在應用程式的整個生命週期內重複使用。
  • 直接建立HttpClient例項,很容易被錯誤使用,建議通過IHttpClientFactory來建立
  • HttpClient是對HttpMessageHandler的包裝,預設使用HttpMessageHandler的子類HttpClientHandler,而HttpClientHandler也只是對SocketsHttpHandler的簡單包裝(不討論WASM)
  • 通過IHttpClientFactory,我們可以方便地建立命名客戶端、型別化客戶端等
  • IHttpClientFactory通過建立多個HttpClient例項,但多個例項重用同一個HttpMessageHandler來優化HttpClient的建立