通过重建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这么一个委托,它相当于一个Func对象,它象体现了针对HttpContext所进行的某项操作,实际上体现某个中间件针对请求的处理。那为何我们不直接用一个RequestDelegate对象来表示一个中间件,而将它表示成一个Func对象呢?

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

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

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

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

从编程便利性考虑,很多预定义的中间件类型都具有对应的扩展方法进行注册,比如我们调用扩展方法UseStaticFiles来注册处理静态文件请求的中间件。对于我们演示的发布图片的应用来说,它也是通过调用一个具有如下定义的扩展方法UseImages来注册处理图片请求的中间件。这个UseImages方法的rootDirectory参数代表存放图片的目录,在这个方法中我们创建了一个Func对象,这个委托对象会根据当前请求的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 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>对象来存放所有注册的中间件,在Build方法中,我们调用它的Aggregate方法将它转换成一个RequestDelegate对象。

   1: public class ApplicationBuilder : IApplicationBuilder
   2: {
   3:     private IList> middlewares = new List>();  
   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 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对象,所以我们在模拟的管道中定义了如下一个DelegateStartup类型来作为这个IStarup接口的默认实现。

   1: public class DelegateStartup : IStartup
   2: {
   3:     private Action _configure;
   4:  
   5:     public DelegateStartup(Action 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 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();
   9:         _config = new ConfigurationBuilder()
  10:             .AddInMemoryCollection()
  11:             .Build();
  12:     }
  13:  
  14:     public IWebHost Build() => new WebHost(_services, _config);
  15:     public IWebHostBuilder ConfigureServices(Action 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对象中,并将这个委托对象作为参数调用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 configure)
   2: { 
   3:     return builder.ConfigureServices(services=>services.AddSingleton(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());
   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();
  15:         _serviceProvider.GetRequiredService().Configure(applicationBuilder);
  16:  
  17:         IServer server = _serviceProvider.GetRequiredService();
  18:         IServerAddressesFeature addressFeatures = server.Features.Get();
  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管道中的处理流程[下]:管道如何创建

源代码下载