通過重建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>
在大部分應用中,我們會針對具體的請求處理需求註冊多個不同的中介軟體,這些中介軟體按照註冊時間的先後順序進行排列進而構成管道。對於某個中介軟體來說,在它完成了自身的請求處理任務之後,需要將請求傳遞給下一個中介軟體作後續的處理。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 : IWebHost