文章出處

從上面的內容我們知道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”),我們的也會幫助我們自動匹配一個文件名(不包含擴展名)相同的圖片。

4

由于我們模擬的管道采用與真實管道一致的應用編程接口,所以兩種采用的編程模式也是一致的。這個用于發布圖片的應用是通過如下幾行簡單的代碼構建起來的。如下面的代碼片斷所示,我們在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會對此上下文實施回收釋放處理。

5

我們通過具有如下定義的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和其他與日志相關上下文信息的封裝。

6

三、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對象來實現的。

image

如上圖所示,不同類型的服務器在接收到請求的時候會創建一個原始的上下文,接下來它會將針對原始上下文的操作封裝成一系列標準的特性對象(特性類型實現統一的接口)。這些特性對象最終服務器被組裝成一個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:  
   7: public interface IHttpResponseFeature
   8: {
   9:     Stream     OutputStream { get; }
  10:     string     ContentType { get; set; }
  11:     int        StatusCode { get; set; }
  12: }

實際上DefaultHttpContext對象中表示請求和響應的DefaultHttpRequest和DefaultHttpResponse對象就是分別根據從提供的FeatureCollection中獲取的HttpRequestFeature和HttpResponseFeature對象創建的,具體的實現體現在如下所示的代碼片斷中。

   1: public class DefaultHttpRequest : HttpRequest
   2: {
   3:     public IHttpRequestFeature RequestFeature { get; }
   4:     public DefaultHttpRequest(DefaultHttpContext context)
   5:     {
   6:         this.RequestFeature = context.HttpContextFeatures.Get<IHttpRequestFeature>();
   7:     }
   8:     public override Uri Url
   9:     {
  10:         get { return this.RequestFeature.Url; }
  11:     }
  12:  
  13:     public override string PathBase
  14:     {
  15:         get { return this.RequestFeature.PathBase; }
  16:     }
  17: }
  18: public class DefaultHttpResponse : HttpResponse
  19: {
  20:     public IHttpResponseFeature ResponseFeature { get; }
  21:  
  22:     public override Stream OutputStream
  23:     {
  24:         get { return this.ResponseFeature.OutputStream; }
  25:     }
  26:  
  27:     public override string ContentType
  28:     {
  29:         get { return this.ResponseFeature.ContentType; }
  30:         set { this.ResponseFeature.ContentType = value; }
  31:     }
  32:  
  33:     public override int StatusCode
  34:     {
  35:         get { return this.ResponseFeature.StatusCode; }
  36:         set { this.ResponseFeature.StatusCode = value; }
  37:     }
  38:  
  39:     public DefaultHttpResponse(DefaultHttpContext context)
  40:     {
  41:         this.ResponseFeature = context.HttpContextFeatures.Get<IHttpResponseFeature>();
  42:     }
  43: }

在了解了DefaultHttpContext的實現原理之后,我們在回頭看看上面作為默認HttpApplication類型的HostingApplication的定義。由于對請求的處理總是在一個由HttpContext對象表示的上下文中進行,所以針對請求的處理最終可以通過具有如下定義的RequestDelegate委托對象來完成。一個HttpApplication對象可以視為對一組中間件的封裝,它對請求的處理工作最終交給這些中間件來完成,所有中間件對請求的處理最終可以轉換成一個RequestDelegate對象,HostingApplication的Application屬性返回的就是這么一個RequestDelegate對象。

   1: public class HostingApplication : IHttpApplication<Context>
   2: {
   3:     public RequestDelegate Application { get; }
   4:  
   5:     public HostingApplication(RequestDelegate application)
   6:     {
   7:         this.Application = application;
   8:     }
   9:  
  10:     public Context CreateContext(IFeatureCollection contextFeatures)
  11:     {
  12:         HttpContext httpContext = new DefaultHttpContext(contextFeatures);
  13:         return new Context
  14:         {
  15:             HttpContext     = httpContext,
  16:             StartTimestamp  = Stopwatch.GetTimestamp()
  17:         };
  18:     }
  19:  
  20:     public void DisposeContext(Context context, Exception exception) => context.Scope?.Dispose();
  21:     public Task ProcessRequestAsync(Context context) => this.Application(context.HttpContext);
  22: }
  23:  
  24: public delegate Task RequestDelegate(HttpContext context);

當我們創建一個HostingApplication對象的時候,需要將所有注冊的中間件轉換成一個RequestDelegate類型的委托對象,并將其作為構造函數的參數,ProcessRequestAsync方法會直接利用這個委托對象來處理請求。當CreateContext方法被執行的時候,它會直接利用封裝原始HTTP上下文的FeatureCollection對象創建一個DefaultHttpContext對象,進而一個Context對象。在簡化的DisposeContext方法中,我們只是調用了Context對象的Scope屬性的Dispose方法(如果Scope存在),實際上我們在創建Context的時候并沒有Scope屬性進行初始化。

我們依然通過一個UML對表示HTTP上下文相關的接口/類型及其相互關系進行總結。如下圖8所示,針對當前請求的HTTP上下文通過抽象類HttpContext表示,請求和響應是HttpContext表述的兩個最為核心的上下文請求,它們分別通過抽象類HttpRequest和HttpResponse表示。ASP.NET Core 默認采用的HttpContext類型為DefaultHttpContext,它描述的請求和響應分別是一個DefaultHttpRequst和DefaultHttpResponse對象。一個DefaultHttpContext對象由描述原始HTTP上下文的特性集合來創建,其中描述請求與相應的特性分別通過接口IHttpRequestFeature和IHttpResponseFeature表示,DefaultHttpRequst和DefaultHttpResponse正是分別根據它們創建的。

8

四、服務器——實現對請求的監聽、接收和響應

管道中的服務器通過IServer接口表示,在模擬管道對應的應用編程接口中,我們只保留了兩個核心成員,其中Features屬性返回描述服務器的特性,而Start方法則負責啟動服務器。Start方法被執行的時候,服務會馬上開始實施監聽工作。HTTP請求一旦抵達,該方法會利用作為參數的HttpApplication對象創建一個上下文,并在此上下文中完成對請求的所有處理操作。當完成了對請求的處理任務之后,HttpApplication對象會自行負責回收釋放由它創建的上下文。

   1: public interface IServer
   2: {
   3:     IFeatureCollection Features { get; }
   4:     void Start<TContext>(IHttpApplication<TContext> application);    
   5: }

在我們演示的發布圖片應用中使用的服務器是一個類型為HttpListenerServer的服務器。顧名思義,這個簡單的服務器直接利用HttpListener來完成對請求的監聽、接收和響應工作。這個HttpListener對象通過Listener這個只讀屬性表示,我們在構造函數中創建它。對于這個HttpListener,我們并沒有直接為他指定監聽地址,監聽地址的獲取是通過一個由IServerAddressesFeature接口表示的特性來提供的。如下面的代碼片段所示,這個特性接口通過一個字符串集合類型的Addresses屬性表示監聽地址列表,ServerAddressesFeature是這個特性接口的默認實現類型。在構造函數中,我們在初始化Features屬性之后,會添加一個ServerAddressesFeature對象到這個特性集合中。

   1: public class HttpListenerServer : IServer
   2: {
   3:     public HttpListener         Listener { get; }
   4:     public IFeatureCollection     Features { get; }
   5:  
   6:     public HttpListenerServer()
   7:     {
   8:         this.Listener = new HttpListener();
   9:         this.Features = new FeatureCollection()
  10:             .Set<IServerAddressesFeature>(new ServerAddressesFeature());
  11:     }
  12:     ...
  13: }
  14:  
  15: public interface IServerAddressesFeature
  16: {
  17:     ICollection<string> Addresses { get; }
  18: }
  19:  
  20: public class ServerAddressesFeature : IServerAddressesFeature
  21: {
  22:     public ICollection<string> Addresses { get; } = new Collection<string>();
  23: }

