文章出處

我們知道ASP.NET Core請求處理管道由一個服務器和一組有序的中間件組成,所以從總體設計來講是非常簡單的,但是就具體的實現來說,由于其中涉及很多對象的交互,我想很少人能夠地把它弄清楚。為了讓讀者朋友們能夠更加容易地理解管道處理HTTP請求的總體流程,我們根據真實管道的實現原理再造了一個“模擬管道”并在此管道上開發了一個發布圖片的應用,這篇文章旨在為你講述管道是如何處理HTTP請求的

目錄
一、HttpApplication
    FeatureCollection
    HostingApplication
二、HttpContext
    DefaultHttpContext
    HostingApplication
    小結
三、服務器
    HttpListenerServer
    ServerFactory
    小結

一、HttpApplication

clip_image002

ASP.NET Core請求處理管道由一個服務器和一組有序排列的中間件組合而成。我們可以在這基礎上作進一步個抽象,將后者抽象成一個HttpApplication對象,那么該管道就成了一個Server和HttpApplication的綜合體(如圖5所示)。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: }

FeatureCollection

用于創建上下文的CreateContext方法具有一個類型為IFeatureCollection接口的參數。顧名思義,這個接口用于描述某個對象所具有的一組特性,我們可以將它視為一個字典,它Vaue代表特性描述對象,Key則表示該對象的注冊類型(可以是特性描述對象的真實類型或者真實類型的基類或者實現的接口)。我們可以調用Get方法根據類型得到設置的特性描述對象,后者的設置則通過Set方法來完成。FeatureCollection類型采用最簡單的方式實現了這個接口。

   1: public interface IFeatureCollection
   2: {
   3:     TFeature Get<TFeature>();
   4:     void Set<TFeature>(TFeature instance);
   5: }
   6:  
   7: public class FeatureCollection : IFeatureCollection
   8: {
   9:     private ConcurrentDictionary<Type, object> features = new ConcurrentDictionary<Type, object>();
  10:  
  11:     public TFeature Get<TFeature>()
  12:     {
  13:         object feature;
  14:         return features.TryGetValue(typeof(TFeature), out feature) 
  15:             ? (TFeature)feature 
  16:             : default(TFeature);
  17:     }
  18:  
  19:     public void Set<TFeature>(TFeature instance)
  20:     {
  21:         features[typeof(TFeature)] = instance;
  22:     }
  23: }

HostingApplication

管道模式采用的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: }

6右圖所示的UML體現了與HttpApplication相關的核心接口/類型之間的關系。總得來說,通過泛型接口IHttpApplication<TContext>表示HttpApplication是對注冊的中間件的封裝。HttpApplication在一個自行創建的上下文中完成對服務器接收請求的處理,而上下文根據表述原始HTTP上下文的特性集合來創建,這個特性集合通過接口IFeatureCollection來表示,FeatureCollection是該接口的默認實現者。ASP.NET Core 默認使用的HttpApplication是一個HostingApplication對象,它創建的上下文類型為Context,一個Context對象是對一個HttpContext和其他與日志相關上下文信息的封裝。

二、HttpContext

用來描述當前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屬性。對于HttpResponse來說,我們保留了三個分別表示輸出流(OutputStream)、媒體類型(ContentType)和響應狀態碼(StatusCode)。

   1: public abstract class HttpRequest
   2: {
   3:     public abstract Uri Url { get; }
   4: }
   5:  
   6: public abstract class HttpResponse
   7: {
   8:     public abstract Stream     OutputStream { get; }
   9:     public abstract string     ContentType { get; set; }
  10:     public abstract int        StatusCode { get; set; }
  11:  
  12:     public void WriteFile(string fileName, string contentType)
  13:     {
  14:         if (File.Exists(fileName))
  15:         {
  16:             byte[] content         = File.ReadAllBytes(fileName);
  17:             this.ContentType       = contentType;
  18:             this.OutputStream.Write(content, 0, content.Length);
  19:         }
  20:         this.StatusCode = 404;
  21:     }
  22: }

