從上面的內容我們知道ASP.NET Core請求處理管道由一個服務器和一組中間件構成,所以從總體設計來講是非常簡單的。但是就具體的實現來說,由於其中涉及很多對象的交互,很少人能夠地把它弄清楚。如果想非常深刻地認識ASP.NET Core的請求處理管道,我覺得可以分兩個步驟來進行:首先,我們可以在忽略具體細節的前提下搞清楚管道處理HTTP請求的總體流程;在對總體流程有了大致了解之後,我們再來補充這些刻意忽略的細節。為了讓讀者朋友們能夠更加容易地理解管道處理HTTP請求的總體流程,我們根據真實管道的實現原理再造了一個“迷你版的管道”。 源代碼下載地址:http://files.cnblogs.com/files/artech/Hosting.mini.rar
目錄
一、建立在“模擬管道”上的應用
二、HttpApplication——一組中間件的有序集合
三、HttpContext——對當前HTTP上下文的抽象
四、服務器——實現對請求的監聽、接收和響應
再造的迷你管道不僅僅體現了真實管道中處理HTTP請求的流程,並且對於其中涉及的接口和類型,我們也基本上采用了相同的命名方式。但是為了避免“細枝末節”造成的干擾,我會進行最大限度的裁剪。對於大部分方法,我們只會保留最核心的邏輯。對於一些接口,我們會剔除那些與核心流程無關的成員。在通過這個模擬管道講解HTTP請求的總體處理流程之前,我們先來看看如何在它基礎上開發一個簡單的應用。
我們在這個模擬管道上開發一個簡單的應用來發布圖片。具體的應用場景是這樣:我們將圖片文件保存在服務器上的某個目錄下,客戶端可以通過發送HTTP請求並在請求地址上指定文件名的方式來獲取目標圖片。如下圖所示,我們利用浏覽器向針對某張圖片的地址(“http://localhost:3721/images/hello.png”)發送請求後,獲取到的目標圖片(hello.png)會直接顯示到浏覽器上。除此之外,如果指定的圖片地址沒有包含擴展名(“.png”),我們的也會幫助我們自動匹配一個文件名(不包含擴展名)相同的圖片。
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”)就保存在這個目錄下。
ASP.NET Core請求處理管道由一個服務器和一組有序排列的中間件組合而成。我們可以在這基礎上作進一步個抽象,將後者抽象成一個HttpApplication對象,那麼該管道就成了一個Server和HttpApplication的綜合體(如下圖所示)。Server會將接收到的HTTP請求轉發給HttpApplication對象,後者會針對當前請求創建一個上下文,並在此上下文中處理請求,請求處理完成並完成響應之後HttpApplication會對此上下文實施回收釋放處理。
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和其他與日志相關上下文信息的封裝。
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對象來實現的。
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正是分別根據它們創建的。
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管道中的處理流程[下]:管道如何創建
源代碼下載