通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[下]:管道是如何构建起来的?

简介:

在《中篇》中,我们对管道的构成以及它对请求的处理流程进行了详细介绍,接下来我们需要了解的是这样一个管道是如何被构建起来的。总的来说,管道由一个服务器和一个HttpApplication构成,前者负责监听请求并将接收的请求传递给给HttpApplication对象处理,后者则将请求处理任务委托给注册的中间件来完成。中间件的注册是通过ApplicationBuilder对象来完成的,所以我们先来了解一下这究竟是个怎样的对象。[本文已经同步到《ASP.NET Core框架揭秘》之中] [源代码从这里下载]

目录
一、ApplicationBuilder——用于注册中间件并创建管道
二、Startup——利用ApplicationBuilder注册中间件
三、作为宿主的WebHost和它的构建者

一、ApplicationBuilder——用于注册中间件并创建管道

我们所说的ApplicationBuilder是对所有实现了IApplicationBuilder接口的所有类型及其对象的统称。用于创建WebHost的WebHostBuilder具有一个用于管道定值的Configure方法,它利用作为参数的ApplicationBuilder对象进行中间件的注册。由于ApplicationBuilder与组成管道的中间件具有直接的关系,所以我们得先来说说中间件在管道中究竟体现为一个怎样的对象。

中间件在请求处理流程中体现为一个类型为Func<RequestDelegate,RequestDelegate>的委托对象,对于很多刚刚接触请求处理管道的读者朋友们来说,可能一开始对此有点难以理解,所以容来略作解释。我们上面已经提到过RequestDelegate这么一个委托,它相当于一个Func<HttpContext, Task>对象,它象体现了针对HttpContext所进行的某项操作,实际上体现某个中间件针对请求的处理。那为何我们不直接用一个RequestDelegate对象来表示一个中间件,而将它表示成一个Func<RequestDelegate,RequestDelegate>对象呢?

在大部分应用中,我们会针对具体的请求处理需求注册多个不同的中间件,这些中间件按照注册时间的先后顺序进行排列进而构成管道。对于某个中间件来说,在它完成了自身的请求处理任务之后,需要将请求传递给下一个中间件作后续的处理。Func<RequestDelegate,RequestDelegate>中作为输入参数的RequestDelegate对象代表一个委托链,体现了后续中间件对请求的处理。一般来说,当某个中间件将自身实现的请求处理任务添加到这个委托链中,新的委托链将作为这个Func<RequestDelegate,RequestDelegate>对象的返回值。

以下图所示的管道为例,如果用一个Func<RequestDelegate,RequestDelegate>来表示中间件B,那么作为输入参数的RequestDelegate对象代表的是C对请求的处理操作,而返回值则代表B和C先后对请求处的处理操作。如果一个Func<RequestDelegate,RequestDelegate>代表第一个从服务器接收请求的中间件(比如A),那么执行该委托对象返回的RequestDelegate实际上体现了整个管道对请求的处理。

clip_image001

在对中间件有了充分的了解之后,我们来看看用于注册中间件的IApplicationBuilder接口的定义。如下所示的是经过裁剪后的IApplicationBuilder接口的定义,我们只保留了两个核心的方法,其中Use方法实现了针对中间件的注册,另一个Build方法则将所有注册的中间件转换成一个RequestDelegate对象。

   1: public interface IApplicationBuilder
   2: {
   3:     RequestDelegate Build();
   4:     IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
   5: }

