3

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

 2 years ago
source link: https://www.cnblogs.com/artech/p/inside-asp-net-core-6-18.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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为最终的输出结果。

图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所示的输出结果中。

图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对象模拟延时。

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

图4 诊断日志(All)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK