ASP.NET Core請求處理管道由一個伺服器和一組中間件構成。如果想非常深刻地認識ASP.NET Core的請求處理管道,我覺得可以分兩個步驟來進行:首先,我們可以在忽略具體細節的前提下搞清楚管道處理HTTP請求的總體流程;在對總體流程有了大致瞭解之後,我們再來補充這些刻意忽略的細節。為了讓讀者... ...
從上面的內容我們知道ASP.NET Core請求處理管道由一個伺服器和一組中間件構成,所以從總體設計來講是非常簡單的。但是就具體的實現來說,由於其中涉及很多對象的交互,很少人能夠地把它弄清楚。如果想非常深刻地認識ASP.NET Core的請求處理管道,我覺得可以分兩個步驟來進行:首先,我們可以在忽略具體細節的前提下搞清楚管道處理HTTP請求的總體流程;在對總體流程有了大致瞭解之後,我們再來補充這些刻意忽略的細節。為了讓讀者朋友們能夠更加容易地理解管道處理HTTP請求的總體流程,我們根據真實管道的實現原理再造了一個“迷你版的管道”。[本文已經同步到《ASP.NET Core框架揭秘》之中] [源代碼從這裡下載]
目錄
一、建立在“模擬管道”上的應用
二、HttpApplication——一組中間件的有序集合
三、HttpContext——對當前HTTP上下文的抽象
四、伺服器——實現對請求的監聽、接收和響應
一、建立在“模擬管道”上的應用
再造的迷你管道不僅僅體現了真實管道中處理HTTP請求的流程,並且對於其中涉及的介面和類型,我們也基本上採用了相同的命名方式。但是為了避免“細枝末節”造成的干擾,我會進行最大限度的裁剪。對於大部分方法,我們只會保留最核心的邏輯。對於一些介面,我們會剔除那些與核心流程無關的成員。在通過這個模擬管道講解HTTP請求的總體處理流程之前,我們先來看看如何在它基礎上開發一個簡單的應用。
我們在這個模擬管道上開發一個簡單的應用來發佈圖片。具體的應用場景是這樣:我們將圖片文件保存在伺服器上的某個目錄下,客戶端可以通過發送HTTP請求併在請求地址上指定文件名的方式來獲取目標圖片。如下圖所示,我們利用瀏覽器向針對某張圖片的地址(“http://localhost:3721/images/hello.png”)發送請求後,獲取到的目標圖片(hello.png)會直接顯示到瀏覽器上。除此之外,如果指定的圖片地址沒有包含擴展名(“.png”),我們的也會幫助我們自動匹配一個文件名(不包含擴展名)相同的圖片。
由於我們模擬的管道採用與真實管道一致的應用編程介面,所以兩種採用的編程模式也是一致的。這個用於發佈圖片的應用是通過如下幾行簡單的代碼構建起來的。如下麵的代碼片斷所示,我們在Main方法中創建了一個WebHostBuilder對象,在調用其Build方法創建應用宿主的WebHost之前,我們調用擴展方法UseHttpListener註冊了一個類型為HttpListenerServer的伺服器。這個HttpListenerServer是我們自己定義的伺服器,它利用一個HttpListener對象實現了針對HTTP請求的監聽、接收和最終的響應。監聽地址(“http://localhost:3721/images”)是通過調用擴展方法UseUrls指定的。
1: public class Program
2: {
3: public static void Main()
4: {
5: new WebHostBuilder()
6: .UseHttpListener()
7: .UseUrls("http://localhost:3721/images")
8: .Configure(app => app.UseImages(@"c:\images"))
9: .Build()
10: .Start();
11:
12: Console.Read();
13: }
14: }
應用針對圖片獲取請求的處理是通過我們自定義的中間件完成的。在調用WebHostBuilder的Configure方法定義管道過程中,我們調用IApplicationBuilder介面的擴展方法UseImages完成了針對這個中間件的定製。在調用這個擴展方法的時候,我們指定了存放圖片的目錄(“c:\images”),我們通過瀏覽器獲取的這個圖片(“hello.png”)就保存在這個目錄下。
二、HttpApplication——一組中間件的有序集合
ASP.NET Core請求處理管道由一個伺服器和一組有序排列的中間件組合而成。我們可以在這基礎上作進一步個抽象,將後者抽象成一個HttpApplication對象,那麼該管道就成了一個Server和HttpApplication的綜合體(如下圖所示)。Server會將接收到的HTTP請求轉發給HttpApplication對象,後者會針對當前請求創建一個上下文,併在此上下文中處理請求,請求處理完成並完成響應之後HttpApplication會對此上下文實施回收釋放處理。
我們通過具有如下定義的IHttpApplication<TContext>類型來表示上述的這個HttpApplication,泛型參數TContext代表它針對每個請求而建立的上下文。一個HttpApplication對象在接收到Server轉發的請求之後需要完成三項基本的操作,即創建上下文、在上下文中處理請求以及請求處理完成之後釋放上下文,這三個基本操作正好通過對應的三個方法來完成。
1: public interface IHttpApplication<TContext>
2: {
3: TContext CreateContext(IFeatureCollection contextFeatures);
4: Task ProcessRequestAsync(TContext context);
5: void DisposeContext(TContext context, Exception exception);
6: }
用於創建上下文的CreateContext方法具有一個類型為IFeatureCollection介面的參數。顧名思義,這個介面用於描述某個對象所具有的一組特性,我們可以將它視為一個Dictionary<Type, object>對象,字典對象的Value代表特性對象,Key則表示該對象的註冊類型(可以是特性描述對象的真實類型、真實類型的基類或者實現的介面)。我們可以調用Get方法根據指定的註冊類型得到設置的特性對象,特性對象的註冊則通過Set方法來完成。我們自定義的FeatureCollection類型採用最簡單的方式實現了這個介面。
1: public interface IFeatureCollection
2: {
3: TFeature Get<T>();
4: void Set<T>(T instance);
5: }
6:
7: public class FeatureCollection : IFeatureCollection
8: {
9: private ConcurrentDictionary<Type, object> features = new ConcurrentDictionary<Type, object>();
10:
11: public TFeature Get<T>()
12: {
13: object feature;
14: return features.TryGetValue(typeof(T), out feature)
15: ? (T)feature
16: : default(T);
17: }
18:
19: public void Set<T>(T instance)
20: {
21: features[typeof(T)] = instance;
22: }
23: }
管道採用的HttpApplication是一個類型為 HostingApplication的對象。如下麵的代碼片段所示,這個類型實現了介面IHttpApplication<Context>,泛型參數Context是一個針對當前請求的上下文對象。一個Context對象是對一個HttpContext的封裝,後者是真正描述當前HTTP請求的上下文,承載著最為核心的上下文信息。除此之外,我們還為Context定義了Scope和StartTimestamp兩個屬性,兩者與日誌記錄和事件追蹤有關,前者被用來將針對同一請求的多次日誌記錄關聯到同一個上下文範圍(即Logger的BeginScope方法的返回值);後者表示開始處理請求的時間戳,如果在完成請求處理的時候記錄下當前的時間戳,我們就可以計算出整個請求處理所花費的時間。
1: public class HostingApplication : IHttpApplication<Context>
2: {
3: //省略成員定義
4: }
5:
6: public class Context
7: {
8: public HttpContext HttpContext { get; set; }
9: public IDisposable Scope { get; set; }
10: public long StartTimestamp { get; set; }
11: }
下圖所示的UML體現了與HttpApplication相關的核心介面/類型之間的關係。總得來說,通過泛型介面IHttpApplication<TContext>表示HttpApplication是對註冊的中間件的封裝。HttpApplication在一個自行創建的上下文中完成對伺服器接收請求的處理,而上下文根據表述原始HTTP上下文的特性集合來創建,這個特性集合通過介面IFeatureCollection來表示,FeatureCollection是該介面的預設實現者。ASP.NET Core 預設使用的HttpApplication是一個HostingApplication對象,它創建的上下文是一個Context對象,一個Context對象是對一個HttpContext和其他與日誌相關上下文信息的封裝。
三、HttpContext——對當前HTTP上下文的抽象
用來描述當前HTTP請求的上下文的HttpContext對於ASP .NET Core請求處理管道來說是一個非常重要的對象,我們不僅僅可以利用它獲取當前請求的所有細節,還可以直接利用它完成對請求的響應。HttpContext是一個抽象類,很多用於描述當前HTTP請求的上下文信息的屬性被定義在這個類型中。在這個這個模擬管道模型中,我們僅僅保留瞭如下兩個核心的屬性,即表示請求和響應的Requst和Response屬性。
1: public abstract class HttpContext
2: {
3: public abstract HttpRequest Request { get; }
4: public abstract HttpResponse Response { get; }
5: }
表示請求和響應的HttpRequest和HttpResponse同樣是抽象類。簡單起見,我們僅僅保留少數幾個與演示實例相關的屬性成員。如下麵的代碼片段所示,我們僅僅為HttpRequest保留了表示當前請求地址的Url屬性和表示基地址的PathBase屬性。對於HttpResponse來說,我們保留了三個分別表示輸出流(OutputStream)、媒體類型(ContentType)和響應狀態碼(StatusCode)的屬性。
1: public abstract class HttpRequest
2: {
3: public abstract Uri Url { get; }
4: public abstract string PathBase { get; }
5: }
6:
7: public abstract class HttpResponse
8: {
9: public abstract Stream OutputStream { get; }
10: public abstract string ContentType { get; set; }
11: public abstract int StatusCode { get; set; }
12: }
ASP.NET Core預設使用的HttpContext是一個類型為DefaultHttpContext對象,在介紹DefaultContext的實現原理之前,我們必須瞭解這樣一個事實:對應這個管道來說,請求的接收者和最終響應者都是伺服器,伺服器接收到請求之後會創建自己的上下文來描述當前請求,針對請求的響應也通過這個原始上下文來完成。以我應用中註冊的HttpListenerServer為例,由於它內部使用的是一個類型為HttpListener的監聽器,所以它總是會創建一個HttpListenerContext對象來描述接收到的請求,針對請求的響應也是利用這個HttpListenerContext對象來完成的。
但是對於建立在管道上的應用來說,它們是不需要關註管道究竟採用了何種類型的伺服器,更不會關註由這個伺服器創建的這個原始上下文。實際上我們的應用不僅統一使用這個DefaultHttpContext對象來獲取請求信息,同時還利用它來完成對請求的響應。很顯然,應用這使用的這個DefaultHttpContext對象必然與伺服器創建的原始上下文存在某個關聯,這種關聯是通過上面我們提到過的這個FeatureCollection對象來實現的。
如上圖所示,不同類型的伺服器在接收到請求的時候會創建一個原始的上下文,接下來它會將針對原始上下文的操作封裝成一系列標準的特性對象(特性類型實現統一的介面)。這些特性對象最終伺服器被組裝成一個FeatureCollection對象,應用程式中使用的DefaultHttpContext就是根據它創建出來的。當我們調用DefaultHttpContext相應的屬性和方法時,在它的內部實際上藉助封裝的特性對象去操作原始的上下文。
一旦瞭解DefaultHttpContext是如何操作原始HTTP上下文之後,對於DefaultHttpContext的定義就很好理解了。如下麵的代碼片斷所示,DefaultHttpContext具有一個IFeatureCollection類型的屬性HttpContextFeatures,它表示的正是由伺服器創建的用於封裝原始HTTP上下文相關特性的FeatureCollection對象。通過構造函數的定義我們知道對於一個DefaultHttpContext對象來說,表示請求和響應的分別是一個DefaultHttpRequest和DefaultHttpResponse對象。
1: public class DefaultHttpContext : HttpContext
2: {
3: public IFeatureCollection HttpContextFeatures { get;}
4:
5: public DefaultHttpContext(IFeatureCollection httpContextFeatures)
6: {
7: this.HttpContextFeatures = httpContextFeatures;
8: this.Request = new DefaultHttpRequest(this);
9: this.Response = new DefaultHttpResponse(this);
10: }
11: public override HttpRequest Request { get; }
12: public override HttpResponse Response { get; }
13: }
由不同類型的伺服器創建的特性對象之所以能夠統一被DefaultHttpContext所用,原因在於它們的類型都實現統一的介面,在模擬的管道模型中,我們定義瞭如下兩個針對請求和響應的特性介面IHttpRequestFeature和IHttpResponseFeature,它們與HttpRequest和HttpResponse具有類似的成員定義。
1: public interface IHttpRequestFeature
2: {
3: Uri Url { get; }
4: string PathBase { get; }
5: }
6: