1. 程式人生 > >ASP.NET Core 2.0 : 八.圖說管道,唐僧掃塔的故事

ASP.NET Core 2.0 : 八.圖說管道,唐僧掃塔的故事

原文: ASP.NET Core 2.0 : 八.圖說管道,唐僧掃塔的故事

  本文通過一張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) 方法,通過它們的型別可以看出來它們是用來新增和儲存中介軟體的。現在說一下大概的流程:

  1. 呼叫startupFilters和_startup的Configure方法,呼叫其中定義的多個UseXXX(進一步呼叫ApplicationBuilder的Use方法)將一個個中介軟體middleware按照順序寫入上文的集合_components(記住這個_components)
  2. 定義了一個 context.Response.StatusCode = 404 的RequestDelegate。
  3. 將集合_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 }

  仔細看這段程式碼,其實這和構建管道的流程非常相似,對比著說一下:

  1. 首先通過GetService獲取到註冊的IStartupFilter集合startupFilters(類比_components)
  2. 然後獲取Startup的Configure(類比404的RequestDelegate)
  3. 翻轉startupFilters,foreach它並且與Startup的Configure連結在一起。
  4. 上文強調要記得最後呼叫 next(app),這個是不是和 next.Invoke() 類似。

  是不是感覺和圖一的翻轉拼接過程非常類似,是不是想到了拼接先後順序的問題。對比著管道構建後中介軟體的執行順序,體會一下後,這時應該可以想到各個IStartupFilter和Startup的Configure的執行順序了吧。沒錯就是按照依賴注入的順序:TestStartupFilter=>TestStartupFilter2=>Startup。