一个迷你ASP.NET Core框架的实现(下)
【框架內幕】|?作者?/ Edison Zhou
這是恰童鞋騷年的第196篇原創文章
上一篇我們了解了AspNetCore.Mini這個項目的背景及項目結構和流程,這一篇我們繼續解析幾個核心對象。本文整理自A大(蔣金楠)的主題分享,點擊本文底部“閱讀原文”可以觀看A大的該主題視頻分享。
1基于HttpListener的WebServer
剛剛在WebHost中注入了Server,并啟動了Server。那么,這個Server長啥樣呢?我們知道,在ASP.NET Core中封裝了Kestrel和IIS兩個Server供我們使用,那么它們肯定有一個抽象層(這里是接口),定義了他們共有的行為,這里我們也寫一個IServer:
public interface IServer {Task RunAsync(RequestDelegate handler); }IServer接口行為很簡單,就是約定一個啟動的方法RunAsync,接受參數是中間件(本質就是一個請求處理的委托)。
有了IServer接口,就可以基于IServer封裝基于不同平臺的WebServer了,這里基于HttpListener實現了一個HttpListenerServer如下(HttpListener簡化了Http協議的監聽,僅需通過字符串的方法提供監聽的地址和端口號以及虛擬路徑,就可以開始監聽請求):
public class HttpListenerServer : IServer {private readonly HttpListener _httpListener;private readonly string[] _urls;public HttpListenerServer(params string[] urls){_httpListener = new HttpListener();// 綁定默認監聽地址(默認端口為5000)_urls = urls.Any() ? urls : new string[] { "http://localhost:5000/" };}public async Task RunAsync(RequestDelegate handler){Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url));if (!_httpListener.IsListening){// 啟動HttpListener_httpListener.Start();}Console.WriteLine("[Info]: Server started and is listening on: {0}", string.Join(";", _urls));while (true){// 等待傳入的請求,該方法將阻塞進程(這里使用了await),直到收到請求var listenerContext = await _httpListener.GetContextAsync();// 打印狀態行: 請求方法, URL, 協議版本Console.WriteLine("{0} {1} HTTP/{2}", listenerContext.Request.HttpMethod, listenerContext.Request.RawUrl, listenerContext.Request.ProtocolVersion);// 獲取抽象封裝后的HttpListenerFeaturevar feature = new HttpListenerFeature(listenerContext);// 獲取封裝后的Feature集合var features = new FeatureCollection().Set<IHttpRequestFeature>(feature).Set<IHttpResponseFeature>(feature);// 創建HttpContextvar httpContext = new HttpContext(features);Console.WriteLine("[Info]: Server process one HTTP request start.");// 開始依次執行中間件await handler(httpContext);Console.WriteLine("[Info]: Server process one HTTP request end.");// 關閉響應listenerContext.Response.Close();}} }/// <summary> /// IWebHostBuilder擴展:使用基于HttpListener的Server /// </summary> public static partial class Extensions {public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder, params string[] urls)=> builder.UseServer(new HttpListenerServer(urls)); }有了Server,也有了中間件,我們要進行處理的上下文在哪里?熟悉ASP.NET請求處理的童鞋都知道,我們會操作一個叫做HttpContext的東西,它包裹了一個HttpRequest和一個HttpResponse,我們要進行的處理操作就是拿到HttpRequest里面的各種參數進行處理,然后將返回的結果包裹或調用HttpResponse的某些方法進行響應返回。在ASP.NET Core Mini中,也不例外,我們會創建一個HttpContext,然后將這個HttpContext傳遞給注冊的中間件,各個中間件也可以拿到這個HttpContext去做具體的處理了。但是,不同的Server和單一的HttpContext之間需要如何適配呢?因為我們可以注冊多樣的Server,可以是IIS也可以是Kestrel還可以是這里的HttpListenerServer。
這時候,我們又可以提取一個抽象層了,如上圖所示,底層是具體的基于不同平臺技術的Server,上層是HttpContext共享上下文,中間層是一個抽象層,它是基于不同Server抽象出來的接口,本質是不同Server的適配器,下面就是這個IFeature的定義:
public interface IHttpRequestFeature {Uri Url { get; }NameValueCollection Headers { get; }Stream Body { get; } }public interface IHttpResponseFeature {int StatusCode { get; set; }NameValueCollection Headers { get; }Stream Body { get; } }這里不再解釋,下面來看看HttpListener的適配的實現:
public class HttpListenerFeature : IHttpRequestFeature, IHttpResponseFeature {private readonly HttpListenerContext _context;public HttpListenerFeature(HttpListenerContext context) => _context = context;Uri IHttpRequestFeature.Url => _context.Request.Url;NameValueCollection IHttpRequestFeature.Headers => _context.Request.Headers;NameValueCollection IHttpResponseFeature.Headers => _context.Response.Headers;Stream IHttpRequestFeature.Body => _context.Request.InputStream;Stream IHttpResponseFeature.Body => _context.Response.OutputStream;int IHttpResponseFeature.StatusCode{get { return _context.Response.StatusCode; }set { _context.Response.StatusCode = value; }} }可以看出,這是一個典型的適配器模式的應用,通過一個抽象層接口,為不同Server提供HttpRequest和HttpResponse對象的核心屬性。
2Middleware與ApplicationBuilder
在啟動項目中,定義了三個中間件如下所示:
public static RequestDelegate FooMiddleware(RequestDelegate next) => async context => {await context.Response.WriteAsync("Foo=>");await next(context); };public static RequestDelegate BarMiddleware(RequestDelegate next) => async context => {await context.Response.WriteAsync("Bar=>");await next(context); };public static RequestDelegate BazMiddleware(RequestDelegate next) => context => context.Response.WriteAsync("Baz");可以看到,每個中間件的作用都很簡單,就是向響應流中輸出一個字符串。其中Foo和Bar兩個中間件在輸出之后,還會調用下一個中間件進行處理,而Baz不會調用下一個中間件進行處理,因此Baz在注冊順序上排在了最后,這也解釋了我們為何在ASP.NET Core中進行中間件的注冊時,注冊的順序比較講究,因為這會影響到后面的執行順序。
剛剛在進行WebHost的創建時,調用了WebHostBuilder的Configure方法進行中間件的注冊,而這個Configure方法的輸入參數是一個IApplicationBuilder的委托:
public interface IApplicationBuilder {IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);RequestDelegate Build(); }可能直接看這個接口定義不是太明白,下面來看看ApplicationBuilder的實現:
public class ApplicationBuilder : IApplicationBuilder {private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares = new List<Func<RequestDelegate, RequestDelegate>>();/// <summary>/// 構建請求處理管道/// </summary>/// <returns>RequestDelegate</returns>public RequestDelegate Build(){_middlewares.Reverse(); // 倒置注冊中間件集合的順序return httpContext =>{// 注冊默認中間件 => 返回404響應RequestDelegate next = _ => {_.Response.StatusCode = 404;return Task.CompletedTask;};// 構建中間件處理管道foreach (var middleware in _middlewares){next = middleware(next);}return next(httpContext);};}/// <summary>/// 注冊中間件/// </summary>/// <param name="middleware">中間件</param>/// <returns>ApplicationBuilder</returns>public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware){_middlewares.Add(middleware);return this;} }其中,Use方法的作用就是接受中間件進行注冊,Build方法的作用就是構建由注冊中間件組成的請求處理管道,而Server加上這個由中間件組成的請求處理管道便是ASP.NET Core的核心內容。因此,我們可以說ASP.NET Core Pipeline = Server + Middlewares。此外,我們還可以將多個Middleware構建成一個單一的“HttpHandler”,那么整個ASP.NET Core框架將具有更加簡單的表達:Pipeline = Server + HttpHandler
因此,這里的Build方法中做了以下幾件事情:
(1)倒置注冊中間件集合的順序
_middlewares.Reverse();為什么要倒置順序呢?不是說執行順序要跟注冊順序保持一致么?別急,且看后面的代碼。
(2)注冊默認中間件
return httpContext => {// 注冊默認中間件 => 返回404響應RequestDelegate next = _ => {_.Response.StatusCode = 404;return Task.CompletedTask;};...... }這里默認中間件是返回404,在如果沒有手動注冊任何中間件的情況下生效。
(3)構建一個中間件處理管道 => "HttpHandler"
public RequestDelegate Build() {......return httpContext =>{......// 構建中間件處理管道foreach (var middleware in _middlewares){next = middleware(next);}return next(httpContext);}; }在通過Use方法注冊多個中間件到middlewares集合中后,會在這里通過一個遍歷組成一個單一的middleware(在這里表示為一個RequestDelegate對象),如下圖所示。
對于middleware,它在這里是一個Func<RequestDeletegate, RequestDelegate>對象,它的輸入和輸出都是RequestDelegate。
對于管道的中的某一個middleware來說,由后續middleware組成的管道體現為一個RequestDelegate對象,由于當前middleware在完成了自身的請求處理任務之后,往往需要將請求分發給后續middleware進行處理,所以它需要將由后續中間件構成的RequestDelegate作為輸入。當代表中間件的委托對象執行之后,我們希望的是將當前中間件“納入”這個管道,那么新的管道體現的RequestDelegate自然成為了輸出結果。
因此,這里也就解釋了為什么要在第一步中進行middleware的順序的倒置,否則無法以注冊的順序構成一個單一的middleware,下圖是示例代碼中的所有middleware構成的一個單一的RequestDelegate,經過層層包裹,以達到依次執行各個middleware的效果。需要注意的就是在BazMiddleware中,沒有調用下一個中間件,因此404中間件便不會得到觸發處理的機會。
下圖是最后的執行結果:
3小結
經過蔣金楠老師的講解以及自己的學習,對這個Mini版的ASP.NET Core框架有了一個初步的理解,正如蔣老師所說,ASP.NET Core的核心就在于由一個服務器和若干中間件構成的管道,了解了這一點,就對ASP.NET Core的核心本質有了大概印象。當然,這個Mini版的ASP.NET Core只是模擬了ASP.NET Core的冰山一角,還有許多的特性都沒有,比如基于Starup來注冊中間件,依賴注入框架,配置系統,預定義中間件等等等等,但是對于廣大ASP.NET Core學習者來說是個絕佳的入門,是進階學習ASP.NET Core源碼的基石讀物,最后感謝大內老A的分享!
最后讓我們一起期待A大的《ASP.NET Core框架揭秘》早日出版!
4參考資料
蔣金楠,《200行代碼,7個對象—讓你了解ASP.NET Core框架的本質》
蔣金楠,《Inside ASP.NET Core Framework》
Edison Zhou,https://github.com/EdisonChou/AspNetCore.Mini
????點擊閱讀原文,觀看A大分享
總結
以上是生活随笔為你收集整理的一个迷你ASP.NET Core框架的实现(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Abp vNext 源码分析] - 1
- 下一篇: HttpClientFactory的套路