我们知道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)

ASP.NET Core 6框架揭秘实例演示[18]:HttpClient处理管道的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  2. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  3. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  4. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  5. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

  6. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  7. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  8. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

  9. ASP.NET Core 6框架揭秘实例演示[15]:针对控制台的日志输出

    针对控制台的ILogger实现类型为ConsoleLogger,对应的ILoggerProvider实现类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包"M ...

随机推荐

  1. C++ 子函数参数传递过程

    编译环境:Visual Studio 2015 参数传递与汇编语言有很大关系.子函数传递参数主要方式有三种(这三种参数传递方式都可用用于x86汇编语言甚至其它汇编语言): 寄存器方式传递参数 存储器方 ...

  2. C++函数调用过程解析

    编译环境:Windows 10 + VS2015. 0.引言 函数调用的过程实际上也就是一个中断的过程,本文演示和深入分析参数入栈.函数跳转.保护现场.恢复现场等函数调用过程. 首先对三个常用的寄存器 ...

  3. 火爆的文字游戏你玩了吗?「GitHub 热点速览 v.22.06」

    不知道你有没有被 Wordle 这款游戏刷屏,在本期热点速览的特推部分选了一个 React 编写的开源版本同你分享,而本次公众号摘要也是一个提示, 只不过这个只能盲猜了.别小瞧 Wordle 这个游戏 ...

  4. C#Xml的三种创建方式(或者是两种?)和增删改查

    一.Xml的创建方式 Xmlwriter(流式读取,Stream) 写过了:https://www.cnblogs.com/dengzhekaihua/p/15438493.html 这种方法虽然快, ...

  5. C# 实例解释面向对象编程中的单一功能原则

    在面向对象编程中,SOLID 是五个设计原则的首字母缩写,旨在使软件设计更易于理解.灵活和可维护.这些原则是由美国软件工程师和讲师罗伯特·C·马丁(Robert Cecil Martin)提出的许多原 ...

  6. ubuntu vi 使用

    转载请注明来源:https://www.cnblogs.com/hookjc/ 文书编辑器介绍 [VI] vi ( m ) 在大多数的 unix 系统中 ( 包括 linux ) 都内建 vi ( m ...

  7. nodejs 文件上传服务端实现

    前段时间在做个人项目的时候,用到了nodejs服务端上传文件,现在回头把这个小结一下,作为记录. 本人上传文件时是基于express的multiparty,当然也可以使用connect-multipa ...

  8. Java中的equals和==的区别以及几个常用的object中的方法简单的调试方法

    一.equals 1.equals:是Object类中的方法,只能判断引用类型 2.默认判断的是地址是否相等(判断两个参数是否是同一个对象),子类中往往重写该方法,用于判断内容(值)是否相等 二.== ...

  9. 【HDU6647】Bracket Sequences on Tree(树Hash 树上Dp)

    题目链接 大意 给出一颗树,按下列方式生成一个括号序列. function dfs(int cur, int parent): print('(') for all nxt that cur is a ...

  10. 浅谈java代理模式

    讲解java代理模式 目录 讲解java代理模式 何谓代理模式 静态代理 动态代理 JDK动态代理 CGLIB动态代理 何谓代理模式 代理模式,即Proxy Pattern,23种java常用设计模式 ...