DefaultHttpContext

ASP.NET Core默認使用的HttpContext是一個類型為DefaultHttpContext對象,在介紹DefaultContext的實現原理之前,我們必須了解這個一個事實:請求的接收者和最終響應者是服務器,一般來說服務器接收到請求之后會創建自己的上下文來描述當前請求,針對請求的相應也通過這個原始上下文來完成。在應用中不僅統一使用這個DefaultHttpContext對象來獲取請求信息,同時還利用它來完成對請求的響應,所以它必然與服務器創建的原始上下文存在某個關聯,這種關聯是通過上面我們提到過的這個FeatureCollection對象來實現的。

clip_image006

如右圖所示,不同類型的服務器在接收到請求的時候會創建一個原始的上下文,然后它會將操作原始上下文的操作封裝成一系列標準的特性對象(特性類型實現統一的接口)。這些特性對象最終服務器被組裝成一個FeatureCollection對象,應用程序中使用的DefaultHttpContext就是根據它創建的。當我們調用DefaultHttpContext相應的屬性和方法時,在它的內部實際上借助封裝的特性對象去操作原始的上下文。

一旦了解DefaultHttpContext是如何操作原始HTTP上下文之后,對于DefaultHttpContext的定義就很好理解了。如下面的代碼片斷所示,DefaultHttpContext具有一個IFeatureCollection類型的屬性HttpContextFeatures,它表示的正是由服務器創建的用于封裝原始HTTP上下文相關特性的FeatureCollection對象。通過構造函數的定義我們知道對于一個DefaultHttpContext對象來說,表示請求和響應的分別是一個DefaultHttpRequst和HttpResponse對象。

   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: }

封裝各種原始HTTP上下文的特性能夠統一被DefaultHttpContext所用,它們的類型需要實現統一的接口,在這里我們定義了如下兩個針對請求和響應的特性接口IHttpRequestFeature和IHttpResponseFeature,它們與HttpRequest和HttpResponse具有類似的成員定義。

   1: public interface IHttpRequestFeature
   2: {
   3:     Uri Url { get; }
   4: }
   5:  
   6: public interface IHttpResponseFeature
   7: {
   8:     Stream     OutputStream { get; }
   9:     string     ContentType { get; set; }
  10:     int        StatusCode { get; set; }
  11: }

實際上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:  
  14: public class DefaultHttpResponse : HttpResponse
  15: {
  16:     public IHttpResponseFeature ResponseFeature { get; }
  17:  
  18:     public override Stream OutputStream
  19:     {
  20:         get { return this.ResponseFeature.OutputStream; }
  21:     }
  22:  
  23:     public override string ContentType
  24:     {
  25:         get { return this.ResponseFeature.ContentType; }
  26:         set { this.ResponseFeature.ContentType = value; }
  27:     }
  28:  
  29:     public override int StatusCode
  30:     {
  31:         get { return this.ResponseFeature.StatusCode; }
  32:         set { this.ResponseFeature.StatusCode = value; }
  33:     }
  34:  
  35:     public DefaultHttpResponse(DefaultHttpContext context)
  36:     {
  37:         this.ResponseFeature = context.HttpContextFeatures.Get<IHttpResponseFeature>();
  38:     }
  39: }

HostingApplication

在了解了DefaultHttpContext的實現原理之后,我們在回頭看看上面作為默認HttpApplication類型的HostingApplication的定義。由于對請求的處理總是在一個由HttpContext對象表示的上下文中進行,所以針對請求的處理最終可以通過具有如下定義的RequestDelegate委托對象來完成。一個HttpApplication對象可以視為對一組中間件的封裝,它對請求的處理工作最終交給這些中間件來完成,所有中間件對請求的處理最終可以轉換成通過屬性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) 
  21:        => context.Scope?.Dispose();
  22:  
  23:     public Task ProcessRequestAsync(Context context) 
  24:        => this.Application(context.HttpContext);
  25: }

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

小結

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

三、服務器

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

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

HttpListenerServer

在我們演示的發布圖片應用中使用的服務器是一個類型為HttpListenerServer的服務器。顧名思義,這個簡單的服務器直接利用HttpListener來完成對請求的監聽、接收和響應工作。如下面的代碼片斷所示,我們創建一個HttpListenerServer對象時需要為HttpListener指定一個監聽地址前綴,如果沒有指定會自動使用默認的地址(“http://localhost:3721/”)。

   1: public class HttpListenerServer : IServer
   2: {
   3:     public HttpListener Listener { get; }
   4:  
   5:     public HttpListenerServer(string url)
   6:     {
   7:         this.Listener = new HttpListener();
   8:         this.Listener.Prefixes.Add(url ?? "http://localhost:3721/");
   9:     }
  10:  
  11:     public void Start<TContext>(IHttpApplication<TContext> application)
  12:     {
  13:         this.Listener.Start();
  14:         while (true)
  15:         {
  16:             HttpListenerContext httpListenerContext = this.Listener.GetContext();
  17:  
  18:             HttpListenerContextFeature feature = new HttpListenerContextFeature(httpListenerContext);
  19:             FeatureCollection contextFeatures = new FeatureCollection();
  20:             contextFeatures.Set<IHttpRequestFeature>(feature);
  21:             contextFeatures.Set<IHttpResponseFeature>(feature);
  22:             TContext context = application.CreateContext(contextFeatures);
  23:  
  24:             application.ProcessRequestAsync(context)
  25:                 .ContinueWith(_ => httpListenerContext.Response.Close())
  26:                 .ContinueWith(_ => application.DisposeContext(context, _.Exception));
  27:         }
  28:     }
  29: }

在Start方法中,我們調用HttpListener的Start方法開始監聽來自網絡的HTTP請求。HTTP請求一旦抵達,表示原始上下文的HttpListenerContext對象通過調用HttpListener的GetContext方法返回。我們創建了一個表述這個原始上下文相關特性的HttpListenerContextFeature對象,并將它分別針對類型IHttpRequestFeature和IHttpResponseFeature添加到創建的FeatureCollection對象上。作為參數的HttpApplication對象將它作為參數調用CreateContext方法創建上下文,并調用ProcessRequestAsync方法在這個上下文中處理當前請求。當所有的請求處理工作結束之后,我們會調用HttpApplication對象的DisposeContext方法回收釋放這個上下文。

ServerFactory

當WebHost在創建管道的時候并不會直接創建服務器對象,服務器對象是通過它的工廠ServerFactory創建的。ServerFactory是對所有實現了IServerFactory接口的所有類型及其對象的統稱,我們在模擬管道中對這個對象作了如下的簡化,除去了創建服務器的CreateServer方法的參數。作為HttpListenerServer的工廠類,HttpListenerServerFactory直接利用構造函數中指定的監聽地址創建了在CreateServer方法中返回的HttpListenerServer對象。

   1: public interface IServerFactory
   2: {
   3:     IServer CreateServer();
   4: }
   5:  
   6: public class HttpListenerServerFactory : IServerFactory
   7: {
   8:     private string listenUrl;
   9:  
  10:     public HttpListenerServerFactory(string listenUrl = null)
  11:     {
  12:         this.listenUrl = listenUrl?? "http://localhost:3721/";
  13:     }
  14:  
  15:     public IServer CreateServer()
  16:     {
  17:         return new HttpListenerServer(listenUrl);
  18:     }
  19: }

小結

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

 


一、采用管道處理HTTP請求
二、創建一個“迷你版”的管道來模擬真實管道請求處理流程
三、管道如何處理HTTP請求的
四、管道是如何被創建出來的


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

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