通过重建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实际上体现了整个管道对请求的处理。

在对中间件有了充分的了解之后,我们来看看用于注册中间件的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:         .Configure(app => app.UseImages(@"c:\images"))

   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管道中的处理流程[下]:管道如何创建

源代码下载

时间: 2024-10-13 00:00:23

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

一个基于Microsoft Azure、ASP.NET Core和Docker的博客系统

原文地址: http://www.cnblogs.com/daxnet/p/6139317.html 2008年11月,我在博客园开通了个人帐号,并在博客园发表了自己的第一篇博客.当然,我写博客也不是从2008年才开始的,在更早时候,也在CSDN和系统分析员协会(之后名为“希赛网”)个人空间发布过一些与编程和开发相关的文章.从入行到现在,我至始至终乐于与网友分享自己的所学所得,希望会有更多的同我一样的业内朋友能够在事业上取得成功,也算是为我们的软件事业贡献自己的一份力量吧,这也是我在博客园建博客

基于Microsoft Azure、ASP.NET Core和Docker的博客系统

欢迎阅读daxnet的新博客:一个基于Microsoft Azure.ASP.NET Core和Docker的博客系统 2008年11月,我在博客园开通了个人帐号,并在博客园发表了自己的第一篇博客.当然,我写博客也不是从2008年才开始的,在更早时候,也在CSDN和系统分析员协会(之后名为"希赛网")个人空间发布过一些与编程和开发相关的文章.从入行到现在,我至始至终乐于与网友分享自己的所学所得,希望会有更多的同我一样的业内朋友能够在事业上取得成功,也算是为我们的软件事业贡献自己的一份力

ASP.NET Core教程:ASP.NET Core 程序部署到Windows系统

一.创建项目 本篇文章介绍如何将一个ASP.NET Core Web程序部署到Windows系统上.这里以ASP.NET Core WebApi为例进行讲解.首先创建一个ASP.NET Core WebApi项目,使用默认的Values控制器,这里使用Visual Studio 2019创建一个ASP.NET Core 3.1d的WebApi项目. 创建新项目的时候选项ASP.NET Core Web应用程序,如下图所示: 配置新项目界面界面设置项目名称和位置,如下图所示: 选择.Net Cor

ASP.NET Core的路由[2]:路由系统的核心对象&mdash;&mdash;Router

ASP.NET Core应用中的路由机制实现在RouterMiddleware中间件中,它的目的在于通过路由解析为请求找到一个匹配的处理器,同时将请求携带的数据以路由参数的形式解析出来供后续请求处理流程使用.但是具体的路由解析功能其实并没有直接实现在RouterMiddleware中间件中,而是由一个Router对象来完成的.[本文已经同步到<ASP.NET Core框架揭秘>之中] 目录一.IRouter接口二.RouteContext三.RouteData四.Route五.RouteHan

ASP.NET Core集成现有系统认证

我们现在大多数转向ASP.NET Core来使用开发的团队,应该都不是从0开始搭建系统,而是老的业务系统已经在运行,ASP.NET Core用来开发新模块.那么解决用户认证的问题,成为我们的第一个拦路虎.本文将给大家简单阐述一下认证与授权的基本概念,以及基于ASP.NET Core 中间件实现的认证和改造JwtBearer 认证中间件来实现的认证达到与老系统(主要是token-based认证)的集成. 目录 认证与授权 什么是认证 何谓授权 用Middleware拦截 定制JWT Bearer 

linux系统编程之管道(一):匿名管道(pipe)

原文地址:http://www.cnblogs.com/mickole/p/3192210.html 一,什么是管道 管道是Linux支持的最初Unix IPC形式之一,具有以下特点: 管道是半双工的,数据只能向一个方向流动:需要双方通信时,需要建立起两个管道: 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程): 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中. 数据的读

ASP.NET Core应用针对静态文件请求的处理[2]: 条件请求与区间请求

通过调用ApplicationBuilder的扩展方法UseStaticFiles注册的StaticFileMiddleware中间件帮助我们处理针对文件的请求.对于StaticFileMiddleware处理请求的逻辑,大部分读者都应该想得到:它根据请求的地址找到目标文件的路径,然后利用注册的ContentTypeProvider根据路径解析出与文件内容相匹配的媒体类型,默认情况下得到的媒体类型是根据目标文件的扩展名解析出来的.解析出来的媒体类型将作为响应报头Content-Type的值.St

4.1ASP.NET Core请求过程「深入浅出ASP.NET Core系列」

希望给你3-5分钟的碎片化学习,可能是坐地铁.等公交,积少成多,水滴石穿,谢谢关注. HTTP请求过程 这里展示整体的HTTP请求的过程,这里化繁为简,保留了主干流程: 从浏览器输入域名开始,这里忽略了建立TCP的3次握手,向服务器发起HTTPRequest请求,服务器接受到之后,会触发服务器对网站的动态解析,然后把生成的网页信息通过HTTPResponse返回给用户,内部包含HTML的Body,Head等信息,最后就是浏览器对这些HTML信息进行内部引擎渲染的过程了. ASP.NET Core

Linux系统理解以及Linux系统学习心得

原创作品转载请注明出处  <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 作者:严哲璟 说一下我对Linux系统的理解 1.加载Linux内核准备:在加载基本输入输出模块(BIOS)之后,从磁盘的引导扇区读入操作系统的代码文件块到内存中,之后开始整个系统的初始化. 2.main.c的start_kernel函数是整个操作系统的入口,这也与Linux是基于C语言的特性相符,start_kernel具体做的动作很多