1. 程式人生 > >通過重建Hosting系統理解HTTP請求在ASP.NET Core管道中的處理流程[上]:採用管道處理請求

通過重建Hosting系統理解HTTP請求在ASP.NET Core管道中的處理流程[上]:採用管道處理請求

之所以稱ASP.NET Core是一個Web開發平臺,而不是一個單純的開發框架,源於它具有一個極具擴充套件性的請求處理管道,我們可以通過對這個管道的定製來滿足各種場景下的HTTP處理需求。ASP. NET Core應用的很多特性,比如路由、認證、會話、快取等,都是通過對管道的定製來實現的。我們甚至可以通過管道定製在ASP.NET Core平臺上建立我們自己的Web框架,實際上MVC和SingalR這兩個重要的Web框架也是採用這樣的方式建立的。 [本文已經同步到《ASP.NET Core框架揭祕》之中] [原始碼從這裡下載]

目錄
一、從Hello World說起
二、管道的構成
三、管道的定製

一、從Hello World說起

HTTP協議自身的特性決定了任何一個Web應用的工作方式都是監聽、接收並處理HTTP請求,並在最終對請求予以響應,HTTP請求處理是管道式設計典型的應用場景。具體來說,我們根據具體的HTTP處理請求構建一個管道,接收到的HTTP請求訊息想水一樣流入這個管道,組成這個管道的各個環節依次對它作相應的處理。處理的結果同樣轉變成訊息逆向流入這個管道進行處理,並最終轉變成回覆給客戶端的HTTP響應。ASP.NET Core的訊息處理管道從設計的角度來講是非常簡單的,但是從具體實現的角度則相對複雜並相對難以理解,為了讓讀者朋友們通過本章對此具有深刻的理解,我們從簡單的部分講起。

為了使讀者朋友們能夠以最直觀的感受認識ASP.NET Core的訊息處理管道,我們來建立一個最簡單的Hello World程式。這是一個控制檯應用,整個程式由如下所示的五行程式碼組成。當我們執行這個程式之後,一個名為KestrelServer的伺服器將會啟動並繫結到本機上的5000埠進行請求監聽。針對所有接收到的請求,我們都有會響應一個“Hello World”字串。

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .Configure(app => app.Run(async context=> await context.Response.WriteAsync("Hello World")))            
   8:             .Build()
   9:             .Run();
  10:     }
  11: }

這個程式涉及到一個名為WebHost重要的物件, 它可以看成是Web應用的宿主,啟動Web應用本質上就是啟動作為宿主的WebHost物件。WebHostBuilder是WebHost的建立者,我們呼叫它的Build方法建立相應的WebHost。當我們呼叫WebHost的擴充套件方法Run啟動應用的時候,用於監聽、接收、處理和響應HTTP請求的管道隨之被建立。那麼在這個過程中,通過呼叫Configure方法註冊到WebHostBuilder上的委託物件(委託型別為Action<IApplicationBuilder>)將用於管道的定製。總的來說,ASP.NET Core管道由WebHost在啟動的時候構建,WebHostBuilder則是後者的建立者,下圖揭示了三者之間的關係。

clip_image001 

二、管道的構成

HTTP請求處理流程始於對請求的監聽與接收,終於對請求的響應,這兩項工作均由同一個物件來完成,我們稱之為 “伺服器(Server)” 。儘管ASP.NET Core的請求處理管道可以被自由地訂製,但是該管道必須有一個Server,Server是整個管道的 “龍頭” 。在上面的這個Hello World應用中,在呼叫WebHostBuilder的Build方法建立一個WebHost之前,我們呼叫了它的一個擴充套件方法UseKestrel,這個方法的作用就是為後續構建的管道註冊一個名為KestrelServer的Server。

