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

Jlion發表於2020-05-25

一、前言

前面分享了 .net core HttpClient 使用之掉坑解析(一),今天來分享自定義訊息處理HttpMessageHandlerPrimaryHttpMessageHandler 的使用場景和區別

二、原始碼閱讀

2.1 核心訊息管道模型圖

先貼上一張核心MessageHandler 管道模型的流程圖,圖如下:

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);
        });

上面程式碼中有兩個核心擴充套件方法,分別是ConfigurePrimaryHttpMessageHandlerAddHttpMessageHandler,這兩個方法大家可能會有疑問是做什麼的呢?
不錯,這兩個方法就是擴充套件註冊自定義的HttpMessageHandler 如果不註冊,會有預設的HttpMessageHandler,接下來我們分別來看下提供的擴充套件方法,如下圖:

圖中提供了一系列的AddHttpMessageHandler 擴充套件方法和ConfigurePrimaryHttpMessageHandler的擴充套件方法。

2.3 AddHttpMessageHandler

我們來看看HttpClientBuilderExtensions中的其中一個AddHttpMessageHandler擴充套件方法,程式碼如下:

        /// <summary>
        /// Adds a delegate that will be used to create an additional message handler for a named <see cref="HttpClient"/>.
        /// </summary>
        /// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
        /// <param name="configureHandler">A delegate that is used to create a <see cref="DelegatingHandler"/>.</param>
        /// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
        /// <remarks>
        /// The <see paramref="configureHandler"/> delegate should return a new instance of the message handler each time it
        /// is invoked.
        /// </remarks>
        public static IHttpClientBuilder AddHttpMessageHandler(this IHttpClientBuilder builder, Func<DelegatingHandler> configureHandler)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            if (configureHandler == null)
            {
                throw new ArgumentNullException(nameof(configureHandler));
            }

            builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
            {
                options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(configureHandler()));
            });

            return builder;
        }

程式碼中把自定義的DelegatingHandler 方法新增到HttpMessageHandlerBuilderActions中,我們再來看看HttpClientFactoryOptions物件原始碼,如下:

 /// <summary>
    /// An options class for configuring the default <see cref="IHttpClientFactory"/>.
    /// </summary>
    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);

        /// <summary>
        /// Gets a list of operations used to configure an <see cref="HttpMessageHandlerBuilder"/>.
        /// </summary>
        public IList<Action<HttpMessageHandlerBuilder>> HttpMessageHandlerBuilderActions { get; } = new List<Action<HttpMessageHandlerBuilder>>();

        /// <summary>
        /// Gets a list of operations used to configure an <see cref="HttpClient"/>.
        /// </summary>
        public IList<Action<HttpClient>> HttpClientActions { get; } = new List<Action<HttpClient>>();

        /// <summary>
        /// Gets or sets the length of time that a <see cref="HttpMessageHandler"/> 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 <see cref="Timeout.InfiniteTimeSpan"/> to disable handler expiry.
        /// </summary>
        /// <remarks>
        /// <para>
        /// The default implementation of <see cref="IHttpClientFactory"/> will pool the <see cref="HttpMessageHandler"/>
        /// 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.
        /// </para>
        /// <para>
        /// 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 <see cref="HandlerLifetime"/> should be
        /// chosen with an understanding of the application's requirement to respond to changes in the network environment.
        /// </para>
        /// <para>
        /// 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
        /// <see cref="HttpClient"/> instances will prevent the underlying <see cref="HttpMessageHandler"/> from being
        /// disposed until all references are garbage-collected.
        /// </para>
        /// </remarks>
        public TimeSpan HandlerLifetime
        {
            get => _handlerLifetime;
            set
            {
                if (value != Timeout.InfiniteTimeSpan && value < MinimumHandlerLifetime)
                {
                    throw new ArgumentException(Resources.HandlerLifetime_InvalidValue, nameof(value));
                }

                _handlerLifetime = value;
            }
        }

        /// <summary>
        /// The <see cref="Func{T, R}"/> which determines whether to redact the HTTP header value before logging.
        /// </summary>
        public Func<string, bool> ShouldRedactHeaderValue { get; set; } = (header) => false;

        /// <summary>
        /// <para>
        /// Gets or sets a value that determines whether the <see cref="IHttpClientFactory"/> will
        /// create a dependency injection scope when building an <see cref="HttpMessageHandler"/>.
        /// If <c>false</c> (default), a scope will be created, otherwise a scope will not be created.
        /// </para>
        /// <para>
        /// This option is provided for compatibility with existing applications. It is recommended
        /// to use the default setting for new applications.
        /// </para>
        /// </summary>
        /// <remarks>
        /// <para>
        /// The <see cref="IHttpClientFactory"/> will (by default) create a dependency injection scope
        /// each time it creates an <see cref="HttpMessageHandler"/>. The created scope has the same
        /// lifetime as the message handler, and will be disposed when the message handler is disposed.
        /// </para>
        /// <para>
        /// When operations that are part of <see cref="HttpMessageHandlerBuilderActions"/> are executed
        /// they will be provided with the scoped <see cref="IServiceProvider"/> via 
        /// <see cref="HttpMessageHandlerBuilder.Services"/>. This includes retrieving a message handler
        /// from dependency injection, such as one registered using 
        /// <see cref="HttpClientBuilderExtensions.AddHttpMessageHandler{THandler}(IHttpClientBuilder)"/>.
        /// </para>
        /// </remarks>
        public bool SuppressHandlerScope { get; set; }
    }