在Start方法中,我們從特性集合中提取出這個ServerAddressesFeature對象,并將設置的監聽地址集合注冊到HttpListener對象上,然后調用其Start方法開始監聽來自網絡的HTTP請求。HTTP請求一旦抵達,我們會調用HttpListener的GetContext方法得到表示原始HTTP上下文的HttpListenerContext對象,并根據它創建一個類型為HttpListenerContextFeature的特性對象,該對象分別采用類型IHttpRequestFeature和IHttpResponseFeature注冊到創建的FeatureCollection對象上。作為參數的HttpApplication對象將它作為參數調用CreateContext方法創建出類型為TContext的上下文對象,我們最終將它作為參數調用HttpApplication對象的ProcessRequestAsync方法讓注冊的中間件來處理當前請求。當所有的請求處理工作結束之后,我們會調用HttpApplication對象的DisposeContext方法回收釋放這個上下文。

   1: public class HttpListenerServer : IServer
   2: {
   3:     ...
   4:     public void Start<TContext>(IHttpApplication<TContext> application)
   5:     {
   6:         IServerAddressesFeature addressFeatures = this.Features.Get<IServerAddressesFeature>();
   7:         foreach (string address in addressFeatures.Addresses)
   8:         {
   9:             this.Listener.Prefixes.Add(address.TrimEnd('/') + "/");
  10:         }
  11:  
  12:         this.Listener.Start();
  13:         while (true)
  14:         {
  15:             HttpListenerContext httpListenerContext = this.Listener.GetContext();
  16:  
  17:             HttpListenerContextFeature feature = new HttpListenerContextFeature(httpListenerContext, this.Listener);
  18:             IFeatureCollection contextFeatures = new FeatureCollection()
  19:                 .Set<IHttpRequestFeature>(feature)
  20:                 .Set<IHttpResponseFeature>(feature);
  21:             TContext context = application.CreateContext(contextFeatures);
  22:  
  23:             application.ProcessRequestAsync(context)
  24:                 .ContinueWith(_ => httpListenerContext.Response.Close())
  25:                 .ContinueWith(_ => application.DisposeContext(context, _.Exception));
  26:         }
  27:     }
  28: }

由于HttpListenerServer采用一個HttpListener對象作為監聽器,由它接收的請求將被封裝成一個類型為HttpListenerContext的上下文對象。我們通過一個HttpListenerContextFeature類型來封裝這個HttpListenerContext對象。如下面的代碼片段所示,HttpListenerContextFeature實現了IHttpRequestFeature和IHttpResponseFeature接口,HttpApplication所代表的中間件不僅僅利用這個特性獲取所有與請求相關的信息,而且針對請求的任何響應也都是利用這個特性來實現的。

   1: public class HttpListenerContextFeature : IHttpRequestFeature, IHttpResponseFeature
   2: {
   3:     private readonly HttpListenerContext context;    
   4:  
   5:     public string ContentType
   6:     {
   7:         get { return context.Response.ContentType; }
   8:         set { context.Response.ContentType = value; }
   9:     }
  10:  
  11:     public Stream OutputStream { get; }
  12:  
  13:     public int StatusCode
  14:     {
  15:         get { return context.Response.StatusCode; }
  16:         set { context.Response.StatusCode = value; }
  17:     }
  18:  
  19:     public Uri Url { get; }
  20:     public string PathBase { get; }
  21:  
  22:     public HttpListenerContextFeature(HttpListenerContext context, HttpListener listener)
  23:     {
  24:         this.context = context;
  25:         this.Url = context.Request.Url;
  26:         this.OutputStream = context.Response.OutputStream;
  27:         this.PathBase = (from it in listener.Prefixes
  28:           let pathBase = new Uri(it).LocalPath.TrimEnd('/')
  29:           where context.Request.Url.LocalPath.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)
  30:           select pathBase).First();
  31:     }
  32: }

下圖所示的UML體現了與服務器相關的接口/類型之間的關系。通過接口IServer表示的服務器表示管道中完成請求監聽、接收與相應的組件,我們自定義的HttpListenerServer利用一個HttpListener實現了這三項基本操作。當HttpListenerServer接收到抵達的HTTP請求之后,它會將表示原始HTTP上下文的特性封裝成一個HttpListenerContextFeature對象,HttpListenerContextFeature實現了分別用于描述請求和響應特性的接口IHttpRequestFeature和IHttpResponseFeature,HostingApplication可以利用這個HttpListenerContextFeature對象來創建DefaultHttpContext對象。

image

 


通過重建Hosting系統理解HTTP請求在ASP.NET Core管道中的處理流程[上]:采用管道處理請求
通過重建Hosting系統理解HTTP請求在ASP.NET Core管道中的處理流程[中]:管道如何處理請求
通過重建Hosting系統理解HTTP請求在ASP.NET Core管道中的處理流程[下]:管道如何創建
源代碼下載


文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()