在《中篇》中,我們對管道的構成以及它對請求的處理流程進行了詳細介紹,接下來我們需要了解的是這樣一個管道是如何被構建起來的。總的來說,管道由一個服務器和一個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管道中的處理流程[下]:管道如何創建
文章列表