跟我一起学.NetCore之中间件(Middleware)简介和解析请求管道构建
前言
中間件(Middleware)對于Asp.NetCore項目來說,不能說重要,而是不能缺少,因為Asp.NetCore的請求管道就是通過一系列的中間件組成的;在服務器接收到請求之后,請求會經過請求管道進行相關的過濾或處理;
正文
那中間件是那路大神?
會經常聽說,需要注冊一下中間件,如圖:
所以說,中間件是針對請求進行某種功能需求封裝的組件,而這個組件可以控制是否繼續執行下一個中間件;如上圖中的app.UserStaticFiles()就是注冊靜態文件處理的中間件,在請求管道中就會處理對應的請求,如果沒有靜態文件中間件,那就處理不了靜態文件(如html、css等);這也是Asp.NetCore與Asp.Net不一樣的地方,前者是根據需求添加對應的中間件,而后者是提前就全部準備好了,不管用不用,反正都要路過,這也是Asp.NetCore性能比較好的原因之一;
而對于中間件執行邏輯,官方有一個經典的圖:
如圖所示,請求管道由一個個中間件(Middleware)組成,每個中間件可以在請求和響應中進行相關的邏輯處理,在有需要的情況下,當前的中間件可以不傳遞到下一個中間件,從而實現斷路;如果這個不太好理解,如下圖:
每層外圈代表一個中間件,黑圈代表最終的Action方法,當請求過來時,會依次經過中間件,Action處理完成后,返回響應時也依次經過對應的中間件,而執行的順序如箭頭所示;(這里省去了一些其他邏輯,只說中間件)。
好了好了,理論說不好,擔心把看到的小伙伴繞進去了,就先到這吧,接下來從代碼中看看中間件及請求管道是如何實現的;老規矩,找不到下手的地方,就先找能"摸"的到的地方,這里就先扒靜態文件的中間件:
namespace Microsoft.AspNetCore.Builder {public static class StaticFileExtensions{//?調用就是這個擴展方法public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app){if (app == null){throw new ArgumentNullException(nameof(app));}//?這里調用了?IApplicationBuilder?的擴展方法return app.UseMiddleware<StaticFileMiddleware>();}//?這里省略了兩個重載方法,是可以指定參數的} }UseMiddleware方法實現
// 看著調用的方法 public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args) {// 內部調用了以下方法return app.UseMiddleware(typeof(TMiddleware), args); } //?其實這里是對自定義中間件的注冊,這里可以不用太深入了解 public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args) {if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo())){// IMiddleware doesn't support passing args directly since it's// activated from the containerif (args.Length > 0){throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));}return UseMiddlewareInterface(app, middleware);}// 取得容器var applicationServices = app.ApplicationServices;// 反編譯進行包裝成注冊中間件的樣子(Func<ReuqestDelegate,RequestDelegate>),但可以看到本質使用IApplicationBuilder中Use方法return app.Use(next =>{// 獲取指定類型中的方法列表var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);// 找出名字是Invoke或是InvokeAsync的方法var invokeMethods = methods.Where(m =>string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal)|| string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal)).ToArray();// 如果有多個方法 ,就拋出異常,這里保證方法的唯一if (invokeMethods.Length > 1){throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));}// 如果沒有找到,也就拋出異常if (invokeMethods.Length == 0){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));}// 取得唯一的方法Invoke或是InvokeAsync方法var methodInfo = invokeMethods[0];// 判斷類型是否返回Task,如果不是就拋出異常,要求返回Task的目的是為了后續包裝RequestDelegateif (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));}// 判斷方法的參數,參數的第一個參數必須是HttpContext類型var parameters = methodInfo.GetParameters();if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext)){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));}// 開始構造RequestDelegate對象var ctorArgs = new object[args.Length + 1];ctorArgs[0] = next;Array.Copy(args, 0, ctorArgs, 1, args.Length);var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);// 如果參數只有一個HttpContext 就包裝成一個RequestDelegate返回if (parameters.Length == 1){return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);}// 如果參數有多個的情況就單獨處理,這里不詳細進去了var factory = Compile<object>(methodInfo, parameters);return context =>{var serviceProvider = context.RequestServices ?? applicationServices;if (serviceProvider == null){throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));}return factory(instance, context, serviceProvider);};}); }以上代碼其實現在拿出來有點早了,以上是對自定義中間件的注冊方式,為了扒代碼的邏輯完整,拿出來了;這里可以不用深究里面內容,知道內部調用了IApplicationBuilder的Use方法即可;
由此可見,IApplicationBuilder就是構造請求管道的核心類型,如下:
namespace Microsoft.AspNetCore.Builder {public interface IApplicationBuilder{//?容器,用于依賴注入獲取對象的IServiceProvider ApplicationServices{get;set;}//?屬性集合,用于中間件共享數據IDictionary<string, object> Properties{get;}// 針對服務器的特性IFeatureCollection ServerFeatures{get;}//?構建請求管道RequestDelegate Build();// 克隆實例的IApplicationBuilder?New();// 注冊中間件IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);} }IApplicationBuilder的默認實現就是ApplicationBuilder,走起,一探究竟:
namespace Microsoft.AspNetCore.Builder { // 以下 刪除一些屬性和方法,具體可以私下看具體代碼public class ApplicationBuilder : IApplicationBuilder{// 存儲注冊中間件的鏈表private?readonly?IList<Func<RequestDelegate,?RequestDelegate>>?_components?=?new?List<Func<RequestDelegate,?RequestDelegate>>();// 注冊中間件public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware){//?將中間件加入到鏈表_components.Add(middleware);return this;}// 構造請求管道public RequestDelegate Build(){//?構造一個404的中間件,這就是為什么地址匹配不上時會報404的原因RequestDelegate app = context =>{//?判斷是否有Endpoint中間件var endpoint = context.GetEndpoint();var endpointRequestDelegate = endpoint?.RequestDelegate;if (endpointRequestDelegate != null){var message =$"The request reached the end of the pipeline without executing the endpoint: '{endpoint.DisplayName}'. " +$"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +$"routing.";throw new InvalidOperationException(message);}//?返回404?Codecontext.Response.StatusCode = 404;return Task.CompletedTask;};//?構建管道,首先將注冊的鏈表倒序一把,保證按照注冊順序執行foreach (var component in _components.Reverse()){app = component(app);}// 最終返回return app;}} }在注冊的代碼中,可以看到所謂的中間件就是Func<RequestDelegate, RequestDelegate>,其中RequestDelegate就是一個委托,用于處理請求的,如下:
public delegate Task RequestDelegate(HttpContext context);之所以用Func<RequestDelegate, RequestDelegate>的形式表示中間件,應該就是為了中間件間驅動方便,畢竟中間件不是單獨存在的,是需要多個中間件結合使用的;
那請求管道構造完成了,那請求是如何到管道中呢?
應該都知道,Asp.NetCore內置了IServer,負責監聽對應的請求,當請求過來時,會將請求給IHttpApplication<TContext>進行處理,簡單看一下接口定義:
namespace Microsoft.AspNetCore.Hosting.Server {public interface IHttpApplication<TContext> {//?執行上下文創建TContext CreateContext(IFeatureCollection contextFeatures);// 執行上下文釋放void?DisposeContext(TContext?context,?Exception?exception);//?處理請求,這里就使用了請求管道處理Task ProcessRequestAsync(TContext context);} }而對于IHttpApplication<TContext>類型來說,默認創建的就是HostingApplication,如下:
namespace Microsoft.AspNetCore.Hosting {internal class HostingApplication : IHttpApplication<HostingApplication.Context>{//?構建出來的請求管道private readonly RequestDelegate _application;// 用于創建請求上下文的private readonly IHttpContextFactory _httpContextFactory;private readonly DefaultHttpContextFactory _defaultHttpContextFactory;private HostingApplicationDiagnostics _diagnostics;// 構造函數初始化變量public HostingApplication(RequestDelegate application,ILogger logger,DiagnosticListener diagnosticSource,IHttpContextFactory httpContextFactory){_application = application;_diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource);if (httpContextFactory is DefaultHttpContextFactory factory){_defaultHttpContextFactory = factory;}else{_httpContextFactory = httpContextFactory;}}// 創建對應的請求的上下文public Context CreateContext(IFeatureCollection contextFeatures){Context hostContext;if (contextFeatures is IHostContextContainer<Context> container){hostContext = container.HostContext;if (hostContext is null){hostContext = new Context();container.HostContext = hostContext;}}else{// Server doesn't support pooling, so create a new ContexthostContext = new Context();}HttpContext httpContext;if (_defaultHttpContextFactory != null){var defaultHttpContext = (DefaultHttpContext)hostContext.HttpContext;if (defaultHttpContext is null){httpContext = _defaultHttpContextFactory.Create(contextFeatures);hostContext.HttpContext = httpContext;}else{_defaultHttpContextFactory.Initialize(defaultHttpContext, contextFeatures);httpContext = defaultHttpContext;}}else{httpContext = _httpContextFactory.Create(contextFeatures);hostContext.HttpContext = httpContext;}_diagnostics.BeginRequest(httpContext, hostContext);return hostContext;}//?將創建出來的請求上下文交給請求管道處理public Task ProcessRequestAsync(Context context){// 請求管道處理return _application(context.HttpContext);}//?以下刪除了一些代碼,具體可下面查看....} }這里關于Server監聽到請求及將請求交給中間處理的具體過程沒有具體描述,可以結合啟動流程和以上內容在細扒一下流程吧(大家私下搞吧),這里就簡單說說中間件及請求管道構建的過程;(后續有時間將整體流程走一遍);
總結
這節又是純代碼來“忽悠”小伙伴了,對于理論概念可能表達的不夠清楚,歡迎交流溝通;其實這里只是根據流程走了一遍源碼,并沒有一行行解讀,所以小伙伴看此篇文章代碼部分的時候,以調試的思路去看,從注冊中間件那塊開始,到最后請求交給請求管道處理,注重這個流程即可;
下一節說說中間件的具體應用;
------------------------------------------------
一個被程序搞丑的帥小伙,關注"Code綜藝圈",識別關注跟我一起學~~~
總結
以上是生活随笔為你收集整理的跟我一起学.NetCore之中间件(Middleware)简介和解析请求管道构建的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 中居然也有切片语法糖,太厉害了
- 下一篇: C#刷剑指Offer | 二叉搜索树的后