在《利用IHttpClientFactory工厂来创建HttpClient》之后,我们将关注点放到HttpClient对象上。我们知道ASP.NET的核心就是由中间件组成的请求处理管道,HttpClient也采用了类似的设计。HttpClient管道由一组HttpMessageHandler对象构成,这些HttpMessageHandler相当于ASPNET的中间件。如下这些示例演示帮助我们更清楚地认识HttpMessageHandler处理管道。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)
[S1208]HttpClient的默认管道结构[S1208]HttpClient的默认管道结构(源代码)
[S1209]定制HttpClient管道(源代码)
[S1210]针对HTTP调用的日志输出(>=Information)(源代码)
[S1211]针对HTTP调用的日志输出(>=Trace)(源代码)
接下来我们通过如下的演示程序使用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:完成基于网络传输的请求发送和响应接收。
对于任何一个由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 定制处理器管道
对于由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)
由于在默认情况下只有等级不低于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)