ASP.NET Core管道深度剖析(4):管道是如何建立起来的?
在《管道是如何处理HTTP请求的?》中,我们对ASP.NET Core的请求处理管道的构成以及它对请求的处理流程进行了详细介绍,接下来我们需要了解的是这样一个管道是如何被构建起来的。这样一个管道由一个服务器和一个HttpApplication构成,前者负责监听请求并将接收的请求传递给给HttpAppkication对象处理,后者则将请求处理任务委托给注册的中间件来完成。中间件的注册是通过ApplicationBuilder对象来完成的,所以我们先来了解一下这究竟是个怎样的对象。
目录
ApplicationBuilder
StartupLoader
WebHost
WeHostBuilder
总结
一、ApplicationBuilder
我们所说的ApplicationBuilder是对所有实现了IApplicationBuilder接口的所有类型及其对象的统称。注册到WebHostBuilder上的启动类型具有一个用于管道定值的Configure方法,它利用作为参数的ApplicationBuilder对象进行中间件的注册。由于ApplicationBuilder与组成管道的中间件具有直接的关系,所以我们得先来说说中间件在管道中究竟体现为一个怎样的对象。
中间件在请求处理流程中体现为一个类型为Func<RequestDelegate,RequestDelegate>的委托对象,对于很多刚刚接触请求处理管道的读者朋友们来说,可能一开始对此有点难以理解,所以容来略作解释。我们上面已经提到过RequestDelegate这么一个委托,它相当于一个Func<HttpContext, Task>对象,该委托对象表示针对提供的HttpContext所做进行一项处理操作,这项操作代表某个中间件针对请求的处理。那为何我们不直接用一个RequestDelegate对象来表示一个中间件,而将它表示成一个Func<RequestDelegate,RequestDelegate>对象呢?
在大部分应用中,我们会针对具体的请求处理需求注册多个不同的中间件,这些中间件按照注册时间的先后顺序进行排列进而构成我们所谓的请求处理管道。对于某个中间件来说,在它完成了自身的请求处理任务之后,需要将请求传递给下一个中间件作后续的处理。Func<RequestDelegate,RequestDelegate>中作为输入参数的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来注册处理图片请求的中间件。
1: public static class ApplicationBuilderExtensions
2: {
3: public static IApplicationBuilder UseImages(this IApplicationBuilder app, string directory)
4: {
5: Func<RequestDelegate, RequestDelegate> middleware = next =>
6: {
7: return context =>
8: {
9: string fileName = context.Request.Url.LocalPath.TrimStart('/');
10: if (string.IsNullOrEmpty(Path.GetExtension(fileName)))
11: {
12: fileName += ".jpg";
13: }
14: fileName = Path.Combine(directory, fileName);
15: context.Response.WriteFile(fileName, "image/jpg");
16: return next(context);
17: };
18: };
19: return app.Use(middleware);
20: }
21: }
ASP.NET Core默认使用的是一个类型为ApplicationBuilder的对象来注册中间件,我们采用如下的代码片断来模拟它的实现逻辑。我们采用一个List<Func<RequestDelegate, RequestDelegate>>对象来存放所有注册的中间件,并调用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: }
ASP.NET Core并不会直接创建ApplicationBuilder对象来注册中间件,而是利用对应的工厂来创建它。创建爱你ApplicationBuilder的工厂通过接口IApplicationBuilderFactory表示,在模拟的管道中我们将这个接口简化成如下的形式,该接口的默认实现者ApplicationBuilderFactory会直接创建一个ApplicationBuilder类型的对象。
1: public interface IApplicationBuilderFactory
2: {
3: IApplicationBuilder CreateBuilder();
4: }
5:
6: public class ApplicationBuilderFactory : IApplicationBuilderFactory
7: {
8: public IApplicationBuilder CreateBuilder()
9: {
10: return new ApplicationBuilder();
11: }
12: }
二、StartupLoader
一个服务器和一组中间件组成了ASP .NET Core的HTTP请求处理管道,中间件的注册通过调用ApplicationBuilder的Use方法来完成,而这一切实现在注册为启动类型的Configure方法中,我们可以将针对这个方法的调用抽象成一个类型为Action <IApplicationBuilder> 的委托对象。在管道初始化过程中,WebHost必须获取并执行这个委托以完成中间件的注册工作。具体来说这个委托对象的获取是利用一个名为StatupLoader对象来完成的。
这里的StartupLoader是对所有实现了IStartupLoader接口的所有类型机器对象的统称,我们在模拟管道中将这个接口作了如下所示的简化。IStartupLoader接口具有的唯一方法GetConfigureDelegate根据指定的启动类型生成一个Action <IApplicationBuilder> 。对于默认实现该接口的StartupLoader类来说,它的GetConfigureDelegate方法返回的委托会以反射的方式执行定义在指定启动类型的Configure方法。简单起见,我们假设这个Configure方法为实例方法,启动对象可以直接调用默认无参构造函数来创建。
1: public interface IStartupLoader
2: {
3: Action<IApplicationBuilder> GetConfigureDelegate(Type startupType);
4: }
5:
6: public class StartupLoader : IStartupLoader
7: {
8: public Action<IApplicationBuilder> GetConfigureDelegate(Type startupType)
9: => app => startupType.GetMethod("Configure").Invoke(Activator.CreateInstance(startupType), new object[] { app });
10: }
三、WebHost
ASP.NET Core的请求处理管道是由作为应用宿主的WebHost对象创建出来的,后者是对所有实现了IWebHost接口的所有类型及其对象的统称,我们在模拟管道中将这个接口作了如下的简化,仅仅保留了唯一的方法Start。随着WebHost因Start方法的调用而被开启,整个管道也随之被建立起来。
1: public interface IWebHost
2: {
3: void Start();
4: }
通过上面的介绍我们知道请求处理管道可以理解为一个服务器和一个HttpApplication的组合,当我们创建出一个服务器并指定一个具体的HttpApplication对象调用其Start方法将其启动时,这个管道就被建立起来。服务器的创建是利用ServerFactory来完成的,而默认采用的HttpApplication类型为HostingApplication。
当我们创建一个HostingApplication对象的时候,需要指定一个类型为RequestDelegate的委托对象,后者通过调用ApplicationBuilder的Build方法获得,代表了所有注册的中间件针对当前请求的处理。所以HostingApplication的创建需要一个ApplicationBuilder对象,这个对象通过ApplicationBuilderFactory来创建。在调用ApplicationBuilder的Build方法将注册的中间件转换成RequestDelegate委托之前,需要完成针对中间件的注册工作。实现在启动类型的Configure方法中针对中间件的注册可以体现为一个Action <IApplicationBuilder>对象,这对委托对象可以通过StartupLoader来获取。
综上所述,为了创建并启动一个服务器,WebHost至少需要一个ServerFactory和ApplicationBuilderFactory来创建服务器和ApplicationBuilder,还需要一个StartupLoader来最终完成对中间件的注册。除此之外,还需要知道注册到WebHostBuilder上的启动类型。由于依赖注入被广泛应用到了ASP.NET Core的请求处理管道中,对于前面三个对象,会先以服务的形式注册到DI容器中,那么WebHost只需要利用ServiceProvider对象根据对应的服务接口得到这三个对象。
1: public class WebHost : IWebHost
2: {
3: private IServiceProvider serviceProvider;
4: private Type startupType;
5:
6: public WebHost(IServiceCollection appServices, Type startupType)
7: {
8: this.serviceProvider = appServices.BuildServiceProvider();
9: this.startupType = startupType;
10: }
11:
12: public void Start()
13: {
14: IApplicationBuilder applicationBuilder = serviceProvider.GetRequiredService<IApplicationBuilderFactory>().CreateBuilder();
15: serviceProvider.GetRequiredService<IStartupLoader>().GetConfigureDelegate(startupType)(applicationBuilder);
16: IServer server = serviceProvider.GetRequiredService<IServerFactory>().CreateServer();
17: server.Start(new HostingApplication(applicationBuilder.Build()));
18: }
19: }
由上面代码片段提供的这个极简版的WebHost类通过构造函数的参数提供包含原始服务注册的ServiceCollection对象和启动类型,我们利用前者创建对应的ServiceProvider。在Start方法中,我们利用ServiceProvider得到一个ApplicationBuilder对象和一个StartupLoader对象。我们将启动类型作为参数调用StartupLoader的GetConfigureDelegate方法得到一个Action<IApplicationBuilder>对象。接下来,我们将ApplicationBuilder对象作为参数调用这个Action<IApplicationBuilder>委托对象,后者会执行定义在启动类型中的Configure方法并最终完整对中间件的注册。
在这之后,我们利用ServiceProvider得到一个ServiceFactory对象并利用它创建出代码服务器的Server对象。为了调用其Start方法,我们需要创建一个HostingApplication对象作为参数,而后者的创建需要一个代表所有中间件针对当前请求进行处理的RequestDelegate对象,这个对象直接通过调用ApplicationBuilder对象的Build方法得到。当服务器因Start方法的调用而被启动后,整个请求处理管道被正式建立起来。
四、WebHostBuilder
作为应用宿主的WebHost创建了ASP.NET Core的请求处理管道,而WebHost又是由它的工厂WebHostBuilder创建的。WebHostBuilder是对所有实现了IWebHostBuilder接口的所有类型及其对象的统称,我们在模拟管道中对这个接口做了极大的简化,仅仅保留了如下面代码片段所示的三个方法成员。针对WebHost的创建通过Build方法实现,额外两个方法(UseStartup和UseServer)分别用于注册启动类型和用于创建服务器的ServerFactory。
1: public interface IWebHostBuilder
2: {
3: IWebHostBuilder UseStartup(Type startupType);
4: IWebHostBuilder UseServer(IServerFactory factory);
5: IWebHost Build();
6: }
依赖注入在ASP.NET Core 请求处理管道中得到了极大的应用,创建WebHost提供的ServiceCollection对象最初由WebHostBuilder提供。WebHost在构建管道时使用的一系列服务对象(ApplicationBuilderFactory和StartupLoader)最初都由WebHostBuilder注册到这个ServiceCollection对象中,这一切都体现如下所示的这个默认使用的WebHostBuilder类型中。
1: public class WebHostBuilder : IWebHostBuilder
2: {
3: private Type startupType;
4: private IServiceCollection services;
5:
6: public WebHostBuilder()
7: {
8: services = new ServiceCollection()
9: .AddTransient<IStartupLoader, StartupLoader>()
10: .AddTransient<IApplicationBuilderFactory, ApplicationBuilderFactory>();
11: }
12:
13: public IWebHost Build() => new WebHost(services, this.startupType);
14:
15: public IWebHostBuilder UseServer(IServerFactory factory)
16: {
17: services.AddSingleton<IServerFactory>(factory);
18: return this;
19: }
20:
21: public IWebHostBuilder UseStartup(Type startupType)
22: {
23: this.startupType = startupType;
24: return this;
25: }
26: }
五、总结
综上所述,我们已经对ASP.NET Core应用如何利用WebHostBuilder最终构建出请求处理管道的流程以及管道自身处理请求的流程具有了一定的了解,现在我们来做一个简单的总结。请求处理管道涉及到四个核心的对象,它们分别是WebHostBuilder、WebHost、Server和HttpApplication,它们之间具有如图11所示的关系。我们通过WebHostBuilder来创建WebHost,并领用后者来构建请求处理管道。
请求处理管道通过一个Server和一个HttpApplication对象组成,后者是对所有注册的中间件的封装。当WebHost被启动的时候,它会创建Server和HttpApplication对象,并将后者作为参数调用Server的Start方法以启动服务器。启动后的Server开启监听请求并利用HttpApplication来处理接收到请求。当HttpApplication完成了所有请求处理工作之后,它会利用Server完成对请求的最终响应。
上面所述的所有内容都是针对我们自定义的模拟管道来介绍的,虽然我们对这个模拟管道做了极大的简化,但是它依然体现了ASP.NET Core管道处理请求的真实流程,而且真实管道的创建方式也与模拟管道基本一致。如果读者朋友们能够对这个模拟管道具有深刻的理解,我相信对真实管道的把握就会变得非常容易。
一、采用管道处理HTTP请求
二、创建一个“迷你版”的管道来模拟真实管道请求处理流程
三、管道如何处理HTTP请求的
四、管道是如何被创建出来的