天天看點

【.NET源碼解讀】深入剖析中間件的設計與實作(一)

作者:DotNET技術圈

.NET本身就是一個基于中間件(middleware)的架構,它通過一系列的中間件元件來處理HTTP請求和響應。在之前的文章《.NET源碼解讀kestrel伺服器及建立HttpContext對象流程》中,已經通過源碼介紹了如何将HTTP資料包轉換為.NET的HttpContext對象。接下來,讓我們深入了解一下.NET是如何設計中間件來處理HttpContext對象。

通過本文,您可以了解以下内容:

  • 認識中間件的本質
  • 實作自定義中間件
  • 源碼解讀中間件原理

一、重新認識中間件

1. 中間件的實作方式

在介紹中間件之前,讓我們先了解一下管道設計模式:

管道設計模式是一種常見的軟體設計模式,用于将一個複雜的任務或操作分解為一系列獨立的處理步驟。每個步驟按特定順序處理資料并傳遞給下一個步驟,形成線性的處理流程。每個步驟都是獨立且可重用的元件。

在.NET中,針對每個HTTP請求的處理和響應任務被分解為可重用的類或匿名方法,這些元件被稱為中間件。中間件的連接配接順序是特定的,它們在一個管道中按順序連接配接起來,形成一個處理流程。這種設計方式可以根據需求自由地添加、删除或重新排序中間件。

中間件的實作非常簡單,它基于一個委托,接受一個HttpContext對象和一個回調函數(表示下一個中間件)作為參數。當請求到達時,委托執行自己的邏輯,并将請求傳遞給下一個中間件元件。這個過程會持續進行,直到最後一個中間件完成響應并将結果傳回給用戶端。

