在《中篇》中,我们对管道的构成以及它对请求的处理流程进行了详细介绍,接下来我们需要了解的是这样一个管道是如何被构建起来的。总的来说,管道由一个服务器和一个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管道中的处理流程[下]:管道如何创建

源代码下载

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

  1. ASP.NET Core Razor中处理Ajax请求

    如何ASP.NET Core Razor中处理Ajax请求 在ASP.NET Core Razor(以下简称Razor)刚出来的时候,看了一下官方的文档,一直没怎么用过.今天闲来无事,准备用Rozor ...

  2. 如何ASP.NET Core Razor中处理Ajax请求[转载]

    在ASP.NET Core Razor(以下简称Razor)刚出来的时候,看了一下官方的文档,一直没怎么用过. 今天闲来无事,准备用Rozor做个项目熟练下,结果写第一个页面就卡住了..折腾半天才搞好 ...

  3. 欢迎阅读daxnet的新博客:一个基于Microsoft Azure、ASP.NET Core和Docker的博客系统

    2008年11月,我在博客园开通了个人帐号,并在博客园发表了自己的第一篇博客.当然,我写博客也不是从2008年才开始的,在更早时候,也在CSDN和系统分析员协会(之后名为"希赛网" ...

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

    2008年11月,我在博客园开通了个人帐号,并在博客园发表了自己的第一篇博客.当然,我写博客也不是从2008年才开始的,在更早时候,也在CSDN和系统分析员协会(之后名为“希赛网”)个人空间发布过一些 ...

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

    欢迎阅读daxnet的新博客:一个基于Microsoft Azure.ASP.NET Core和Docker的博客系统   2008年11月,我在博客园开通了个人帐号,并在博客园发表了自己的第一篇博客 ...

  6. ASP.NET Core 运行原理解剖[2]:Hosting补充之配置介绍

    在上一章中,我们介绍了 ASP.NET Core 的启动过程,主要是对 WebHost 源码的探索.而本文则是对上文的一个补充,更加偏向于实战,详细的介绍一下我们在实际开发中需要对 Hosting 做 ...

  7. ASP.NET Core 防止跨站请求伪造(XSRF/CSRF)攻击 (转载)

    什么是反伪造攻击? 跨站点请求伪造(也称为XSRF或CSRF,发音为see-surf)是对Web托管应用程序的攻击,因为恶意网站可能会影响客户端浏览器和浏览器信任网站之间的交互.这种攻击是完全有可能的 ...

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

    ASP.NET Core应用中的路由机制实现在RouterMiddleware中间件中,它的目的在于通过路由解析为请求找到一个匹配的处理器,同时将请求携带的数据以路由参数的形式解析出来供后续请求处理流 ...

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

    我们现在大多数转向ASP.NET Core来使用开发的团队,应该都不是从0开始搭建系统,而是老的业务系统已经在运行,ASP.NET Core用来开发新模块.那么解决用户认证的问题,成为我们的第一个拦路 ...

随机推荐

  1. ThinkPHP3.2 volist嵌套循环显示原理

    php页面:$fatherList = $Document->where('pid=1')->select();        foreach($fatherList as $n=> ...

  2. 移植UE4的Spline与SplineMesh组件到Unity5

    一个月前,想开始看下UE4的源码,刚开始以为有Ogre1.9与Ogre2.1源码的基础 ,应该还容易理解,把源码下起后,发现我还是想的太简单了,UE4的代码量对比Ogre应该多了一个量级,毕竟Ogre ...

  3. Java 设计模式学习总结(上)

    在编写和维护公司的现有代码的时候,我经常思考的问题是:怎样的系统算一个好的系统?如何设计具有扩展.可维护.复用的系统,它能最大限度的应对产品经理不断变化的需求.答案似乎是:设计模式. Remember ...

  4. log4Net配置详解

    <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSe ...

  5. Android Scroller类的详细分析

    尊重原创作者,转载请注明出处: http://blog.csdn.net/gemmem/article/details/7321910 Scroller这个类理解起来有一定的困难,刚开始接触Scrol ...

  6. MYSQL触发器学习笔记

    课程学至金色晨曦科技公司技术总监沙利穆 触发器 1.       什么是触发器 触发器是一种特殊类型的存储过程,不由用户直接调用.创建触发器时会对其进行定义,以便在对特定表或列作特定类型的数据修改时执 ...

  7. java 生成条形码

    package com.sun.erwei; import java.awt.image.BufferedImage;import java.io.ByteArrayOutputStream;impo ...

  8. JVM平台上的响应式流(Reactive Streams)规范

    // Reactive Streams // 响应式流是一个倡议,用来为具有非阻塞后压的异步流处理提供一个标准.大家努力的目标集中在运行时环境(JVM和JavaScript)和网络协议上. 注:响应式 ...

  9. rt-thread之 for (fn_ptr = &amp;__rt_init_rti_board_start; fn_ptr &lt; &amp;__rt_init_rti_board_end; fn_ptr++) 理解

    @2019-01-30 [小记] 利用宏 INIT_EXPORT() --- __attribute__() 将函数重新放置在自定义段,执行时去该段获取函数地址 RT-Thread 的 INIT_BO ...

  10. loj#2483. 「CEOI2017」Building Bridges 斜率优化 cdq分治

    loj#2483. 「CEOI2017」Building Bridges 链接 https://loj.ac/problem/2483 思路 \[f[i]=f[j]+(h[i]-h[j])^2+(su ...