dotNET Core 3.X 请求处理管道和中间件的理解
理解 dotNET Core 中的管道模型,對我們學習 dotNET Core 有很大的好處,能讓我們知其然,也知其所以然,這樣在使用第三方組件或者自己寫一些擴展時,可以避免入坑,或者說避免同樣的問題多次入坑。
本文分為以下幾個部分來進行介紹:
新老管道模型對比
分析代碼理解請求處理
中間件和過濾器的區別
自定義中間件
新老管道模型對比
我們知道,在 Web 應用中,無論使用什么技術,都是客戶端發送一個請求,服務器端經過一系列的處理后返回結果給客戶端。
(圖1)
在服務器端返回響應前我們的請求都會經過一些列的處理才會產生最終的結果,不管是之前的 dotNET Frameowrk 程序還是現在的 dotNET Core,中間的處理都采用了管道的設計。
ASP.NET 管道
通常,我們會將 ASP.NET 程序部署到 IIS 中,這樣就形成了 IIS 和 ASP.NET 運行時的雙管道模型,大致請求流程如下:
1、程序在 IIS 中運行后,會啟動一個名為 w3wp.exe 的進程,我們進行服務器端 Debug 時就需要附加這個進程;
2、在 w3wp.exe 中利用 aspnet_isapi.dll 加載 .NET 運行時;
3、隨后運行時 IsapiRuntime 會被加載,加載后,會接管整個 HTTP 請求,然后創建一個 IsapiWorkerRequest 對象來包裝 HTTP 請求;
4、包裝好 HTTP 請求后,將 IsapiWorkerRequest 傳遞給 ASP.NET 的 HttpRuntime ,這時請求就進入了 ASP.NET 的管道;
5、HttpRuntime 會根據 IsapiWorkerRequest 對象創建表示當前 HTTP 請求上下文 (Context) 對象 HttpContext;
6、HttpContext 創建后,HttpRuntime 會使用 HttpApplicationFactory 創建當前的 HttpApplication 對象,HttpApplication 對象會有多個,處理完后會被釋放到 HttpApplication 的對象池中;
7、到了 HttpApplication 中之后,就是我們所熟悉的 HttpModule 和 HttpHandler 了,先經過 HttpModule ,比如 ASP.NET 自帶的授權、身份認證、緩存等就是通過 HttpModule 處理,我們也可以自定義自己的 HttpModule ,而具體的 aspx、ascx 等就是由 HttpHandler 處理。
具體的處理流程圖如下:
(圖2)HttpModule 和 HttpHandler 的細化圖如下:
(圖3)dotNET Core 管道
在 dotNET Core 中,HttpModule 和 HttpHandler 已經消失了。取而代之的是 MiddleWare(中間件) 。在 Core 中請求處理管道由一個服務器和一組中間件來組成,服務器默認就是內置的 Kestrel ,官方經典的流程圖如下:
(圖4)
請求經過中間件處理完后,進入下一個中間件,然后按照順序依次返回。相比較原來的 HttpModule ,更簡單和輕量級,而且即便是系統級別的中間件,也是可以由用戶自己選擇使用的,更加靈活,同時也有更好的性能。更多中間件和 HttpModule 的對比可以參考:https://docs.microsoft.com/zh-cn/aspnet/core/migration/http-modules?view=aspnetcore-3.1
分析代碼理解請求處理
控制臺程序
在 Rider 中創建一個 dotNET Core 3.1 的控制臺程序,修改項目文件如下:
<Project?Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>netcoreapp3.1</TargetFramework></PropertyGroup> </Project>控制臺的 Skd 類型為 Microsoft.NET.Sdk ,將其修改為 Microsoft.NET.Sdk.Web 后會自動引用 ASP.NET Core 的相關包。這樣這個控制臺就有了 Web 應用的能力了,在 Program 類添加 using 引用:
using?Microsoft.AspNetCore.Builder; using?Microsoft.AspNetCore.Hosting; using?Microsoft.AspNetCore.Http; using?Microsoft.Extensions.Hosting;Main 函數添加如下代碼:
Host.CreateDefaultBuilder().ConfigureWebHost(builder?=>?builder.Configure(app?=>?app.Run(context?=>?context.Response.WriteAsync("hello?world!"))).UseKestrel().UseUrls("http://localhost:5000")).Build().Run();運行程序,可以看到瀏覽器會顯示 hello world
(圖5)Main 函數中的代碼調用步驟如下:
調用類 Host 的靜態方法 CreateDefaultBuilder 創建一個 IHostBuilder,對象,在 CreateDefaultBuilder 方法中,系統幫我做了很多事情,比如設置根目錄、加載配置文件、配置默認日志框架等;
最終調用 IHostBuilder 的 Build 方法構建一個 IHost,并調用擴展方法 Run;
在上面的 IHostBuilder 構建后,調用 ConfigureWebHost 方法對請求處理管道進行定制,該方法是 IHostBuilder 的一個擴展方法,接收一個 Action
類型的委托,在該方法中,可以注冊服務和使用中間件,比如上面例子中的 app.Run(context => context.Response.WriteAsync("hello world!")) 就是一個簡單的中間件,中間件被注冊到 Configure 方法的參數 Action<IApplicationBuilder>?委托中;隨后調用 UseKestrel 來構建一個 Kestrel 的服務器,調用 UseUrls 方法來設置服務器監聽的端口。
控制臺程序到 Web API 的轉變
如果我們創建的是一個 Web API 項目,在 Program 類中會有一個 CreateHostBuilder 的靜態方法來返回 IHostBuilder 對象:
public?static?IHostBuilder?CreateHostBuilder(string[]?args)?=>Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder??=>{?webBuilder.UseStartup<Startup>();?});上面代碼中調用 webBuilder.UseStartup(); 加載了 Startup 類,Startup 類并沒有繼承任何類,但其實是按照 IStartup 接口的約束來實現的,IStartup 接口代碼如下:
??public?interface?IStartup{IServiceProvider?ConfigureServices(IServiceCollection?services);void?Configure(IApplicationBuilder?app);}ConfigureServices:用來注冊服務;
Configure:用來加載中間件
既然 Configure 方法是用來注冊中間件的,我們修改 Startup 類的 Configure 方法,可以實現和上面的控制臺例子一樣的效果:
public?void?Configure(IApplicationBuilder?app,?IWebHostEnvironment?env) {app.Run(context?=>?context.Response.WriteAsync("hello?world!")); }模擬多個中間件請求
在 Configure 中注冊中間件通常使用 app.Use() 方法,Use 方法接收一個 Func<RequestDelegate, RequestDelegate>?的委托作為參數,這個委托即是我們的中間件,而 RequestDelegate 代表著 HTTP 請求的處理器,在整個請求處理中流轉,RequestDelegate 的參數 HttpContext 包裝了 HttpRequest 和 HttpResponse。
修改 Startup 類的 Configure 方法,代碼如下:
public?void?Configure(IApplicationBuilder?app,?IWebHostEnvironment?env) {app.Use(next?=>{Console.WriteLine("第一個中間件");return?new?RequestDelegate(async?context?=>{await?context.Response.WriteAsync("First?Middleware?Begin?>>>");await?next.Invoke(context);await?context.Response.WriteAsync($"First?Middleware?>>>");});});app.Use(next?=>{Console.WriteLine("第二個中間件");return?new?RequestDelegate(async?context?=>{await?context.Response.WriteAsync("Second?Middleware?Begin?>>>");await?context.Response.WriteAsync("Second?Middleware?End?>>>");});}); }先來看運行結果:
(圖6)(圖7)
從圖6 可以看出注冊中間件的順序和我們代碼的順序是相反的,這個看看 ApplicationBuilder 的源碼就清楚,在 Build 方法中執行時將收集到的所有中間件進行了反轉
(圖8)
從圖7 可以看出,中間件的執行順序是按照注冊的順序一個一個進入,然后傳遞到后面一個中間件,最后一個執行完后原路返回。
中間件和過濾器的區別
我們可以在中間件中進行請求到攔截,做一些自己的處理,或者可以直接中斷請求,同樣 dotNET Core 中的 過濾器(Filter)也可以做同樣的事情,那么兩者有什么區別呢?
在之前的文章 《dotNET Core WebAPI 統一處理(返回值、參數驗證、異常)》 中就是通過過濾器來實現返回值、異常等的統一處理,所以說過濾器跟 Controller 或者 Action 關系更緊密,是整個 MVC 這個中間件的一部分。
而中間件更多是關注業務無關的,比如 Session 存儲、身份認證等。在 Web API 中經常使用 Swagger 來做文檔管理,也是以中間件的方式來使用,添加如下代碼就可以:
app.UseSwagger(); app.UseSwaggerUI(c?=> {c.SwaggerEndpoint("/swagger/v1/swagger.json",?"DotNet?Core?WebAPI文檔"); });自定義中間件
實現自己的中間件,我們可以繼承 IMiddleware 這個接口,可以看看這個接口的代碼,只有一個方法需要實現:
public?interface?IMiddleware {Task?InvokeAsync(HttpContext?context,?RequestDelegate?next); }現在來設定一個使用場景(不一定恰當),來使用自定義的中間件實現:
項目是前后端分離的開發模式;
接口需要只在當前站點中可以使用,脫離站點去調用是不允許的;
假設當前站點為:http://fwhyy.com 。
1、創建 RequestSourceCheckMiddleware 類繼承 IMiddleware ,并實現方法
public?async?Task?InvokeAsync(HttpContext?context,?RequestDelegate?next) {string?urlRef?=?context.Request.Headers["Referer"];if?(string.IsNullOrWhiteSpace(urlRef)?||?!urlRef.Contains("http://fwhyy.com")){context.Response.StatusCode?=?403;?await?Task.CompletedTask;}else{await?next.Invoke(context);} }2、創建擴展方法
public?static?class?RequestSourceCheckMiddlewareExtension {public?static?IApplicationBuilder?UseRequestSourceCheck(this?IApplicationBuilder?app){app.UseMiddleware<RequestSourceCheckMiddleware>();return?app;} }3、在 Starup 類的 Configure 方法中調用擴展方法使用中間件
app.UseRequestSourceCheck();4、調用結果如下
(圖8)
實現中間件,我們也可以不繼承 IMiddleware 接口,按照約束去定義中間件的類一樣可以實現功能,在 dotNET Core 還有很多的地方使用著固有的約定,比如 Starup 類也沒有實現 IStarup 接口,也是一樣的道理。按照約定的方式實現代碼如下:
public?class?RequestSourceCheckMiddlewareNew {private?readonly?RequestDelegate?_next;public?RequestSourceCheckMiddlewareNew(RequestDelegate?next){_next?=?next;}public?async?Task?Invoke(HttpContext?context){string?urlRef?=?context.Request.Headers["Referer"];if?(string.IsNullOrWhiteSpace(urlRef)?||?!urlRef.Contains("http://fwhyy.com")){?context.Response.StatusCode?=?403;?await?Task.CompletedTask;}else{await??_next.Invoke(context);}} }希望本文對您有所幫助,下一篇準備講講 Web API 中 Jwt 的使用。
文中示例代碼:https://github.com/oec2003/DotNetCoreThreeAPIDemo
總結
以上是生活随笔為你收集整理的dotNET Core 3.X 请求处理管道和中间件的理解的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: C#两大知名Redis客户端连接哨兵集群
 - 下一篇: 了解.NET中的垃圾回收