跟我一起學.NetCore之中間件(Middleware)簡介和解析請求管道構建


前言

中間件(Middleware)對於Asp.NetCore項目來說,不能說重要,而是不能缺少,因為Asp.NetCore的請求管道就是通過一系列的中間件組成的;在服務器接收到請求之后,請求會經過請求管道進行相關的過濾或處理;

正文

那中間件是那路大神?

會經常聽說,需要注冊一下中間件,如圖:

img

所以說,中間件是針對請求進行某種功能需求封裝的組件,而這個組件可以控制是否繼續執行下一個中間件;如上圖中的app.UserStaticFiles()就是注冊靜態文件處理的中間件,在請求管道中就會處理對應的請求,如果沒有靜態文件中間件,那就處理不了靜態文件(如html、css等);這也是Asp.NetCore與Asp.Net不一樣的地方,前者是根據需求添加對應的中間件,而后者是提前就全部准備好了,不管用不用,反正都要路過,這也是Asp.NetCore性能比較好的原因之一;

而對於中間件執行邏輯,官方有一個經典的圖:

img

如圖所示,請求管道由一個個中間件(Middleware)組成,每個中間件可以在請求和響應中進行相關的邏輯處理,在有需要的情況下,當前的中間件可以不傳遞到下一個中間件,從而實現斷路;如果這個不太好理解,如下圖:

img

每層外圈代表一個中間件,黑圈代表最終的Action方法,當請求過來時,會依次經過中間件,Action處理完成后,返回響應時也依次經過對應的中間件,而執行的順序如箭頭所示;(這里省去了一些其他邏輯,只說中間件)。

好了好了,理論說不好,擔心把看到的小伙伴繞進去了,就先到這吧,接下來從代碼中看看中間件及請求管道是如何實現的;老規矩,找不到下手的地方,就先找能"摸"的到的地方,這里就先扒靜態文件的中間件:

img

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 container
        if (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的目的是為了后續包裝RequestDelegate
        if (!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 Code
                context.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(如Kestrel),負責監聽對應的請求,當請求過來時,會將請求給IHttpApplication 進行處理,簡單看一下接口定義:

namespace Microsoft.AspNetCore.Hosting.Server
{
    public interface IHttpApplication<TContext>
{
        // 執行上下文創建
        TContext CreateContext(IFeatureCollection contextFeatures);
        // 執行上下文釋放
        void DisposeContext(TContext context, Exception exception);
        // 處理請求,這里就使用了請求管道處理
        Task ProcessRequestAsync(TContext context);
    }
}

而對於IHttpApplication 類型來說,默認創建的就是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 Context
                hostContext = 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綜藝圈",識別關注跟我一起學~~~

image-20200903102501781


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM