1. 程式人生 > >.net core HttpClient 使用之訊息管道解析(二)

.net core HttpClient 使用之訊息管道解析(二)

## 一、前言 前面分享了 [.net core HttpClient 使用之掉坑解析(一)](https://www.cnblogs.com/jlion/p/12813692.html),今天來分享自定義訊息處理`HttpMessageHandler`和`PrimaryHttpMessageHandler` 的使用場景和區別 ## 二、原始碼閱讀 ### 2.1 核心訊息管道模型圖 先貼上一張核心MessageHandler 管道模型的流程圖,圖如下: ![](https://img2020.cnblogs.com/blog/824291/202005/824291-20200523173914953-1939057591.png) HttpClient 中的`HttpMessageHandler` 負責主要核心的業務,`HttpMessageHandler` 是由MessageHandler 連結串列結構組成,形成一個訊息管道模式;具體我們一起來看看原始碼 ### 2.2 Demo程式碼演示 再閱讀原始碼的時候我們先來看下下面注入`HttpClient` 的Demo 程式碼,程式碼如下: ``` services.AddHttpClient("test") .ConfigurePrimaryHttpMessageHandler(provider => { return new PrimaryHttpMessageHandler(provider); }) .AddHttpMessageHandler(provider => { return new LogHttpMessageHandler(provider); }) .AddHttpMessageHandler(provider => { return new Log2HttpMessageHandler(provider); }); ``` 上面程式碼中有兩個核心擴充套件方法,分別是`ConfigurePrimaryHttpMessageHandler`和`AddHttpMessageHandler`,這兩個方法大家可能會有疑問是做什麼的呢? 不錯,這兩個方法就是擴充套件註冊自定義的`HttpMessageHandler` 如果不註冊,會有預設的`HttpMessageHandler`,接下來我們分別來看下提供的擴充套件方法,如下圖: ![](https://img2020.cnblogs.com/blog/824291/202005/824291-20200523175819887-1350104124.png) 圖中提供了一系列的`AddHttpMessageHandler` 擴充套件方法和`ConfigurePrimaryHttpMessageHandler`的擴充套件方法。 ### 2.3 `AddHttpMessageHandler` 我們來看看`HttpClientBuilderExtensions`中的其中一個`AddHttpMessageHandler`擴充套件方法,程式碼如下: ``` /// /// Adds a delegate that will be used to create an additional message handler for a named
. ///
/// The . /// A delegate that is used to create a . /// An that can be used to configure the client. /// /// The delegate should return a new instance of the message handler each time it /// is invoked. /// public static IHttpClientBuilder AddHttpMessageHandler(this IHttpClientBuilder builder, Func configureHandler) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } if (configureHandler == null) { throw new ArgumentNullException(nameof(configureHandler)); } builder.Services.Configure(builder.Name, options => { options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(configureHandler())); }); return builder; } ``` 程式碼中把自定義的`DelegatingHandler` 方法新增到`HttpMessageHandlerBuilderActions`中,我們再來看看`HttpClientFactoryOptions`物件原始碼,如下: ``` /// /// An options class for configuring the default
. ///
public class HttpClientFactoryOptions { // Establishing a minimum lifetime helps us avoid some possible destructive cases. // // IMPORTANT: This is used in a resource string. Update the resource if this changes. internal readonly static TimeSpan MinimumHandlerLifetime = TimeSpan.FromSeconds(1); private TimeSpan _handlerLifetime = TimeSpan.FromMinutes(2); /// /// Gets a list of operations used to configure an
. ///
public IList> HttpMessageHandlerBuilderActions { get; } = new List>(); /// /// Gets a list of operations used to configure an . /// public IList> HttpClientActions { get; } = new List>(); /// /// Gets or sets the length of time that a instance can be reused. Each named /// client can have its own configured handler lifetime value. The default value of this property is two minutes. /// Set the lifetime to to disable handler expiry. /// /// /// /// The default implementation of will pool the /// instances created by the factory to reduce resource consumption. This setting configures the amount of time /// a handler can be pooled before it is scheduled for removal from the pool and disposal. /// /// /// Pooling of handlers is desirable as each handler typically manages its own underlying HTTP connections; creating /// more handlers than necessary can result in connection delays. Some handlers also keep connections open indefinitely /// which can prevent the handler from reacting to DNS changes. The value of should be /// chosen with an understanding of the application's requirement to respond to changes in the network environment. /// /// /// Expiry of a handler will not immediately dispose the handler. An expired handler is placed in a separate pool /// which is processed at intervals to dispose handlers only when they become unreachable. Using long-lived /// instances will prevent the underlying from being /// disposed until all references are garbage-collected. /// /// public TimeSpan HandlerLifetime { get => _handlerLifetime; set { if (value != Timeout.InfiniteTimeSpan && value < MinimumHandlerLifetime) { throw new ArgumentException(Resources.HandlerLifetime_InvalidValue, nameof(value)); } _handlerLifetime = value; } } /// /// The which determines whether to redact the HTTP header value before logging. /// public Func ShouldRedactHeaderValue { get; set; } = (header) => false; /// /// /// Gets or sets a value that determines whether the will /// create a dependency injection scope when building an . /// If false (default), a scope will be created, otherwise a scope will not be created. /// /// /// This option is provided for compatibility with existing applications. It is recommended /// to use the default setting for new applications. /// /// /// /// /// The will (by default) create a dependency injection scope /// each time it creates an . The created scope has the same /// lifetime as the message handler, and will be disposed when the message handler is disposed. /// /// /// When operations that are part of are executed /// they will be provided with the scoped via /// . This includes retrieving a message handler /// from dependency injection, such as one registered using /// . /// /// public bool SuppressHandlerScope { get; set; } } ``` 原始碼中有如下核心List: ``` public IList> HttpMessageHandlerBuilderActions { get; } = new List>(); ``` 提供了`HttpMessageHandlerBuilder` HttpMessageHandler 的構造器列表物件,故,通過`AddHttpMessageHandler`可以新增一系列的訊息構造器方法物件 我們再來看看這個訊息構造器類,核心部分,程式碼如下: ``` public abstract class HttpMessageHandlerBuilder { /// /// Gets or sets the name of the being created. /// /// /// The is set by the infrastructure /// and is public for unit testing purposes only. Setting the outside of /// testing scenarios may have unpredictable results. /// public abstract string Name { get; set; } /// /// Gets or sets the primary . /// public abstract HttpMessageHandler PrimaryHandler { get; set; } /// /// Gets a list of additional instances used to configure an /// pipeline. /// public abstract IList AdditionalHandlers { get; } /// /// Gets an which can be used to resolve services /// from the dependency injection container. /// /// /// This property is sensitive to the value of /// . If true this /// property will be a reference to the application's root service provider. If false /// (default) this will be a reference to a scoped service provider that has the same /// lifetime as the handler being created. /// public virtual IServiceProvider Services { get; } /// /// Creates an . /// /// /// An built from the and /// . /// public abstract HttpMessageHandler Build(); protected internal static HttpMessageHandler CreateHandlerPipeline(HttpMessageHandler primaryHandler, IEnumerable additionalHandlers) { // This is similar to https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Net.Http.Formatting/HttpClientFactory.cs#L58 // but we don't want to take that package as a dependency. if (primaryHandler == null) { throw new ArgumentNullException(nameof(primaryHandler)); } if (additionalHandlers == null) { throw new ArgumentNullException(nameof(additionalHandlers)); } var additionalHandlersList = additionalHandlers as IReadOnlyList ?? additionalHandlers.ToArray(); var next = primaryHandler; for (var i = additionalHandlersList.Count - 1; i >= 0; i--) { var handler = additionalHandlersList[i]; if (handler == null) { var message = Resources.FormatHttpMessageHandlerBuilder_AdditionalHandlerIsNull(nameof(additionalHandlers)); throw new InvalidOperationException(message); } // Checking for this allows us to catch cases where someone has tried to re-use a handler. That really won't // work the way you want and it can be tricky for callers to figure out. if (handler.InnerHandler != null) { var message = Resources.FormatHttpMessageHandlerBuilder_AdditionHandlerIsInvalid( nameof(DelegatingHandler.InnerHandler), nameof(DelegatingHandler), nameof(HttpMessageHandlerBuilder), Environment.NewLine, handler); throw new InvalidOperationException(message); } handler.InnerHandler = next; next = handler; } return next; } } ``` `HttpMessageHandlerBuilder`構造器中有兩個核心屬性`PrimaryHandler` 和`AdditionalHandlers` ,細心的同學可以發現`AdditionalHandlers`是一個`IList`列表,也就是說可以HttpClient 可以新增多個`DelegatingHandler` 即多個`HttpMessageHandler` 訊息處理Handler 但是隻能有一個`PrimaryHandler` Handler 同時`HttpMessageHandlerBuilder`提供了一個抽象的`Build`方法,還有一個`CreateHandlerPipeline` 方法,這個方法主要是把`IList` 和`PrimaryHandler` 構造成一個MessageHandler 連結串列結構(通過`DelegatingHandler` 的`InnerHandler`屬性進行連線起來) ### 2.4 ConfigurePrimaryHttpMessageHandler ``` public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this IHttpClientBuilder builder, Func configureHandler) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } if (configureHandler == null) { throw new ArgumentNullException(nameof(configureHandler)); } builder.Services.Configure(builder.Name, options => { options.HttpMessageHandlerBuilderActions.Add(b => b.PrimaryHandler = configureHandler()); }); return builder; } ``` 通過上面的`HttpMessageHandlerBuilder` 原始碼分析`ConfigurePrimaryHttpMessageHandler` 方法主要是給Builder 中新增`PrimaryHandler`訊息Handler ### 2.5 DefaultHttpMessageHandlerBuilder 我們知道在`services.AddHttpClient()` 方法中會註冊預設的`DefaultHttpMessageHandlerBuilder` 訊息構造器方法,它繼承`DefaultHttpMessageHandlerBuilder`,那我們來看看它的原始碼 ``` internal class DefaultHttpMessageHandlerBuilder : HttpMessageHandlerBuilder { public DefaultHttpMessageHandlerBuilder(IServiceProvider services) { Services = services; } private string _name; public override string Name { get => _name; set { if (value == null) { throw new ArgumentNullException(nameof(value)); } _name = value; } } public override HttpMessageHandler PrimaryHandler { get; set; } = new HttpClientHandler(); public override IList AdditionalHandlers { get; } = new List(); public override IServiceProvider Services { get; } public override HttpMessageHandler Build() { if (PrimaryHandler == null) { var message = Resources.FormatHttpMessageHandlerBuilder_PrimaryHandlerIsNull(nameof(PrimaryHandler)); throw new InvalidOperationException(message); } return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers); } ``` 程式碼中`Build` 會去呼叫HttpMessageHandlerBuilder 的`CreateHandlerPipeline`方法把HttpMessageHandler 構建成一個類似於連結串列的結構。 到這裡原始碼已經分析完了,接下來我們來演示一個Demo,來證明上面的核心HttpMessageHandler 流程走向圖 ## 三、Demo演示證明 我們繼續來看上面我的Demo程式碼: ``` services.AddHttpClient("test") .ConfigurePrimaryHttpMessageHandler(provider => { return new PrimaryHttpMessageHandler(provider); }) .AddHttpMessageHandler(provider => { return new LogHttpMessageHandler(provider); }) .AddHttpMessageHandler(provider => { return new Log2HttpMessageHandler(provider); }); ``` 程式碼中自定義了兩個`HttpMessageHandler`和一個`PrimaryHttpMessageHandler` 我們再來分別看看`Log2HttpMessageHandler`、`LogHttpMessageHandler` 和`PrimaryHttpMessageHandler` 程式碼,程式碼很簡單就是`SendAsync`前後輸出了Log資訊,程式碼如下: 自定義的`PrimaryHttpMessageHandler` 程式碼如下: ``` public class PrimaryHttpMessageHandler: DelegatingHandler { private IServiceProvider _provider; public PrimaryHttpMessageHandler(IServiceProvider provider) { _provider = provider; InnerHandler = new HttpClientHandler(); } protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { System.Console.WriteLine("PrimaryHttpMessageHandler Start Log"); var response= await base.SendAsync(request, cancellationToken); System.Console.WriteLine("PrimaryHttpMessageHandler End Log"); return response; } } ``` `Log2HttpMessageHandler` 程式碼如下: ``` public class Log2HttpMessageHandler : DelegatingHandler { private IServiceProvider _provider; public Log2HttpMessageHandler(IServiceProvider provider) { _provider = provider; //InnerHandler = new HttpClientHandler(); } protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { System.Console.WriteLine("LogHttpMessageHandler2 Start Log"); var response=await base.SendAsync(request, cancellationToken); System.Console.WriteLine("LogHttpMessageHandler2 End Log"); return response; } } ``` `LogHttpMessageHandler`程式碼如下: ``` public class LogHttpMessageHandler : DelegatingHandler { private IServiceProvider _provider; public LogHttpMessageHandler(IServiceProvider provider) { _provider = provider; //InnerHandler = new HttpClientHandler(); } protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { System.Console.WriteLine("LogHttpMessageHandler Start Log"); var response=await base.SendAsync(request, cancellationToken); System.Console.WriteLine("LogHttpMessageHandler End Log"); return response; } } ``` 三個自定義Handler 程式碼已經完成,我們繼續新增呼叫程式碼,如下: ``` /// /// /// /// /// public async Task GetBaiduAsync(string url) { var client = _clientFactory.CreateClient("test"); var result = await client.GetStringAsync(url); return result; } ``` 現在我們執行訪問介面,執行後的控制檯Log 如下圖: ![](https://img2020.cnblogs.com/blog/824291/202005/824291-20200523183405089-1274193421.png) 看到輸出結果,大家有沒有發現跟Asp.net core 中的中介軟體管道的執行圖一樣。 ## 四、總結 `HttpClient`中`HttpMessageHandler`可以自定義多個,但是隻能有一個`PrimaryHttpMessageHandler`如果新增多個只會被最後面新增的給覆蓋;新增的一系列Handler 構成一個鏈式管道模型,並且`PrimaryHttpMessageHandler` 主的訊息Handler 是在管道的最外層,也就是管道模型中的最後一道Handler。 使用場景:我們可以通過自定義的MessageHandler 來動態載入請求證書,通過資料庫的一些資訊,在自定義的Handler 中載入注入對應的證書,這樣可以起到動態載入支付證書作用,同時可以SendAsync 之前或者之後做一些自己的驗證等相關業務,大家只需要理解它們的用途,自然知道它的強大作用,今天就分享