原始碼中有如下核心List:

 public IList<Action<HttpMessageHandlerBuilder>> HttpMessageHandlerBuilderActions { get; } = new List<Action<HttpMessageHandlerBuilder>>();

提供了HttpMessageHandlerBuilder HttpMessageHandler 的構造器列表物件,故,通過AddHttpMessageHandler可以新增一系列的訊息構造器方法物件
我們再來看看這個訊息構造器類,核心部分,程式碼如下:

public abstract class HttpMessageHandlerBuilder
    {
        /// <summary>
        /// Gets or sets the name of the <see cref="HttpClient"/> being created.
        /// </summary>
        /// <remarks>
        /// The <see cref="Name"/> is set by the <see cref="IHttpClientFactory"/> infrastructure
        /// and is public for unit testing purposes only. Setting the <see cref="Name"/> outside of
        /// testing scenarios may have unpredictable results.
        /// </remarks>
        public abstract string Name { get; set; }

        /// <summary>
        /// Gets or sets the primary <see cref="HttpMessageHandler"/>.
        /// </summary>
        public abstract HttpMessageHandler PrimaryHandler { get; set; }

        /// <summary>
        /// Gets a list of additional <see cref="DelegatingHandler"/> instances used to configure an
        /// <see cref="HttpClient"/> pipeline.
        /// </summary>
        public abstract IList<DelegatingHandler> AdditionalHandlers { get; }

        /// <summary>
        /// Gets an <see cref="IServiceProvider"/> which can be used to resolve services
        /// from the dependency injection container.
        /// </summary>
        /// <remarks>
        /// This property is sensitive to the value of 
        /// <see cref="HttpClientFactoryOptions.SuppressHandlerScope"/>. If <c>true</c> this
        /// property will be a reference to the application's root service provider. If <c>false</c>
        /// (default) this will be a reference to a scoped service provider that has the same
        /// lifetime as the handler being created.
        /// </remarks>
        public virtual IServiceProvider Services { get; }

        /// <summary>
        /// Creates an <see cref="HttpMessageHandler"/>.
        /// </summary>
        /// <returns>
        /// An <see cref="HttpMessageHandler"/> built from the <see cref="PrimaryHandler"/> and
        /// <see cref="AdditionalHandlers"/>.
        /// </returns>
        public abstract HttpMessageHandler Build();

        protected internal static HttpMessageHandler CreateHandlerPipeline(HttpMessageHandler primaryHandler, IEnumerable<DelegatingHandler> 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<DelegatingHandler> ?? 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構造器中有兩個核心屬性PrimaryHandlerAdditionalHandlers ,細心的同學可以發現AdditionalHandlers是一個IList<DelegatingHandler>列表,也就是說可以HttpClient 可以新增多個DelegatingHandler 即多個HttpMessageHandler 訊息處理Handler 但是隻能有一個PrimaryHandler Handler

同時HttpMessageHandlerBuilder提供了一個抽象的Build方法,還有一個CreateHandlerPipeline 方法,這個方法主要是把IList<DelegatingHandler>PrimaryHandler 構造成一個MessageHandler 連結串列結構(通過DelegatingHandlerInnerHandler屬性進行連線起來)

2.4 ConfigurePrimaryHttpMessageHandler

 public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this IHttpClientBuilder builder, Func<HttpMessageHandler> configureHandler)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            if (configureHandler == null)
            {
                throw new ArgumentNullException(nameof(configureHandler));
            }

            builder.Services.Configure<HttpClientFactoryOptions>(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<DelegatingHandler> AdditionalHandlers { get; } = new List<DelegatingHandler>();

        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
我們再來分別看看Log2HttpMessageHandlerLogHttpMessageHandlerPrimaryHttpMessageHandler 程式碼,程式碼很簡單就是SendAsync前後輸出了Log資訊,程式碼如下:
自定義的PrimaryHttpMessageHandler 程式碼如下:

public class PrimaryHttpMessageHandler: DelegatingHandler
    {
        private IServiceProvider _provider;

        public PrimaryHttpMessageHandler(IServiceProvider provider)
        {
            _provider = provider;
            InnerHandler = new HttpClientHandler();
        }

        protected async override Task<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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 程式碼已經完成,我們繼續新增呼叫程式碼,如下:

        /// <summary>
        /// 
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        public async Task<string> GetBaiduAsync(string url)
        {
            var client = _clientFactory.CreateClient("test");
            var result = await client.GetStringAsync(url);
            return result;
        }

現在我們執行訪問介面,執行後的控制檯Log 如下圖:

看到輸出結果,大家有沒有發現跟Asp.net core 中的中介軟體管道的執行圖一樣。

四、總結

HttpClientHttpMessageHandler可以自定義多個,但是隻能有一個PrimaryHttpMessageHandler如果新增多個只會被最後面新增的給覆蓋;新增的一系列Handler 構成一個鏈式管道模型,並且PrimaryHttpMessageHandler 主的訊息Handler 是在管道的最外層,也就是管道模型中的最後一道Handler。
使用場景:我們可以通過自定義的MessageHandler 來動態載入請求證書,通過資料庫的一些資訊,在自定義的Handler 中載入注入對應的證書,這樣可以起到動態載入支付證書作用,同時可以SendAsync 之前或者之後做一些自己的驗證等相關業務,大家只需要理解它們的用途,自然知道它的強大作用,今天就分享到這裡

相關文章