ASP.NET Core 6框架揭秘實例演示[18]:HttpClient處理管道


在《利用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為最終的輸出結果。

image
圖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所示的輸出結果中。

image
圖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對象模擬延時。

image
圖3 診斷日志(Level >=Information)

[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的日志之中。

image
圖4 診斷日志(All)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM