老A說的一句話讓我很受啟發,想要深入了解框架,你要把精力聚焦在 架構設計的層面來思考問題。而透徹了解底層原理,最好的笨辦法就是根據原理對框架核心進行 重建或者說 再造。看起來沒有捷徑,也是最快的捷徑。
題外話
相信很多讀者已經看過老A寫的這篇文章《200行代碼,7個對象——讓你了解ASP.NET Core框架的本質》,這是一篇模仿和重建的典范。重建說白了就是模仿,模仿有一個前置條件就是你對底層原理要爛熟於心。否則畫虎難畫骨,原本要畫虎,最后出來的是只貓。
要理解原理就要去閱讀源碼,就像新人學開車,如何使用尚且磕磕碰碰,更何況讓你去了解汽車的構造和引擎。
所以老A是引路人,我像個門外漢一樣對前輩的文章解讀不下5遍。我有幾個疑問,1.為什么是7個對象?2.這些對象如何分類,如何排序?3.這些對象發明的那個“無”是什么?
在我深入學習和解讀的時候,我越加感覺到老A的這篇文章很值得去深入解讀,所謂知其然,知其所以然,這樣在編碼過程才會游刃有余,以下開始我個人的解讀。
知識准備
- 委托
- 構建模式
- 適配器模式
引子
public class Program { public static void Main() => new WebHostBuilder() .UseKestrel() .Configure(app => app.Use(context => context.Response.WriteAsync("Hello World!"))) .Build() .Run(); }
以上是原文的代碼,我們可以看到WebHostBuilder、Server(即Kestrel)、ApplicationBuilder(即app)三大重要的對象,如下圖所示:
WebHostBuilder這個父親生出WebHost這個孩子,WebHost又生成整個ASP.NET Core最核心的內容,即由Server和中間件(Middleware)構成的管道Pipeline。我們看下Pipeline的放大圖:
繼續把Pipeline拆開,有個很重要的ApplicationBuilder對象,里面包含Middleware、RequestDelegate。至於HttpContext是獨立共享的對象,貫穿在整個管道中間,至此7大對象全部出場完畢。
Configure是個什么玩意?看下代碼:
public IWebHostBuilder Configure(Action<IApplicationBuilder> configure) { _configures.Add(configure); return this; }
我們看到他是一個接受IApplicationBuilder的委托!繼續刨根問底,IApplicationBuilder是什么玩意?看下源碼:
public interface IApplicationBuilder { IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware); RequestDelegate Build(); }
他是一個注冊中間件和生成Application的容器,那么Application是什么呢?源碼沒有這個對象,但是看代碼(如下所示)我們可以知道他是真正的委托執行者(Handler),執行是一個動作可以理解為app,我猜想這是取名為ApplicationBuilder的原因。
public RequestDelegate Build() { _middlewares.Reverse(); return httpContext => { //_是一個有效的標識符,因此它可以用作參數名稱 RequestDelegate next = _ => { _.Response.StatusCode = 404; return Task.CompletedTask; }; foreach (var middleware in _middlewares) { next = middleware(next); } return next(httpContext); }; }
更詳細的過程可以參考下面這張圖(圖片來源),
WebHostBuilder開始Build的那一刻開始,WebHost被構造,Server被指定,Middlewares被指定,等WebHost真正啟動的時候,Server開始監聽,收到請求后,Middleware開始執行。
到此,一個完整的ASP.NET Core的流程就簡單的走完了。接下來,我們跟着老A一個一個對象的詳細介紹。
7個對象解讀
1.HttpContext
這個對象應該是最容易理解的,也是我們在編程時候遇到的最多的,最重要的(沒有之一)對象。請看這個對象的簡要代碼:
public class HttpContext { public HttpRequest Request { get; } public HttpResponse Response { get; } } public class HttpRequest { public Uri Url { get; } public NameValueCollection Headers { get; } public Stream Body { get; } } public class HttpResponse { public NameValueCollection Headers { get; } public Stream Body { get; } public int StatusCode { get; set;} }
我們知道一個Http事務包括最核心的Request(輸入)和Response(輸出),所以HttpContext包含這兩個核心的東西。
老A建議大家從管道的角度來理解該對象的作用,管道和HTTP請求流程一脈相承。在Server接收到請求后,HttpContext被創建。
在服務器和中間件,中間件之間通過什么來傳遞信息?就是共享上下文,這個上下文就是HttpContext。可以說HttpContext是根據HTTP請求原理包裹的在管道之間的共享的一個上下文對象。
為什么這里要把HttpContext放在第一個來介紹,因為這是一個最基礎的對象。這7大對象的講解順序,我感覺是從底層基礎開始講起,再層層往上,最后到WebHostBuilder。
2.RequestDelegate
這個委托太重要了,和HttpContext一樣,老A建議大家從管道的角度來理解這個委托。我們再復習一下管道的含義,如圖所示:
這里的管道:Pipeline = Server + Middlewares
還能更簡單一點嗎?可以的:如下圖所示
這里的管道:Pipeline =Server + HttpHandler。
多個Middlewares構成一個HttpHandler對象,這是整個管道的核心,那么應該如何用代碼來表示呢?
老A講到:“既然針對當前請求的所有輸入和輸出都通過HttpContext來表示,那么HttpHandler就可以表示成一個Action<HttpContext>對象”。
但是由於ASP.NET Core推崇異步編程,所以你應該想得到Task對象,那么HttpHandler自然就可以表示為一個Func<HttpContext,Task>對象。由於這個委托對象實在太重要了,所以我們將它定義成一個獨立的類型。下圖展示的就是整個RequestDelegate的設計思路
public delegate Task RequestDelegate(HttpContext context);
這就是委托的由來!
- 為什么是委托,而不是別的函數?
委托是架構設計的底層技術,非常常見。因為委托可以承載約定的函數,遵循開閉原則,能很好的把擴展對外進行開放,保證了底層架構的穩定性。
3.Middleware
這個對象比較費解。根據源碼我們知道Middleware也是一個委托對象(代碼如下所示),中間件其實就是一個Func<RequestDelegate, RequestDelegate>對象:
private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares = new List<Func<RequestDelegate, RequestDelegate>>();
該對象的輸入和輸入都是RequestDelegate,為什么要這么設計呢?我們想一下,當前中間件處理完成后需要將請求分發給后續中間件進行處理,他如何讓后續的中間件參與當前的請求呢?所以他必須要拿到代表后續中間件管道構成的那個Handler。
如下圖所示,也就是說,后續三個中間件構成的管道就是一個輸入,執行完畢后,當前中間件也將被“融入”這個管道(此時該新管道就會由四個中間件構成的一個委托鏈),然后再輸出給你由所有的中間件構成的新管道。如下圖所示:
4.ApplicationBuilder
這又是一個builder,可見builder模式在ASP.NET Core有非常廣泛的應用。但是該Builder構建的不是Application,到構建什么內容呢?從下面代碼聲明我們可以看到他有兩個功能。
從Use的使用來看,第一個功能是注冊器,他把一個個中間件串聯成一個管道。
public interface IApplicationBuilder { IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware); RequestDelegate Build(); }
第二個功能是Build,如下所示:
public class ApplicationBuilder : IApplicationBuilder { private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares = new List<Func<RequestDelegate, RequestDelegate>>(); public RequestDelegate Build() { _middlewares.Reverse(); return httpContext => { RequestDelegate next = _ => { _.Response.StatusCode = 404; return Task.CompletedTask; }; foreach (var middleware in _middlewares) { next = middleware(next); } return next(httpContext); }; } public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware) { _middlewares.Add(middleware); return this; } }
Build真正做的事情是循環組裝中間件,最后把組裝好的委托鏈進行返回。從_middlewares.Reverse();我們又可以知道,對於委托鏈來說,中間件的注冊順序和執行順序是相反的,這里需要進行反轉,然后才能保證先注冊的中間件先執行。
5.Server
Server對象相對比較簡單,我們看下他的接口定義:
public interface IServer { Task StartAsync(RequestDelegate handler); }
我們可以看到Server有個啟動函數StartAsync,StartAsync內部封裝了RequestDelegate中間件,同時內部也會new一個HttpContext(features),這樣Server、RequestDelegate、HttpContext三者就全部聚齊了。
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 StartAsync(RequestDelegate handler) { Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url)); _httpListener.Start(); Console.WriteLine("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); // 獲取抽象封裝后的HttpListenerFeature var feature = new HttpListenerFeature(listenerContext); // 獲取封裝后的Feature集合 var features = new FeatureCollection() .Set<IHttpRequestFeature>(feature) .Set<IHttpResponseFeature>(feature); // 創建HttpContext var 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(); } } } public static partial class Extensions { public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder, params string[] urls) => builder.UseServer(new HttpListenerServer(urls));
通過以上代碼分析,我們可以畫個圖做總結:
- 適配器模式
由於ASP.NET Core可以支持不同的WebServer,比如Kestrel和IIS,不同的WebServer返回的HttpContext各不相同,所以這里又增加了一個中間層進行適配。這個中間層是什么呢?如下圖所示,就是IRequestFeature和IResponseFeature。這一層是典型的適配器模式。
這里重點講解的7大對象,這個適配器模式的實現細節暫且略過。
6.WebHost
public interface IWebHost { Task StartAsync(); }
根據這段定義,我們只能知道簡單知道WebHost只要是用來啟動什么對象用的,具體什么對象似乎都可以。直到我們看了實現,如下代碼所示:
public class WebHost : IWebHost { private readonly IServer _server; private readonly RequestDelegate _handler; public WebHost(IServer server, RequestDelegate handler) { _server = server; _handler = handler; } public Task StartAsync() => _server.StartAsync(_handler); }
通過StartAsync,我們知道WebHost是用來啟動管道的中間件的,管道是在作為應用宿主的WebHost對象啟動的時候被構建出來的。
而WebHost是如何被創建的呢?接下來就要講他的父親WebHostBuilder
7.WebHostBuilder
public class WebHostBuilder : IWebHostBuilder { private IServer _server; private readonly List<Action<IApplicationBuilder>> _configures = new List<Action<IApplicationBuilder>>(); public IWebHostBuilder Configure(Action<IApplicationBuilder> configure) { _configures.Add(configure); return this; } public IWebHostBuilder UseServer(IServer server) { _server = server; return this; } public IWebHost Build() { var builder = new ApplicationBuilder(); foreach (var configure in _configures) { configure(builder); } return new WebHost(_server, builder.Build()); } }
我們看到該對象有個Build方法,內部返回一個WebHost對象,這就是父親的職責,負責生娃,他的娃就是WebHost。生出來的時候,給孩子一個ApplicationBuilder作為食物。而這個食物其實是包裹起來的,展開來看就是一個個RequestDelegate委托鏈。
public interface IWebHostBuilder { IWebHostBuilder UseServer(IServer server); IWebHostBuilder Configure(Action<IApplicationBuilder> configure); IWebHost Build(); }
父親除了創建WebHost之外,他還提供了注冊服務器的UseServer方法和用來注冊中間件的Configure方法。說到Configure方法,我們一定還記得ApplicationBuilder方法的Use也是一個注冊器。這兩個注冊器有何不同呢?我們對比一下代碼:
- WebHostBuilder:
public IWebHostBuilder Configure(Action<IApplicationBuilder> configure) { _configures.Add(configure); return this; }
- ApplicationBuilder:
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware) { _middlewares.Add(middleware); return this; }
其中Use只是增加一個中間件,Configure輸入的是中間件構成的委托鏈。我們看下入口函數的代碼就知道了:
public static async Task Main() { await new WebHostBuilder() .UseHttpListener() .Configure(app => app .Use(FooMiddleware) .Use(BarMiddleware) .Use(BazMiddleware)) .Build() .StartAsync(); }