从编程便利性考虑,很多预定义的中间件类型都具有对应的扩展方法进行注册,比如我们调用扩展方法UseStaticFiles来注册处理静态文件请求的中间件。对于我们演示的发布图片的应用来说,它也是通过调用一个具有如下定义的扩展方法UseImages来注册处理图片请求的中间件。这个UseImages方法的rootDirectory参数代表存放图片的目录,在这个方法中我们创建了一个Func<RequestDelegate, RequestDelegate>对象,这个委托对象会根据当前请求的URL和PathBase解析出目标图片的真实路径,并最终将文件内容写入到响应的输出流中。

   1: public static class Extensions
   2: {
   3:     private static Dictionary<string, string> mediaTypeMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
   4:  
   5:     static Extensions()
   6:     {
   7:         mediaTypeMappings.Add(".jpg", "image/jpeg");
   8:         mediaTypeMappings.Add(".gif", "image/gif");
   9:         mediaTypeMappings.Add(".png", "image/png");
  10:         mediaTypeMappings.Add(".bmp", "image/bmp");
  11:     }
  12:  
  13:     public static IApplicationBuilder UseImages(this IApplicationBuilder app, string rootDirectory)
  14:     {
  15:         Func<RequestDelegate, RequestDelegate> middleware = next =>
  16:         {
  17:             return async context =>
  18:             {
  19:                 string filePath = context.Request.Url.LocalPath.Substring(context.Request.PathBase.Length + 1);
  20:                 filePath = Path.Combine(rootDirectory, filePath).Replace('/', Path.DirectorySeparatorChar);
  21:                 filePath = File.Exists(filePath)
  22:                     ? filePath
  23:                     : Directory.GetFiles(Path.GetDirectoryName(filePath)).FirstOrDefault(it => string.Compare(Path.GetFileNameWithoutExtension(it), Path.GetFileName(filePath), true) == 0);
  24:  
  25:                 if (!string.IsNullOrEmpty(filePath))
  26:                 {
  27:                     string extension = Path.GetExtension(filePath);
  28:                     string mediaType;
  29:                     if (mediaTypeMappings.TryGetValue(extension, out mediaType))
  30:                     {
  31:                         await context.Response.WriteFileAsync(filePath, "image/jpg");
  32:                     }
  33:                 }
  34:                 await next(context);
  35:             };
  36:         };
  37:  
  38:         return app.Use(middleware);
  39:     }
  40:  
  41:     public static async Task WriteFileAsync(this HttpResponse response, string fileName, string contentType)
  42:     {
  43:         if (File.Exists(fileName))
  44:         {
  45:             byte[] content = File.ReadAllBytes(fileName);
  46:             response.ContentType = contentType;
  47:             await response.OutputStream.WriteAsync(content, 0, content.Length);
  48:         }
  49:         response.StatusCode = 404;
  50:     }
  51: }

针对图片文件内容的响应实现在另一个针对HttpResponse的扩展方法WriteFileAsync中。除了将图片文件的内容写入响应的输出流中,我们还需要针对图片的类型为响应设置对应的媒体类型(对应着HttpResponse的ContentType属性)。严格来说,媒体类型应该由读取的文件内容来确定,简单起见,我们指定的媒体类型是通过图片文件的扩展名推导出来的。

我们定义了一个ApplicationBuilder类型来作为IApplicationBuilder的默认实现者。如下面的代码片段所示,我们采用一个List<Func<RequestDelegate, RequestDelegate>>对象来存放所有注册的中间件,在Build方法中,我们调用它的Aggregate方法将它转换成一个RequestDelegate对象。

   1: public class ApplicationBuilder : IApplicationBuilder
   2: {
   3:     private IList<Func<RequestDelegate, RequestDelegate>> middlewares = new List<Func<RequestDelegate, RequestDelegate>>();  
   4:  
   5:     public RequestDelegate Build()
   6:     {
   7:         RequestDelegate seed = context => Task.Run(() => {});
   8:         return middlewares.Reverse().Aggregate(seed, (next, current) => current(next));
   9:     }    
  10:  
  11:     public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
  12:     {
  13:         middlewares.Add(middleware);
  14:         return this;
  15:     }
  16: }

二、Startup——利用ApplicationBuilder注册中间件

一个服务器和一组中间件组成了ASP .NET Core的HTTP请求处理管道,中间件的注册通过调用ApplicationBuilder的Use方法来完成。中间件的注册以及管道的构建是应用启动时所作的一项核心工作,ASP.NET Core为此专门定义了一个IStarup接口来从事启动时的初始化工作,我们将实现这个接口的类型以及对应对象统称为Startup。对于模拟管道的这个同名接口来说,我们对它进行了简化,只保留了如下一个唯一的Configure方法。由于这个Configure方法的主要目的在于为构建的管道注册相应的中间件,所以该方法具有的唯一参数是一个ApplicationBuilder对象。

   1: public interface IStartup
   2: {
   3:     void Configure(IApplicationBuilder app);
   4: }

定义在IStarup接口中的Configure方法以用于注册中间件的ApplicationBuilder对象作为输入,所以这个方法其实体现为一个Action<IApplicationBuilder>对象,所以我们在模拟的管道中定义了如下一个DelegateStartup类型来作为这个IStarup接口的默认实现。

   1: public class DelegateStartup : IStartup
   2: {
   3:     private Action<IApplicationBuilder> _configure;
   4:  
   5:     public DelegateStartup(Action<IApplicationBuilder> configure)
   6:     {
   7:         _configure = configure;
   8:     }
   9:  
  10:     public void Configure(IApplicationBuilder app)
  11:     {
  12:         configure(app);
  13:     }
  14: }

三、作为宿主的WebHost和它的构建者

ASP.NET Core管道是由作为应用宿主的WebHost对象创建出来的,后者是对所有实现了IWebHost接口的所有类型及其对象的统称。我们在模拟管道中将这个接口作了如下的简化,仅仅保留了用于启动当前WebHost的Start方法。随着WebHost因Start方法的调用而被开启,整个管道也随之被建立起来。

   1: public interface IWebHost
   2: {
   3:     void Start();
   4: }

我们总是利用一个WebHostBuilder对象来创建WebHost,WebHostBuilder是对所有实现了IWebHostBuilder接口的所有类型以及对应对象的通称。在模拟的管道中,我们为这个接口保留了如下三个方法,其中WebHost对象的创建实现在Build方法中。WebHost在启动的时候需要将整个管道构建出来,管道创建过程中所需的所有信息都来源于作为创建者的WebHostBuilder,后者采用“依赖注入”的形式来为创建的WebHost提供这些信息。换句话说,我们会将WebHost在管道构建过程中所需的对象以服务的形式注册到WebHostBuilder上面。

   1: public interface IWebHostBuilder
   2: {
   3:     IWebHost Build();
   4:     IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices);
   5:     IWebHostBuilder UseSetting(string key, string value);
   6: }

当我们调用Build方法创建对应WebHost的时候,WebHostBuilder会根据注册的这些服务创建一个ServiceProvider对象并提供给WebHost,后者正式利用这个ServiceProvider得到它所需要的服务对象。IWebHostBuilder接口通过定义的ConfigureServices方法帮助我们完成服务的注册工作。除了向创建的WebHost提供一个ServiceProvider之外,WebHostBuilder还需要将一些配置提供给WebHost,配置数据的设置可以通过调用UseSetting方法来完成。

如下所示的 WebHostBuilder类型是模拟管道针对IWebHostBuilder接口的默认实现。它具有_services和_config两个字段,前者用来存放通过ConfigureServices方法注册的服务,而后者则保存着通过UseSetting方法设置的配置。通过构造函数的定义可以看出,我们以Singleton模式对ApplicationBuilder类型进行了注册。至于配置,我们默认采用的配置源类型是内存变量。在Build方法中,我们利用这两个对象创建并返回了一个类型为WebHost的对象。

   1: public class WebHostBuilder : IWebHostBuilder
   2: {
   3:     private readonly IServiceCollection _services;
   4:     private readonly IConfiguration     _config;
   5:  
   6:     public WebHostBuilder()
   7:     {
   8:         _services = new ServiceCollection().AddSingleton<IApplicationBuilder, ApplicationBuilder>();
   9:         _config = new ConfigurationBuilder()
  10:             .AddInMemoryCollection()
  11:             .Build();
  12:     }
  13:  
  14:     public IWebHost Build() => new WebHost(_services, _config);
  15:     public IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices)
  16:     {
  17:         configureServices(_services);
  18:         return this;
  19:     }
  20:     public IWebHostBuilder UseSetting(string key, string value)
  21:     {
  22:         _config[key] = value;
  23:         return this;
  24:     }
  25: }

我们演示的实例通过一个自定义的中间件很好地完成了针对图片请求的处理,这个中间件的注册定义在IApplicationBuilder接口的扩展方法UseImages方法中,而针对着方法的调用在体现在下面这段代码中。如下面的代码片段所示,我们将针对UseImages方法的调用封装在一个Action<IApplicationBuilder>对象中,并将这个委托对象作为参数调用IWebHostBuilder的扩展方法Confiure。

   1: public static void Main()
   2: {
   3:     new WebHostBuilder()
   4:         .UseHttpListener()
   5:         .UseUrls("http://localhost:3721/images")
   6:         
   7:         .Build()
   8:         .Start();
   9:     Console.Read();
  10: }

