1. 程式人生 > >通過重建Hosting系統理解HTTP請求在ASP.NET Core管道中的處理流程[下]:管道是如何構建起來的?

通過重建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>

的委託物件,對於很多剛剛接觸請求處理管道的讀者朋友們來說,可能一開始對此有點難以理解,所以容來略作解釋。我們上面已經提到過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實際上體現了整個管道對請求的處理。

clip_image001

在對中介軟體有了充分的瞭解之後,我們來看看用於註冊中介軟體的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