1. 程式人生 > >.net core 中間件管道底層剖析

.net core 中間件管道底層剖析

應用程序 管道 zh-cn nss 變量 enc mage typeof nvi

.net core 管道(Pipeline)是什麽?

技術分享圖片

由上圖可以看出,.net core 管道是請求抵達服務器到響應結果返回的中間的一系列的處理過程,如果我們簡化一下成下圖來看的話,.net core 的管道其實就是中間件的部分。微軟中間件文檔

技術分享圖片

為什麽管道就是中間件的部分了呢?我是這麽理解的,.net core 是通過Startup 類配置服務和應用的請求管道,所以狹義點來講這個管道就是指的請求管道,就是我們今天要理解的中間件管道。

.net core 核心體系結構的特點就是一個中間件系統,它是處理請求和響應的代碼段。中間件彼此鏈接,形成一個管道。傳入的請求通過管道傳遞,其中每個中間件都有機會在將它們傳遞到下一個中間件之前對它們進行處理。傳出響應也以相反的順序通過管道傳遞。

PS:簡單來講就是請求開始到響應結束的中間的一大部分。你可以理解成 " 汽車銷售 " (開始買車到提車的過程,但願不會坐在奔馳車蓋上哭),哈哈……

還有我們來看看,為什麽我們要簡化來看,在運行時 .net core 會預先註入一些必要的服務及依賴項,默認註入(ServiceCollection)的服務清單如下:

技術分享圖片

技術分享圖片

技術分享圖片

技術分享圖片

我們先斷章取義地看,這裏面有 Kestrel 處理請求,將接收到的請求內容(字符串流)轉化成結構化的數據(HttpContext)供後面的中間件使用的服務。欸,服務喲。那其實也就是 Kestrel 服務也是中間件嘛。

而第一張圖中的MVC本身也作為中間件來實現的。

還有一些相關的服務都可以看看上面截圖的服務,而且旁邊標註的生命周期的類型就是之前所講到的 .net core 的三種註入模式 。

那麽從程序入口來講,過程是怎麽樣的呢?

從應用程序主入口 Main() --> WebHost --> UseStartup

/// <summary>
/// Specify the startup type to be used by the web host.
/// </summary>
/// <param name="hostBuilder">The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" />
to configure.</param> /// <param name="startupType">The <see cref="T:System.Type" /> to be used.</param> /// <returns>The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" />.</returns> public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType) { string name = startupType.GetTypeInfo().Assembly.GetName().Name; return hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, name).ConfigureServices((Action<IServiceCollection>)delegate(IServiceCollection services) { if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo())) { ServiceCollectionServiceExtensions.AddSingleton(services, typeof(IStartup), startupType); } else { ServiceCollectionServiceExtensions.AddSingleton(services, typeof(IStartup), (Func<IServiceProvider, object>)delegate(IServiceProvider sp) { IHostingEnvironment requiredService = ServiceProviderServiceExtensions.GetRequiredService<IHostingEnvironment>(sp); return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, requiredService.get_EnvironmentName())); }); } }); }

上面的代碼就可以解釋說,會預先註入的必要的服務,在通過委托的方式,註入 Startup 裏的服務。具體可以繼續探究:

UseSetting:Add or replace a setting in the configuration.
/// <summary>
/// Add or replace a setting in the configuration.
/// </summary>
/// <param name="key">The key of the setting to add or replace.</param>
/// <param name="value">The value of the setting to add or replace.</param>
/// <returns>The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" />.</returns>
public IWebHostBuilder UseSetting(string key, string value)
{
    _config.set_Item(key, value);
    return this;
}
ConfigureServices:Adds a delegate for configuring additional services for the host or web application. This may be called multiple times.
/// <summary>
/// Adds a delegate for configuring additional services for the host or web application. This may be called
/// multiple times.
/// </summary>
/// <param name="configureServices">A delegate for configuring the <see cref="T:Microsoft.Extensions.DependencyInjection.IServiceCollection" />.</param>
/// <returns>The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" />.</returns>
public IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices)
{
    if (configureServices == null)
    {
        throw new ArgumentNullException("configureServices");
    }
    return ConfigureServices(delegate(WebHostBuilderContext _, IServiceCollection services)
    {
        configureServices(services);
    });
}

ConventionBasedStartup

public class ConventionBasedStartup : IStartup
{
    private readonly StartupMethods _methods;

    public ConventionBasedStartup(StartupMethods methods)
    {
        _methods = methods;
    }

    public void Configure(IApplicationBuilder app)
    {
        try
        {
            _methods.ConfigureDelegate(app);
        }
        catch (Exception ex)
        {
            if (ex is TargetInvocationException)
            {
                ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
            }
            throw;
        }
    }

    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        try
        {
            return _methods.ConfigureServicesDelegate(services);
        }
        catch (Exception ex)
        {
            if (ex is TargetInvocationException)
            {
                ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
            }
            throw;
        }
    }
}

OK,到這裏就已經確定了,我們可控的是通過Startup註入我們所需的服務,就是Startup註入的中間件可以做所有的事情,如處理認證,錯誤,靜態文件等等,並且如上面所說的 MVC 在 .net core 也是作為中間件實現的。

那麽 .net core 給我們內置了多少中間件呢?如下圖:

技術分享圖片

我們很經常用到的內置中間件有:

    app.UseExceptionHandler(); //異常處理
    app.UseStaticFiles(); //靜態文件
    app.UseAuthentication(); //Auth驗證
    app.UseMvc(); //MVC

我們知道可以在啟動類的 Configure 方法中配置 .net core 管道,通過調用 IApplicationBuilder 上的 Use*** 方法,就可以向管道添加一個中間件,被添加的順序決定了請求遍歷它們的順序。因此,如上面添加內置中間件的順序,傳入的請求將首先遍歷異常處理程序中間件,然後是靜態文件中間件,然後是身份驗證中間件,最終將由MVC中間件處理。

Use*** 方法實際上只是 .net core 提供給我們的“快捷方式”,以便更容易地構建管道。在幕後,它們最終都使用(直接或間接)這些關鍵字:Use 和 Run 。兩者都向管道中添加了一個中間件,不同之處在於Run添加了一個終端中間件,即管道中的最後一個中間件。

那麽有內置,就應該可以定制的。如何定制自己的中間件呢?上一些簡陋的demo演示一下:

(1)無分支管道

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{

       // Middleware A
       app.Use(async (context, next) =>
       {
            Console.WriteLine("A (before)");
            await next();
            Console.WriteLine("A (after)");
       });

       // Middleware B
       app.Use(async (context, next) =>
       {
            Console.WriteLine("B (before)");
            await next();
            Console.WriteLine("B (after)");
       });

       // Middleware C (terminal)
       app.Run(async context =>
       {
            Console.WriteLine("C");
            await context.Response.WriteAsync("Hello world");
       });

}

打印結果:

A (before)
B (before)
C
B (after)
A (after)

那用管道圖展示的話就是:

技術分享圖片

(2)有分支管道,當使用無分支的管道時,相當於就是一條線直走到底再返回響應結果。但一般情況下,我們都希望管道更具靈活性。創建有分支的管道就需要使用到 Map 擴展用作約定來創建管道分支。Map 是基於給定請求路徑的匹配項來創建請求管道分支的,如果請求路徑以給定的路徑開頭,就執行分支。那麽就有兩種類型有分支的管道:

1.無連結分支,上官方demo:

public class Startup
{
    private static void HandleMapTest1(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 1");
        });
    }

    private static void HandleMapTest2(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 2");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1", HandleMapTest1);

        app.Map("/map2", HandleMapTest2);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

結果:

技術分享圖片

以上無連結分支很容易就理解了,就是不同的路徑跑不同的分支。如果是有參數匹配的話,就要使用 MapWhen,而 MapWhen 基於給定謂詞的結果創建請求管道分支。Func<HttpContext, bool> 類型的任何謂詞均可用於將請求映射到管道的新分支。 謂詞用於檢測查詢字符串變量 branch 是否存在。

2.有連結(重新連接上主管道)分支,創建有連結分支管道就要使用到 UseWhen,上demo:

public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        Console.WriteLine("A (before)");
        await next();
        Console.WriteLine("A (after)");
    });

    app.UseWhen(
        context => context.Request.Path.StartsWithSegments(new PathString("/foo")),
        a => a.Use(async (context, next) =>
        {
            Console.WriteLine("B (before)");
            await next();
            Console.WriteLine("B (after)");
        }));

    app.Run(async context =>
    {
        Console.WriteLine("C");
        await context.Response.WriteAsync("Hello world");
    });
}

像上面的代碼,當請求不是以 " /foo " 開頭的時候,結果為:

A (before)
C
A (after)

當請求是以 " /foo " 開頭的時候,結果為:

A (before)
B (before)
C
B (after)
A (after)

正如您所看到的,中間件管道背後的思想非常簡單,但是非常強大。大多數功能都是 .net core(身份驗證、靜態文件、緩存、MVC等)作為中間件實現。當然,編寫自己的代碼也很容易!

後面可以進階寫自己的中間件,官方文檔。

.net core 中間件管道底層剖析