IWebHostBuilder的Configure方法和注册的Startup类型的Configure方法具有相同的作用,那就是注册一个Startup服务来完成应用启动时必须完成的初始化操作,其核心操作就是为构建的管道注册对应的中间件。通过上面一节的介绍我们知道这个所谓的Startup服务对应着IStartup接口,所以Configure方法的目的就是针对这个接口注册对应的服务。如下面的代码片断所示,我们调用ConfigureServices方法注册的是一个DelegateStartup对象。

   1: public static IWebHostBuilder Configure(this IWebHostBuilder builder, Action<IApplicationBuilder> configure)
   2: { 
   3:     return builder.ConfigureServices(services=>services.AddSingleton<IStartup>(new DelegateStartup(configure)));
   4: }

WebHost在构建管道的时候必须知道采用何种类型的服务器,服务器采用怎样的监听地址。在我们演示的实例中,这两者的指定体现在我们为IWebHostBuilder定义的两个扩展方法中。如下面的代码片断所示,扩展方法UseHttpListener实际上就是调用了ConfigureServices方法将自定义的服务器类型HttpListenerServer以Singleton模式注册到WebHostBuilder上。通过扩展方法UseUrls设置的监听地址最终是通过调用UseSetting保存在配置上面。

   1: public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder)
   2: {
   3:     return builder.ConfigureServices(services => services.AddSingleton<IServer, HttpListenerServer>());
   4: }
   5:  
   6: public static IWebHostBuilder UseUrls(this IWebHostBuilder builder, params string[] urls)
   7: {
   8:     string addresses = string.Join(";", urls);
   9:     return builder.UseSetting("ServerAddresses", addresses);
  10: }

WebHost的Build方法最终创建的WebHost对象具有如下的定义。如下面的代码片段所示,WebHostBuilder在创建这个对象的时候需要提供包含所有注册服务的ServiceCollection对象和一个承载配置的Configuration对象,WebHost在初始化的时候会利用前者创建一个ServiceProvider对象。当我们调用它的Start方法的时候,WebHost利用这个ServiceProvider得到分别得到一个ApplicationBuilder对象和Startup,并将前者作为参数调用后者的Configure方法完成了所有中间件的注册工作。

   1: public class WebHost : IWebHost
   2: {
   3:     private readonly IServiceProvider     _serviceProvider;
   4:     private readonly IConfiguration     _config;
   5:  
   6:     public WebHost(IServiceCollection services, IConfiguration config)
   7:     {
   8:         _serviceProvider = services.BuildServiceProvider();
   9:         _config          = config;
  10:     }
  11:  
  12:     public void Start()
  13:     {
  14:         IApplicationBuilder applicationBuilder = _serviceProvider.GetRequiredService<IApplicationBuilder>();
  15:         _serviceProvider.GetRequiredService<IStartup>().Configure(applicationBuilder);
  16:  
  17:         IServer server = _serviceProvider.GetRequiredService<IServer>();
  18:         IServerAddressesFeature addressFeatures = server.Features.Get<IServerAddressesFeature>();
  19:  
  20:         string addresses = _config["ServerAddresses"] ?? "http://localhost:5000";
  21:         foreach (string address in addresses.Split(';'))
  22:         {
  23:             addressFeatures.Addresses.Add(address);
  24:         }
  25:  
  26:         server.Start(new HostingApplication(applicationBuilder.Build()));
  27:     }
  28: }

接下来,WebHost同样是利用这个ServiceProvider对象得到注册的服务器对象。在启动服务器之前,我们必须为它指定相应的监听地址。通过上面的介绍我们知道服务器总是利用它的一个ServerAddressesFeature特性对象来获取监听地址,所以我们先提取这个特性对象,并将配置承载的监听地址添加到这个ServerAddressesFeature对象上。如果我们没有显式指定监听地址,我们会使用默认的监听地址“http://localhost:5000”。在调用Start方法启动服务器的时候需要指定一个HttpApplication对象作为参数,后者代表由所示注册中间件构成的管道,它可以通过调用ApplicationBuilder的Build方法创建出来。


通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[上]:采用管道处理请求
通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[中]:管道如何处理请求
通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[下]:管道如何创建

源代码下载

作者:蒋金楠
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
3天前
|
设计模式 存储 前端开发
揭秘.NET架构设计模式:如何构建坚不可摧的系统?掌握这些,让你的项目无懈可击!
【8月更文挑战第28天】在软件开发中,设计模式是解决常见问题的经典方案,助力构建可维护、可扩展的系统。本文探讨了.NET中三种关键架构设计模式:MVC、依赖注入与仓储模式,并提供了示例代码。MVC通过模型、视图和控制器分离关注点;依赖注入则通过外部管理组件依赖提升复用性和可测性;仓储模式则统一数据访问接口,分离数据逻辑与业务逻辑。掌握这些模式有助于开发者优化系统架构,提升软件质量。
17 5
|
3天前
|
存储 缓存 安全
.NET 在金融行业的应用:高并发交易系统的构建与优化之路
【8月更文挑战第28天】在金融行业,交易系统需具备高并发处理、低延迟及高稳定性和安全性。利用.NET构建此类系统时,可采用异步编程提升并发能力,优化数据库访问以降低延迟,使用缓存减少数据库访问频率,借助分布式事务确保数据一致性,并加强安全性措施。通过综合优化,满足金融行业的严苛要求。
13 1
|
3天前
|
机器学习/深度学习 人工智能 算法
【悬念揭秘】ML.NET:那片未被探索的机器学习宝藏,如何让普通开发者一夜变身AI高手?——从零开始,揭秘构建智能应用的神秘旅程!
【8月更文挑战第28天】ML.NET 是微软推出的一款开源机器学习框架,专为希望在本地应用中嵌入智能功能的 .NET 开发者设计。无需深厚的数据科学背景,即可实现预测分析、推荐系统和图像识别等功能。它支持多种数据源,提供丰富的预处理工具和多样化的机器学习算法,简化了数据处理和模型训练流程。
16 1
|
3天前
|
大数据 开发工具 开发者
从零到英雄:.NET核心技术带你踏上编程之旅,构建首个应用,开启你的数字世界探险!
【8月更文挑战第28天】本文带领读者从零开始,使用强大的.NET平台搭建首个控制台应用。无论你是新手还是希望扩展技能的开发者,都能通过本文逐步掌握.NET的核心技术。从环境搭建到创建项目,再到编写和运行代码,详细步骤助你轻松上手。通过计算两数之和的小项目,你不仅能快速入门,还能为未来开发更复杂的应用奠定基础。希望本文为你的.NET学习之旅开启新篇章!
12 1
|
3天前
|
传感器 开发框架 物联网
揭开.NET在IoT领域的神秘面纱:如何构建智能设备,让未来生活触手可及?
【8月更文挑战第28天】随着物联网技术的发展,智能设备正深入我们的生活。.NET作为跨平台开源框架,在IoT领域应用广泛。本文介绍如何利用.NET构建智能设备,通过实例展示从环境搭建到项目创建、代码编写及运行的全过程,帮助开发者快速实现IoT解决方案,开启智能设备开发的新篇章。
11 0
|
3天前
|
开发框架 监控 .NET
开发者的革新利器:ASP.NET Core实战指南,构建未来Web应用的高效之道
【8月更文挑战第28天】本文探讨了如何利用ASP.NET Core构建高效、可扩展的Web应用。ASP.NET Core是一个开源、跨平台的框架,具有依赖注入、配置管理等特性。文章详细介绍了项目结构规划、依赖注入配置、中间件使用及性能优化方法,并讨论了安全性、可扩展性以及容器化的重要性。通过这些技术要点,开发者能够快速构建出符合现代Web应用需求的应用程序。
11 0
|
3天前
|
缓存 数据库连接 API
Entity Framework Core——.NET 领域的 ORM 利器,深度剖析其最佳实践之路
【8月更文挑战第28天】在软件开发领域,高效的数据访问与管理至关重要。Entity Framework Core(EF Core)作为一款强大的对象关系映射(ORM)工具,在 .NET 开发中扮演着重要角色。本文通过在线书店应用案例,展示了 EF Core 的核心特性和优势。我们定义了 `Book` 实体类及其属性,并通过 `BookStoreContext` 数据库上下文配置了数据库连接。EF Core 提供了简洁的 API,支持数据的查询、插入、更新和删除操作。
14 0
|
7天前
|
开发框架 监控 .NET
【Azure 应用程序见解】在Docker中运行的ASP.NET Core应用如何开启Application Insights的Profiler Trace呢?
【Azure 应用程序见解】在Docker中运行的ASP.NET Core应用如何开启Application Insights的Profiler Trace呢?
|
3月前
|
安全 网络协议 网络安全
IP代理的三大协议:HTTP、HTTPS与SOCKS5的区别
**HTTP代理**适用于基本网页浏览,简单但不安全;**HTTPS代理**提供加密,适合保护隐私;**SOCKS5代理**灵活强大,支持TCP/UDP及认证,适用于绕过限制。选择代理协议应考虑安全、效率及匿名需求。
下一篇
云函数