/*              * 入參1 string:代表HttpContext              * 入參2 Func<Task>:下一個中間件的方法              * 結果傳回 Task:避免線程阻塞              * **/              Func<string, Func<Task>, Task> middleware = async (context, next) =>              {              Console.WriteLine($"Before middleware: {context}");                  await next(); // 調用下一個中間件                  Console.WriteLine($"After middleware: {context}");              };           
Func<Task> finalMiddleware = () =>              {              // 最後一個中間件的邏輯              Console.WriteLine("Final middleware");              return Task.CompletedTask;              };           
為了給所有的中間件和終端處理器提供統一的委托類型,使得它們在請求處理管道中可以無縫地連接配接起來。是以引入了RequestDelegate委托。上文中Func方法,最終都會轉換成RequestDelegate委托,這一點放在下文源碼解析中。
public delegate Task RequestDelegate(HttpContext context);           

2. 中間件管道建構器原理

下面是從源碼中提取出的一個簡單的中間件管道建構器實作示例。它包含一個 _middlewares 清單,用于存儲中間件委托,并提供了 Use 方法用于添加中間件,以及 Build 方法用于建構最終的請求處理委托。

這個實作示例雖然代碼不多,但卻能充分展示中間件的建構原理。你可以仔細閱讀這段代碼,深入了解中間件是如何建構和連接配接的。

public class MiddlewarePipeline              {              private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares =               new List<Func<RequestDelegate, RequestDelegate>>();                  public void Use(Func<RequestDelegate, RequestDelegate> middleware)              {              _middlewares.Add(middleware);              }                  public RequestDelegate Build()              {              RequestDelegate next = context => Task.CompletedTask;                  for (int i = _middlewares.Count - 1; i >= 0; i--)              {              next = _middlewares[i](next);              }                  return next;              }              }           

二、實作自定義中間件

如果您想了解中間件中Run、Use、Map、MapWhen等方法,可以直接看官方文檔:https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-7.0

1. 使用内聯中間件

該中間件通過查詢字元串設定目前請求的區域性:

using System.Globalization;                  var builder = WebApplication.CreateBuilder(args);              var app = builder.Build();                  app.UseHttpsRedirection();                  app.Use(async (context, next) =>              {              var cultureQuery = context.Request.Query["culture"];              if (!string.IsOrWhiteSpace(cultureQuery))              {              var culture = new CultureInfo(cultureQuery);                  CultureInfo.CurrentCulture = culture;              CultureInfo.CurrentUICulture = culture;              }                  // Call the next delegate/middleware in the pipeline.              await next(context);              });                  app.Run(async (context) =>              {              await context.Response.WriteAsync(              $"CurrentCulture.DisplayName: {CultureInfo.CurrentCulture.DisplayName}");              });                  app.Run();           

2.中間件類

以下代碼将中間件委托移動到類: 該類必須具備:

  • 具有類型為 RequestDelegate 的參數的公共構造函數。
  • 名為 Invoke 或 InvokeAsync 的公共方法。 此方法必須:
    • 傳回 Task。
    • 接受類型 HttpContext 的第一個參數。 構造函數和 Invoke/InvokeAsync 的其他參數由依賴關系注入 (DI) 填充。
using System.Globalization;                  namespace Middleware.Example;                  public class RequestCultureMiddleware              {              private readonly RequestDelegate _next;                  public RequestCultureMiddleware(RequestDelegate next)              {              _next = next;              }                  public async Task InvokeAsync(HttpContext context)              {              var cultureQuery = context.Request.Query["culture"];              if (!string.IsOrWhiteSpace(cultureQuery))              {              var culture = new CultureInfo(cultureQuery);                  CultureInfo.CurrentCulture = culture;              CultureInfo.CurrentUICulture = culture;              }                  // Call the next delegate/middleware in the pipeline.              await _next(context);              }              }                  // 封裝擴充方法              public static class RequestCultureMiddlewareExtensions              {              public static IApplicationBuilder UseRequestCulture(              this IApplicationBuilder builder)              {              return builder.UseMiddleware<RequestCultureMiddleware>();              }              }           

3. 基于工廠的中間件

上文描述的自定義類,其實是按照約定來定義實作的。也可以根據IMiddlewareFactory/IMiddleware 中間件的擴充點來使用:

// 自定義中間件類實作 IMiddleware 接口              public class CustomMiddleware : IMiddleware              {              public async Task InvokeAsync(HttpContext context, RequestDelegate next)              {              // 中間件邏輯              await next(context);              }              }                  // 自定義中間件工廠類實作 IMiddlewareFactory 接口              public class CustomMiddlewareFactory : IMiddlewareFactory              {              public IMiddleware Create(IServiceProvider serviceProvider)              {              // 在這裡可以進行一些初始化操作,如依賴注入等              return new CustomMiddleware();              }              }                  // 在 Startup.cs 中使用中間件工廠模式添加中間件              public void Configure(IApplicationBuilder app)              {              app.UseMiddleware<CustomMiddlewareFactory>();              }               

詳細具體的自定義中間件方式請參閱官方文檔:https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/write?view=aspnetcore-7.0

三、源碼解讀中間件

以下是源代碼的部分删減和修改,以便于更好地了解

1. 建立主機建構器

為了更好地了解中間件的建立和執行在整個架構中的位置,我們仍然從 Program 開始。在 Program 中使用 CreateBuilder 方法建立一個預設的主機建構器,配置應用程式的預設設定,并注入基礎服務。

// 在Program.cs檔案中調用              var builder = WebApplication.CreateBuilder(args);           

CreateBuilder方法傳回了WebApplicationBuilder執行個體

public static WebApplicationBuilder CreateBuilder(string[] args) =>              new WebApplicationBuilder(new WebApplicationOptions(){ Args = args });           

在 WebApplicationBuilder 的構造函數中,将配置并注冊中間件

internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilder>? configureDefaults = )              {              // 建立BootstrapHostBuilder執行個體              var bootstrapHostBuilder = new BootstrapHostBuilder(_hostApplicationBuilder);                  // bootstrapHostBuilder 上調用 ConfigureWebHostDefaults 方法,以進行特定于 Web 主機的配置              bootstrapHostBuilder.ConfigureWebHostDefaults(webHostBuilder =>              {              // 配置應用程式包含了中間件的注冊過程和一系列的配置              webHostBuilder.Configure(ConfigureApplication);              });                  var webHostContext = (WebHostBuilderContext)bootstrapHostBuilder.Properties[typeof(WebHostBuilderContext)];              Environment = webHostContext.HostingEnvironment;                  Host = new ConfigureHostBuilder(bootstrapHostBuilder.Context, Configuration, Services);              WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);              }           

ConfigureApplication 方法是用于配置應用程式的核心方法。其中包含了中間件的注冊過程。本篇文章隻關注中間件,路由相關的内容會在下一篇文章進行詳細解釋。

private void ConfigureApplication(WebHostBuilderContext context, IApplicationBuilder app)              {              Debug.Assert(_builtApplication is not );                  // 在 WebApplication 之前調用 UseRouting,例如在 StartupFilter 中,              // 我們需要移除該屬性并在最後重新設定,以免影響過濾器中的路由              if (app.Properties.TryGetValue(EndpointRouteBuilderKey, out var priorRouteBuilder))              {              app.Properties.Remove(EndpointRouteBuilderKey);              }                  // ...                  // 将源管道連接配接到目标管道              var wireSourcePipeline = new WireSourcePipeline(_builtApplication);              app.Use(wireSourcePipeline.CreateMiddleware);                  // ..                  // 将屬性複制到目标應用程式建構器              foreach (var item in _builtApplication.Properties)              {              app.Properties[item.Key] = item.Value;              }                  // 移除路由建構器以清理屬性,我們已經完成了将路由添加到管道的操作              app.Properties.Remove(WebApplication.GlobalEndpointRouteBuilderKey);                  // 如果之前存在路由建構器,則重置它,這對于 StartupFilters 是必要的              if (priorRouteBuilder is not )              {              app.Properties[EndpointRouteBuilderKey] = priorRouteBuilder;              }              }           

通過新建構的RequestDelegate委托處理請求,在目标中間件管道中連接配接源中間件管道

private sealed class WireSourcePipeline(IApplicationBuilder builtApplication)              {              private readonly IApplicationBuilder _builtApplication = builtApplication;                  public RequestDelegate CreateMiddleware(RequestDelegate next)              {              _builtApplication.Run(next);              return _builtApplication.Build();              }              }           

2. 啟動主機,并偵聽HTTP請求

從Program中app.Run()開始,啟動主機,最終會調用IHost的StartAsync方法。

// Program調用Run              app.Run();                  // 實作Run();              public void Run([StringSyntax(StringSyntaxAttribute.Uri)] string? url = )              {              Listen(url);              HostingAbstractionsHostExtensions.Run(this);              }                  // 實作HostingAbstractionsHostExtensions.Run(this);              public static async Task RunAsync(this IHost host, CancellationToken token = default)              {              try              {              await host.StartAsync(token).ConfigureAwait(false);                  await host.WaitForShutdownAsync(token).ConfigureAwait(false);              }              finally              {              if (host is IAsyncDisposable asyncDisposable)              {              await asyncDisposable.DisposeAsync().ConfigureAwait(false);              }              else              {              host.Dispose();              }              }              }           

将中間件和StartupFilters擴充傳入HostingApplication主機,并進行啟動

public async Task StartAsync(CancellationToken cancellationToken)              {              // ...省略了從配置中擷取伺服器監聽位址和端口...                  // 通過配置建構中間件管道              RequestDelegate? application = ;              try              {              IApplicationBuilder builder = ApplicationBuilderFactory.CreateBuilder(Server.Features);                  foreach (var filter in StartupFilters.Reverse())              {              configure = filter.Configure(configure);              }              configure(builder);              // Build the request pipeline              application = builder.Build();              }              catch (Exception ex)              {              Logger.ApplicationError(ex);              }                  /*              * application:中間件              * DiagnosticListener:事件監聽器              * HttpContextFactory:HttpContext對象的工廠              */              HostingApplication httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory, HostingEventSource.Log, HostingMetrics);                  await Server.StartAsync(httpApplication, cancellationToken);                  }           

IApplicationBuilder 提供配置應用程式請求管道的機制,Build方法生成此應用程式用于處理HTTP請求的委托。

public RequestDelegate Build()              {              // 建構一個 RequestDelegate 委托,代表請求的處理邏輯              RequestDelegate app = context =>              {              var endpoint = context.GetEndpoint();              var endpointRequestDelegate = endpoint?.RequestDelegate;              if (endpointRequestDelegate != )              {              throw new InvalidOperationException(message);              }                  return Task.CompletedTask;              };                  // 逐漸建構了包含所有中間件的管道              for (var c = _components.Count - 1; c >= 0; c--)              {              app = _components[c](app);              }                  return app;              }           

3. IApplicationBuilder作用及實作

這裡對IApplicationBuilder做個整體了解,然後再回歸上文流程。

IApplicationBuilder的作用是提供了配置應用程式請求管道的機制。它定義了一組方法和屬性,用于建構和配置應用程式的中間件管道,處理傳入的 HTTP 請求。

  • 通路應用程式的服務容器(ApplicationServices 屬性)。
  • 擷取應用程式的伺服器提供的 HTTP 特性(ServerFeatures 屬性)。
  • 共享資料在中間件之間傳遞的鍵值對集合(Properties 屬性)。
  • 向應用程式的請求管道中添加中間件委托(Use 方法)。
  • 建立一個新的 IApplicationBuilder 執行個體,共享屬性(New 方法)。
  • 建構處理 HTTP 請求的委托(Build 方法)。
public partial class ApplicationBuilder : IApplicationBuilder              {              private readonly List<Func<RequestDelegate, RequestDelegate>> _components = new();              private readonly List<string>? _descriptions;                  /// <summary>              /// Adds the middleware to the application request pipeline.              /// </summary>              /// <param name="middleware">The middleware.</param>              /// <returns>An instance of <see cref="IApplicationBuilder"/> after the operation has completed.</returns>              public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)              {              _components.Add(middleware);              _descriptions?.Add(CreateMiddlewareDescription(middleware));                  return this;              }                  private static string CreateMiddlewareDescription(Func<RequestDelegate, RequestDelegate> middleware)              {              if (middleware.Target != )              {              // To IApplicationBuilder, middleware is just a func. Getting a good description is hard.              // Inspect the incoming func and attempt to resolve it back to a middleware type if possible.              // UseMiddlewareExtensions adds middleware via a method with the name CreateMiddleware.              // If this pattern is matched, then ToString on the target returns the middleware type name.              if (middleware.Method.Name == "CreateMiddleware")              {              return middleware.Target.ToString()!;              }                  return middleware.Target.GetType().FullName + "." + middleware.Method.Name;              }                  return middleware.Method.Name.ToString();              }                  /// <summary>              /// Produces a <see cref="RequestDelegate"/> that executes added middlewares.              /// </summary>              /// <returns>The <see cref="RequestDelegate"/>.</returns>              public RequestDelegate Build()              {              RequestDelegate app = context =>              {              // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened.              // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware.              var endpoint = context.GetEndpoint();              var endpointRequestDelegate = endpoint?.RequestDelegate;              if (endpointRequestDelegate != )              {              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);              }                  // Flushing the response and calling through to the next middleware in the pipeline is              // a user error, but don't attempt to set the status code if this happens. It leads to a confusing              // behavior where the client response looks fine, but the server side logic results in an exception.              if (!context.Response.HasStarted)              {              context.Response.StatusCode = StatusCodes.Status404NotFound;              }                  // Communicates to higher layers that the request wasn't handled by the app pipeline.              context.Items[RequestUnhandledKey] = true;                  return Task.CompletedTask;              };                  for (var c = _components.Count - 1; c >= 0; c--)              {              app = _components[c](app);              }                  return app;              }                  }           

回歸上文流程,将生成的管道傳入HostingApplication中,并在處理Http請求時,進行執行。

// Execute the request              public Task ProcessRequestAsync(Context context)              {              return _application(context.HttpContext!);              }           

還是不清楚執行位置的同學,可以翻閱《.NET源碼解讀kestrel伺服器及建立HttpContext對象流程》文章中的這塊代碼來進行了解。

【.NET源碼解讀】深入剖析中間件的設計與實作(一)

四、小結

.NET 中間件就是基于管道模式和委托來進行實作。每個中間件都是一個委托方法,接受一個 HttpContext 對象和一個 RequestDelegate 委托作為參數,可以對請求進行修改、添加額外的處理邏輯,然後調用 RequestDelegate 來将請求傳遞給下一個中間件或終止請求處理。

如果您覺得這篇文章有所收獲,還請點個贊并關注。如果您有寶貴建議,歡迎在評論區留言,非常感謝您的支援!

繼續閱讀