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

Code綜藝圈發表於2020-09-03

前言

中介軟體(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

相關文章