從上面的內容我們知道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:
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正是分別根據它們建立的。
四、伺服器——實現對請求的監聽、接收和響應
管道中的伺服器通過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物件。
通過重建Hosting系統理解HTTP請求在ASP.NET Core管道中的處理流程[上]:採用管道處理請求
通過重建Hosting系統理解HTTP請求在ASP.NET Core管道中的處理流程[中]:管道如何處理請求
通過重建Hosting系統理解HTTP請求在ASP.NET Core管道中的處理流程[下]:管道如何建立
原始碼下載