隨著WebHost的Start方法(當我們呼叫WebHost的擴充套件方法Run時,它的Start方法會自動被呼叫)的呼叫,定製的管道會被構建出來,管道的伺服器將會繫結到一個預設的埠(比如KestrelServer預設採用5000作為監聽埠)開始監聽請求。HTTP請求一旦抵達,Server會並將其標準並分發給管道後續的節點,我們將管道中位於伺服器之後的節點稱為“中介軟體(Middleware)”。每個中介軟體都具有各自獨立的功能,比如我們有專門實現路由功能的中介軟體,有專門實施使用者認證的中介軟體。所謂的管道定製體現在根據具體的需求選擇對應的中介軟體組成最終處理請求的管道。下圖揭示了由一個伺服器和一組中介軟體構成的請求處理管道。

clip_image002

一個建立在ASP.NET Core之上的應用一般都是根據某個框架開發的,一般來說,開發框架本身就是通過某一個或者多箇中間件構建的。以ASP.NET Core MVC這個最著名的開發框架為例,它實際上是藉助於一個叫做 “路由” 的中介軟體實現了請求地址與Controller/Action之間的對映,並在此基礎實現了啟用Controller、執行Action以及呈現View等一系列的功能。所以應用程式可以視為某個中介軟體的一部分,如果一定要將它獨立出來,整個請求處理管道將呈現出如下圖所示的結構。

clip_image003 

三、管道的定製

在演示的Hello World程式中,我們在呼叫擴充套件方法UseKestrel註冊KestrelServer伺服器之後,還呼叫WebHostBuilder如下一個名為Configure的擴充套件方法註冊了一個型別為Action<IApplicationBuilder>的委託物件。從請求處理管道的角度來講,註冊這個委託物件的目的在於對構建的管道進行定製,說得更加具體一點,我們利用這個型別為管道註冊需要的中介軟體。演示例項中註冊的這個委託物件呼叫ApplicationBuilder的擴充套件方法Run註冊了一箇中間件來為每個請求響應一個 “Hello World” 字串。

   1: public static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Action<IApplicationBuilder> configureApp) 

除了通過呼叫WebHostBuilder的Configure方法註冊一個Action<IApplicationBuilder>型別的委託,註冊中間定義管道的邏輯更多地還是定義在一個單獨的型別中。由於管道的定製總是在應用啟動(Startup)的時候進行,我們一般稱這個用於定製管道的型別為“啟動型別”,並在大部分情況下會直接命名為Startup。按照約定,通過註冊中介軟體定製管道的操作會實現在名為Configure的方法中,方法的第一個引數型別必須是IApplicationBuilder介面,後面可定義任意數量和型別的引數,當這個方法被ASP.NET Core框架呼叫的時候,這些引數會採用依賴注入的方式來提供。啟動型別可以通過呼叫WebHostBuilder的擴充套件方法UseStartup<T>進行註冊,如下面的程式碼與前面演示的例項是完全等效的。

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .UseStartup<Startup>()
   8:             .Build()
   9:             .Run();
  10:     }
  11:     public class Startup
  12:     {
  13:         public void Configure(IApplicationBuilder app)
  14:         {
  15:             app.Run(async context => await context.Response.WriteAsync("Hello World"));
  16:         }
  17:     }
  18: }

在真正的專案開發中,我們會利用ApplicationBuilder註冊相應的中介軟體進而構建一個適合當前請求處理需求的管道。如下面的程式碼片段所示,我們除了按照如上的方式呼叫擴充套件方法UseMvc註冊了支撐MVC框架的中介軟體(實際上是一個實現路由的中介軟體)之外,我們還通過呼叫其它的擴充套件方法註冊了相應的中介軟體實現了對靜態檔案的訪問(UseStaticFiles)、錯誤頁面的呈現(UseExceptionHandler)以及基於ASP.NET Identity Framework的認證(UseIdentity)。

   1: public class Startup
   2: {
   3:     public void Configure(IApplicationBuilder app)
   4:     {
   5:         app.UseExceptionHandler("/Home/Error");
   6:         app.UseStaticFiles();
   7:         app.UseIdentity();           
   8:  
   9:         app.UseMvc();
  10:     }
  11: }