本文通過一張GIF動圖來繼續聊一下ASP.NET Core的請求處理管道,從管道的配置、構建以及請求處理流程等方面做一下詳細的研究。(ASP.NET Core系列目錄) 一、概述 上文說到,請求是經過 Server監聽=>處理成httpContext=>Application處理生成Response ...
本文通過一張GIF動圖來繼續聊一下ASP.NET Core的請求處理管道,從管道的配置、構建以及請求處理流程等方面做一下詳細的研究。(ASP.NET Core系列目錄)
一、概述
上文說到,請求是經過 Server監聽=>處理成httpContext=>Application處理生成Response。 這個Application的類型RequestDelegate本質是 public delegate Task RequestDelegate (HttpContext context); ,即接收HttpContext並返回Task, 它是由一個個中間件 Func<RequestDelegate, RequestDelegate> middleware 嵌套在一起構成的。它的構建是由ApplicationBuilder完成的,先來看一下這個ApplicationBuilder:
1 public class ApplicationBuilder : IApplicationBuilder 2 { 3 private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>(); 5 public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware) 6 { 7 _components.Add(middleware); 8 return this; 9 } 11 public RequestDelegate Build() 12 { 13 RequestDelegate app = context => 14 { 15 context.Response.StatusCode = 404; 16 return Task.CompletedTask; 17 }; 19 foreach (var component in _components.Reverse())20 { 21 app = component(app); 22 } 24 return app; 25 } 26 }
ApplicationBuilder有個集合 IList<Func<RequestDelegate, RequestDelegate>> _components 和一個用於向這個集合中添加內容的 Use(Func<RequestDelegate, RequestDelegate> middleware) 方法,通過它們的類型可以看出來它們是用來添加和存儲中間件的。現在說一下大概的流程:
- 調用startupFilters和_startup的Configure方法,調用其中定義的多個UseXXX(進一步調用ApplicationBuilder的Use方法)將一個個中間件middleware按照順序寫入上文的集合_components(記住這個_components)。
- 定義了一個 context.Response.StatusCode = 404 的RequestDelegate。
- 將集合_components顛倒一下, 然後遍歷其中的middleware,一個個的與新創建的404 RequestDelegate 連接在一起,組成一個新的RequestDelegate(即Application)返回。
這個最終返回的RequestDelegate類型的Application就是對HttpContext處理的管道了,這個管道是多個中間件按照一定順序連接在一起組成的,startupFilters先不說,以我們非常熟悉的Startup為例,它的Configure方法預設情況下已經依次進行了UseBrowserLink、UseDeveloperExceptionPage、UseStaticFiles、UseMvc了等方法,請求進入管道後,請求也會按照這個順序來經過各個中間件處理,首先進入UseBrowserLink,然後UseBrowserLink會調用下一個中間件UseDeveloperExceptionPage,依次類推到達UseMVC後被處理生成Response開始逆向返回再依次反向經過這幾個中間件,正常情況下,請求到達MVC中間件後被處理生成Response開始逆向返回,而不會到達最終的404,這個404是為了防止其他層未配置或未能處理的時候的一個保險操作。
胡扯兩句:這個管道就像一座塔,話說唐僧路過金光寺去掃金光塔,從前門進入第一層開始掃,然後從前門的樓梯進入第二層、第三層、第四層,然後從第四層的後門掃下來直至後門出去,卻不想妖怪沒處理好, 被唐僧掃到了第五層(頂層)去,發現佛寶被奔波兒灞和霸波爾奔偷走了,大喊:悟空悟空,佛寶被妖怪偷走啦!(404...)
下麵就以這4個為例通過一個動圖形象的描述一下整個過程:
圖1
一個“中規中矩”的管道就是這樣構建並運行的,通過上圖可以看到各個中間件在Startup文件中的配置順序與最終構成的管道中的順序的關係,下麵我們自己創建幾個中間件體驗一下,然後再看一下不“中規中矩”的長了杈子的管道。
二、自定義中間件
先仿照系統現有的寫一個
public class FloorOneMiddleware { private readonly RequestDelegate _next; public FloorOneMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { Console.WriteLine("FloorOneMiddleware In"); //Do Something //To FloorTwoMiddleware await _next(context); //Do Something Console.WriteLine("FloorOneMiddleware Out"); } }
這是塔的第一層,進入第一層後的 //Do Something 表示在第一層需要做的工作, 然後通過 _next(context) 進入第二層,再下麵的 //Do Something 是從第二層出來後的操作。同樣第二層調用第三層也是一樣。再仿寫個UseFloorOne的擴展方法:
public static class FloorOneMiddlewareExtensions { public static IApplicationBuilder UseFloorOne(this IApplicationBuilder builder) { Console.WriteLine("Use FloorOneMiddleware"); return builder.UseMiddleware<FloorOneMiddleware>(); } }
這樣在Startup的Configure方法中就也可以寫 app.UseFloorOne(); 將這個中間件作為管道的一部分了。
通過上面的例子仿照系統預設的中間件完成了一個簡單的中間件的編寫,這裡也可以用簡要的寫法,直接在Startup的Configure方法中這樣寫:
app.Use(async (context,next) => { Console.WriteLine("FloorThreeMiddleware In"); //Do Something //To FloorThreeMiddleware await next.Invoke(); //Do Something Console.WriteLine("FloorThreeMiddleware Out"); });
同樣可以實現上一種例子的工作,但還是建議按照那樣的寫法,在Startup這裡體現的簡潔並且可讀性好的多。
複製一下第一種和第二種的例子,形成如下代碼:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseFloorOne(); app.UseFloorTwo(); app.Use(async (context,next) => { Console.WriteLine("FloorThreeMiddleware In"); //Do Something //To FloorThreeMiddleware await next.Invoke(); //Do Something Console.WriteLine("FloorThreeMiddleware Out"); }); app.Use(async (context, next) => { Console.WriteLine("FloorFourMiddleware In"); //Do Something await next.Invoke(); //Do Something Console.WriteLine("FloorFourMiddleware Out"); }); if (env.IsDevelopment()) { app.UseBrowserLink(); app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
運行一下看日誌:
1 CoreMiddleware> Use FloorOneMiddleware 2 CoreMiddleware> Use FloorTwoMiddleware 3 CoreMiddleware> Hosting environment: Development 4 CoreMiddleware> Content root path: C:\Users\FlyLolo\Desktop\CoreMiddleware\CoreMiddleware 5 CoreMiddleware> Now listening on: http://localhost:10757 6 CoreMiddleware> Application started. Press Ctrl+C to shut down. 7 CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1] 8 CoreMiddleware> Request starting HTTP/1.1 GET http://localhost:56440/ 9 CoreMiddleware> FloorOneMiddleware In 10 CoreMiddleware> FloorTwoMiddleware In 11 CoreMiddleware> FloorThreeMiddleware In 12 CoreMiddleware> FloorFourMiddleware In 13 CoreMiddleware> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1] 14 CoreMiddleware> Executing action method CoreMiddleware.Controllers.HomeController.Index (CoreMiddleware) with arguments ((null)) - ModelState is Valid 15 CoreMiddleware> info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor[1] 16 CoreMiddleware> Executing ViewResult, running view at path /Views/Home/Index.cshtml. 17 CoreMiddleware> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] 18 CoreMiddleware> Executed action CoreMiddleware.Controllers.HomeController.Index (CoreMiddleware) in 9896.6822ms 19 CoreMiddleware> FloorFourMiddleware Out 20 CoreMiddleware> FloorThreeMiddleware Out 21 CoreMiddleware> FloorTwoMiddleware Out 22 CoreMiddleware> FloorOneMiddleware Out 23 CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] 24 CoreMiddleware> Request finished in 10793.8944ms 200 text/html; charset=utf-8
可以看到,前兩行的Use FloorOneMiddleware和Use FloorTwoMiddleware是將對應的中間件寫入集合_components,而中間件本身並未執行,然後10至12行是依次經過我們自定義的例子的處理,第13-18就是在中間件MVC中的處理了,找到並調用對應的Controller和View,然後才是19-22的逆向返回, 最終Request finished返回狀態200, 這個例子再次驗證了請求在管道中的處理流程。
那麼我們試一下404的情況, 把Configure方法中除了自定義的4個中間件外全部註釋掉,再次運行
1 //上面沒變化 省略 2 CoreMiddleware> FloorOneMiddleware In 3 CoreMiddleware> FloorTwoMiddleware In 4 CoreMiddleware> FloorThreeMiddleware In 5 CoreMiddleware> FloorFourMiddleware In 6 CoreMiddleware> FloorFourMiddleware Out 7 CoreMiddleware> FloorThreeMiddleware Out 8 CoreMiddleware> FloorTwoMiddleware Out 9 CoreMiddleware> FloorOneMiddleware Out 10 CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] 11 CoreMiddleware> Request finished in 218.7216ms 404
可以看到,MVC處理的部分沒有了,因為該中間件已被註釋,而最後一條可以看到系統返回了狀態404。
那麼既然MVC可以正常處理請求沒有進入404, 我們怎麼做可以這樣呢?是不是不調用下一個中間件就可以了? 試著把FloorFour改一下
1 app.Use(async (context, next) => 2 { 3 Console.WriteLine("FloorFourMiddleware In"); 4 //await next.Invoke(); 5 await context.Response.WriteAsync("Danger!"); 6 Console.WriteLine("FloorFourMiddleware Out"); 7 });
再次運行,查看輸出和上文的沒有啥太大改變, 只是最後的404變為了200, 網頁上的“404 找不到。。”也變成了我們要求輸出的"Danger!", 達到了我們想要的效果。
但一般情況下我們不這樣寫,ASP.NET Core 提供了Use、Run和Map三種方法來配置管道。
三、Use、Run和Map
Use上面已經用過就不說了,對於上面的問題, 一般用Run來處理,Run主要用來做為管道的末尾,例如上面的可以改成這樣:
app.Run(async (context) => { await context.Response.WriteAsync("Danger!"); });
因為本身他就是作為管道末尾,也就省略了next參數,雖然用use也可以實現, 但還是建議用Run。
Map:
static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration); pathMatch用於匹配請求的path, 例如“/Home”, 必須以“/”開頭, 判斷path是否是以pathMatch開頭。
若是,則進入 Action<IApplicationBuilder> configuration) , 這個參數是不是長得很像startup的Configure方法? 這就像進入了我們配置的另一個管道,它是一個分支,如下圖
圖2
做個例子:
app.UseFloorOne(); app.Map("/Manager", builder => { builder.Use(async (context, next) => { await next.Invoke(); }); builder.Run(async (context) => { await context.Response.WriteAsync("Manager."); }); }); app.UseFloorTwo();
進入第一層後, 添加了一個Map, 作用是當我們請求 localhost:56440/Manager/index 這樣的地址的時候(是不是有點像Area), 會進入這個Map創建的新分支, 結果也就是頁面顯示"Manager." 不會再進入下麵的FloorTwo。若不是“/Manager”開頭的, 這繼續進入FloorTwo。雖然感覺這個Map靈活了我們的管道配置, 但這個只能匹配path開頭的方法太局限了,不著急, 我們看一下MapWhen。
Map When:
MapWhen方法就是一個靈活版的Map,它將原來的PathMatch替換為一個 Func<HttpContext, bool> predicate ,這下就開放多了,它返回一個bool值,現在舉個慄子隨便改一下
app.MapWhen(context=> {return context.Request.Query.ContainsKey("XX");}, builder => { //...TODO... }
當根據請求的參數是否包含“XX”的時候進入這個分支。
從圖2可知,一旦進入分支,是無法回到原分支的, 如果只是想在某種情況下進入某些中間件,但執行完後還可以繼續後續的中間件怎麼辦呢?對比MapWhen,Use也有個UseWhen。
UseWhen:
它和MapWhen一樣,當滿足條件的時候進入一個分支,在這個分支完成之後再繼續後續的中間件,當然前提是這個分支中沒有Run等短路行為。
app.UseWhen(context=> {return context.Request.Query.ContainsKey("XX");}, builder => { //...TODO... }
四、IStartupFilter
我們只能指定一個Startup類作為啟動類,那麼還能在其他的地方定義管道麽? 文章開始的時候說到,構建管道的時候,會調用startupFilters和_startup的Configure方法,調用其中定義的多個UseXXX方法來將中間件寫入_components。自定義一個StartupFilter,實現IStartupFilter的Configure方法,用法和Startup的Configure類似,不過要記得最後調用 next(app) 。
1 public class TestStartupFilter : IStartupFilter 2 { 3 public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) 4 { 5 return app => { 6 app.Use(async (context, next1) => 7 { 8 Console.WriteLine("filter.Use1.begin"); 9 await next1.Invoke(); 10 Console.WriteLine("filter.Use1.end"); 11 }); 12 next(app); 13 }; 14 } 15 }
在複製一個,去startup的ConfigureServices註冊一下:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddSingleton<IStartupFilter,TestStartupFilter>(); services.AddSingleton<IStartupFilter, TestStartupFilter2>(); }
這樣的配置就生效了,現在剖析一下他的生效機制。回顧一下WebHost的BuildApplication方法:
1 private RequestDelegate BuildApplication() 2 { 3 //....省略 4 var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>(); 5 Action<IApplicationBuilder> configure = _startup.Configure; 6 foreach (var filter in startupFilters.Reverse()) 7 { 8 configure = filter.Configure(configure); 9 } 10 11 configure(builder); 12 13 return builder.Build(); 14 }
仔細看這段代碼,其實這和構建管道的流程非常相似,對比著說一下:
- 首先通過GetService獲取到註冊的IStartupFilter集合startupFilters(類比_components)
- 然後獲取Startup的Configure(類比404的RequestDelegate)
- 翻轉startupFilters,foreach它並且與Startup的Configure鏈接在一起。
- 上文強調要記得最後調用 next(app),這個是不是和 next.Invoke() 類似。
是不是感覺和圖一的翻轉拼接過程非常類似,是不是想到了拼接先後順序的問題。對比著管道構建後中間件的執行順序,體會一下後,這時應該可以想到各個IStartupFilter和Startup的Configure的執行順序了吧。沒錯就是按照依賴註入的順序:TestStartupFilter=>TestStartupFilter2=>Startup。