在《利用IHttpClientFactory工廠來創建HttpClient》之后,我們將關注點放到HttpClient對象上。我們知道ASP.NET的核心就是由中間件組成的請求處理管道,HttpClient也采用了類似的設計。HttpClient管道由一組HttpMessageHandler對象構成,這些HttpMessageHandler相當於ASPNET的中間件。如下這些示例演示幫助我們更清楚地認識HttpMessageHandler處理管道。(本篇提供的實例已經匯總到《ASP.NET Core 6框架揭秘-實例演示版》)
[S1208]HttpClient的默認管道結構(源代碼)
[S1209]定制HttpClient管道(源代碼)
[S1210]針對HTTP調用的日志輸出(>=Information)(源代碼)
[S1211]針對HTTP調用的日志輸出(>=Trace)(源代碼)
[S1208]HttpClient的默認管道結構
接下來我們通過如下的演示程序使用IHttpClientFactory工廠創建了 一個HttpClient對象,並查看其管道依次由哪些類型的HttpMessageHandler對象組成。如代碼片段所示,我們定義了一個輔助方法PrintPipeline方法以遞歸的形式將指定HttpMessageHandler對象及其下一個處理器的類型輸出到控制台上。
using Microsoft.Extensions.DependencyInjection; using System.Reflection; var httpClient = new ServiceCollection() .AddHttpClient() .BuildServiceProvider() .GetRequiredService<IHttpClientFactory>() .CreateClient(); var handlerField = typeof(HttpMessageInvoker).GetField("_handler", BindingFlags.NonPublic | BindingFlags.Instance); PrintPipeline((HttpMessageHandler?)handlerField?.GetValue(httpClient), 0); static void PrintPipeline(HttpMessageHandler? handler, int index) { if (index == 0) { Console.WriteLine(handler?.GetType().Name); } else { Console.WriteLine($"{new string(' ', index * 4)}=>{handler?.GetType().Name}"); } if (handler is DelegatingHandler delegatingHandler) { PrintPipeline(delegatingHandler.InnerHandler, index + 1); } }
我們利用依賴注入容器提供的IHttpClientFactory工廠創建出HttpClient對象,並利用反射方式得到表示處理器的HttpMessageHandler對象,它實際上就是管道的第一個DelegatingHandler對象。我們將這個對象作為參數調用PrintPipeline方法將構成管道的每個處理器類型名稱打印出來,圖1為最終的輸出結果。
從圖1所示的輸出結果可以看出,對於采用默認配置構建的IHttpClientFactory工廠創建的HttpClient對象來說,它的處理器管道由如下四個類型的處理器構成:
- LifetimeTrackingHttpMessageHandler:在指定的生命周期內復用HttpMessageHandler對象的以提供更好的性能。
- LoggingScopeHttpMessageHandler:在整個調用的邊界(從開始調用到返回結果)輸出相應的跟蹤診斷日志(比如記錄整個調用耗時)。
- LoggingHttpMessageHandler:在網絡交互邊界(從請求發送到響應接收)輸出相應的跟蹤診斷日志(比如單純記錄網絡通信耗時)。
- HttpClientHandler:完成基於網絡傳輸的請求發送和響應接收。
[S1209]定制HttpClient管道
對於任何一個由IHttpClientFactory工廠創建的HttpClient對象來說,除了位於管道末端作為主處理器的HttpClientHandler可以替換之外,上述的其它三個處理器總是存在的。我們可以通過配置添加為構建的管道上添加任意處理器,它們最終會被添加到LoggingScopeHttpMessageHandler和LoggingHttpMessageHandler之間。我們編寫了一個簡單的實例來演示針對自定義處理器的注冊。如下面的代碼片段所示,我們定義了四個HttpMessageHandler類型,其中派生於HttpClientHandler的ExtendedHttpClientHandler將作為管道末端的主處理器,其他三個派生於DelegatingHandler的處理器將額外“注入”管道中。
public class ExtendedHttpClientHandler : HttpClientHandler { } public class FooHttpMessageHandler : DelegatingHandler { } public class BarHttpMessageHandler : DelegatingHandler { } public class BazHttpMessageHandler : DelegatingHandler { }
如下所示的演示程序在調用AddClient擴展方法得到返回的IHttpClientBuilder對象之后,調用了它的ConfigurePrimaryHttpMessageHandler擴展方法,並利用提供了一個Func<HttpMessageHandler>委托將ExtendedHttpClientHandler對象注冊為主處理器。我們接下來調用了這個IHttpClientBuilder對象的AddHttpMessageHandler擴展方法利用提供的Func<IServiceProvider, DelegatingHandler>委托添加了額外的三個處理器。
using App; using Microsoft.Extensions.DependencyInjection; using System.Reflection; var services = new ServiceCollection(); services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(_ => new ExtendedHttpClientHandler()) .AddHttpMessageHandler(_ => new FooHttpMessageHandler()) .AddHttpMessageHandler(_ => new BarHttpMessageHandler()) .AddHttpMessageHandler(_ => new BazHttpMessageHandler()); var httpClient = services.BuildServiceProvider() .GetRequiredService<IHttpClientFactory>() .CreateClient(); var handlerField = typeof(HttpMessageInvoker).GetField("_handler", BindingFlags.NonPublic | BindingFlags.Instance); PrintPipeline((HttpMessageHandler?)handlerField?.GetValue(httpClient), 0); static void PrintPipeline(HttpMessageHandler? handler, int index) { if (index == 0) { Console.WriteLine(handler?.GetType().Name); } else { Console.WriteLine($"{new string(' ', index * 4)}=>{handler?.GetType().Name}"); } if (handler is DelegatingHandler delegatingHandler) { PrintPipeline(delegatingHandler.InnerHandler, index + 1); } }
在利用IServiceProvider對象構建出IHttpClientFactory工廠之后,我們利用它將HttpClient對象創建出來,並采用與前一個實例相同的方式將它的處理器管道結構打印出來。組成管道的處理器順序體現在如圖2所示的輸出結果中。
[S1210]針對HTTP調用的日志輸出(>=Information)
對於由IHttpClientFactory工廠創建的HttpClient來說,它的處理器管道總是包含兩個與日志相關的處理器,對應的類型分別是LoggingScopeHttpMessageHandler和LoggingHttpMessageHandler,它們會在不同的邊界或范圍輸出相應的跟蹤診斷日志。前者的邊界是針對的是基於整個管道的調用,后者則是針對的是最后一個面向網絡傳輸。它們究竟會輸出怎樣的日志呢?我們不妨通過一個簡單的實例來尋找答案。如下面代碼片段所示,我們自定義了一個繼承自DelegatingHandler的DelayHttpMessageHanadler類型,它會在調用后續處理器前后模擬1秒和2秒的耗時。
public class DelayHttpMessageHanadler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); var response = await base.SendAsync(request, cancellationToken); await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); return response; } }
在調用AddHttpClient擴展方法對DelayHttpMessageHanadler進行注冊之前,我們還添加了針對日志的服務注冊。具體來說,我們添加了針對控制台的輸出,並開啟了針對日志范圍的支持。在利用IHttpClientFactory工廠將HttpClient對象創建出來后,我們用它向地址“http://www.baidu.com”發送了一個GET請求。
using App; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var services = new ServiceCollection().AddLogging(logging => logging .AddConsole() .AddSimpleConsole(options => options.IncludeScopes = true)); services.AddHttpClient(string.Empty).AddHttpMessageHandler(() => new DelayHttpMessageHanadler()); var httpClient = services .BuildServiceProvider() .GetRequiredService<IHttpClientFactory>() .CreateClient(); await httpClient.GetAsync("http://www.baidu.com");
程序運行之后,我們會在控制台上看到如圖3所示的四條日志。日志第一條和最后一條是LoggingScopeHttpMessageHandler輸出的,它創建了一個日志范圍,范圍名稱采用模板為“HTTP {Method} {URL}”,最后一條日志會輸出針對整個管道上的調用耗時。第2條和第3條日志是LoggingHttpMessageHandler對象輸出的,它們寫入的時機分別是發送請求前和接收到請求后,最后一條還是輸出兩者之間的時間間隔,也就是面向網絡傳輸的耗時。從輸出的內容可以看出,兩個耗時基本上相差三秒,剛好是我們注冊的DelayHttpMessageHanadler對象模擬延時。
[S1211]針對HTTP調用的日志輸出(>=Trace)
由於在默認情況下只有等級不低於Information的日志才會輸出到控制台上,所以看不到上述兩個輸出的更低等級(Trace)的日志。接下來我們對程序作如下的改動,通過添加日志過濾器輸出所有等級的日志。
using App; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var services = new ServiceCollection().AddLogging(logging => logging .SetMinimumLevel(LogLevel.Trace) .AddConsole() .AddSimpleConsole(options => options.IncludeScopes = true)); services.AddHttpClient(string.Empty).AddHttpMessageHandler(() => new DelayHttpMessageHanadler()); var httpClient = services .BuildServiceProvider() .GetRequiredService<IHttpClientFactory>() .CreateClient(); await httpClient.GetAsync("http://www.baidu.com");
再次運行我們的演示程序,控制台上將會輸出如圖4所示的日志。我們可以看出LoggingScopeHttpMessageHandler和LoggingHttpMessageHandler會將請求和響應的報頭寫入到等級為Trace的日志之中。