翼度科技»论坛 编程开发 .net 查看内容

【.NET源码解读】深入剖析中间件的设计与实现

7

主题

7

帖子

21

积分

新手上路

Rank: 1

积分
21
.NET本身就是一个基于中间件(middleware)的框架,它通过一系列的中间件组件来处理HTTP请求和响应。在之前的文章《.NET源码解读kestrel服务器及创建HttpContext对象流程》中,已经通过源码介绍了如何将HTTP数据包转换为.NET的HttpContext对象。接下来,让我们深入了解一下.NET是如何设计中间件来处理HttpContext对象。
通过本文,您可以了解以下内容:

  • 认识中间件的本质
  • 实现自定义中间件
  • 源码解读中间件原理
一、重新认识中间件

1. 中间件的实现方式

在介绍中间件之前,让我们先了解一下管道设计模式:
管道设计模式是一种常见的软件设计模式,用于将一个复杂的任务或操作分解为一系列独立的处理步骤。每个步骤按特定顺序处理数据并传递给下一个步骤,形成线性的处理流程。每个步骤都是独立且可重用的组件。
在.NET中,针对每个HTTP请求的处理和响应任务被分解为可重用的类或匿名方法,这些组件被称为中间件。中间件的连接顺序是特定的,它们在一个管道中按顺序连接起来,形成一个处理流程。这种设计方式可以根据需求自由地添加、删除或重新排序中间件。
中间件的实现非常简单,它基于一个委托,接受一个HttpContext对象和一个回调函数(表示下一个中间件)作为参数。当请求到达时,委托执行自己的逻辑,并将请求传递给下一个中间件组件。这个过程会持续进行,直到最后一个中间件完成响应并将结果返回给客户端。
  1. /*
  2. * 入参1 string:代表HttpContext
  3. * 入参2 Func<Task>:下一个中间件的方法
  4. * 结果返回 Task:避免线程阻塞
  5. * **/
  6. Func<string, Func<Task>, Task> middleware = async (context, next) =>
  7. {
  8.     Console.WriteLine($"Before middleware: {context}");
  9.     await next(); // 调用下一个中间件
  10.     Console.WriteLine($"After middleware: {context}");
  11. };
复制代码
  1. Func<Task> finalMiddleware = () =>
  2. {
  3.     // 最后一个中间件的逻辑
  4.     Console.WriteLine("Final middleware");
  5.     return Task.CompletedTask;
  6. };
复制代码
为了给所有的中间件和终端处理器提供统一的委托类型,使得它们在请求处理管道中可以无缝地连接起来。所以引入了RequestDelegate委托。上文中Func方法,最终都会转换成RequestDelegate委托,这一点放在下文源码解析中。
  1. public delegate Task RequestDelegate(HttpContext context);
复制代码
2. 中间件管道构建器原理

下面是从源码中提取出的一个简单的中间件管道构建器实现示例。它包含一个 _middlewares 列表,用于存储中间件委托,并提供了 Use 方法用于添加中间件,以及 Build 方法用于构建最终的请求处理委托。
这个实现示例虽然代码不多,但却能充分展示中间件的构建原理。你可以仔细阅读这段代码,深入理解中间件是如何构建和连接的。
  1. public class MiddlewarePipeline
  2. {
  3.     private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares =
  4.         new List<Func<RequestDelegate, RequestDelegate>>();
  5.     public void Use(Func<RequestDelegate, RequestDelegate> middleware)
  6.     {
  7.         _middlewares.Add(middleware);
  8.     }
  9.     public RequestDelegate Build()
  10.     {
  11.         RequestDelegate next = context => Task.CompletedTask;
  12.         
  13.         for (int i = _middlewares.Count - 1; i >= 0; i--)
  14.         {
  15.             next = _middlewares[i](next);
  16.         }
  17.         return next;
  18.     }
  19. }
复制代码
二、实现自定义中间件

如果您想了解中间件中Run、Use、Map、MapWhen等方法,可以直接看官方文档
1. 使用内联中间件

该中间件通过查询字符串设置当前请求的区域性:
  1. using System.Globalization;
  2. var builder = WebApplication.CreateBuilder(args);
  3. var app = builder.Build();
  4. app.UseHttpsRedirection();
  5. app.Use(async (context, next) =>
  6. {
  7.     var cultureQuery = context.Request.Query["culture"];
  8.     if (!string.IsNullOrWhiteSpace(cultureQuery))
  9.     {
  10.         var culture = new CultureInfo(cultureQuery);
  11.         CultureInfo.CurrentCulture = culture;
  12.         CultureInfo.CurrentUICulture = culture;
  13.     }
  14.     // Call the next delegate/middleware in the pipeline.
  15.     await next(context);
  16. });
  17. app.Run(async (context) =>
  18. {
  19.     await context.Response.WriteAsync(
  20.         $"CurrentCulture.DisplayName: {CultureInfo.CurrentCulture.DisplayName}");
  21. });
  22. app.Run();
复制代码
2.中间件类

以下代码将中间件委托移动到类:
该类必须具备:

  • 具有类型为 RequestDelegate 的参数的公共构造函数。
  • 名为 Invoke 或 InvokeAsync 的公共方法。 此方法必须:

    • 返回 Task。
    • 接受类型 HttpContext 的第一个参数。
      构造函数和 Invoke/InvokeAsync 的其他参数由依赖关系注入 (DI) 填充。

  1. using System.Globalization;
  2. namespace Middleware.Example;
  3. public class RequestCultureMiddleware
  4. {
  5.     private readonly RequestDelegate _next;
  6.     public RequestCultureMiddleware(RequestDelegate next)
  7.     {
  8.         _next = next;
  9.     }
  10.     public async Task InvokeAsync(HttpContext context)
  11.     {
  12.         var cultureQuery = context.Request.Query["culture"];
  13.         if (!string.IsNullOrWhiteSpace(cultureQuery))
  14.         {
  15.             var culture = new CultureInfo(cultureQuery);
  16.             CultureInfo.CurrentCulture = culture;
  17.             CultureInfo.CurrentUICulture = culture;
  18.         }
  19.         // Call the next delegate/middleware in the pipeline.
  20.         await _next(context);
  21.     }
  22. }
  23. // 封装扩展方法
  24. public static class RequestCultureMiddlewareExtensions
  25. {
  26.     public static IApplicationBuilder UseRequestCulture(
  27.         this IApplicationBuilder builder)
  28.     {
  29.         return builder.UseMiddleware<RequestCultureMiddleware>();
  30.     }
  31. }
复制代码
3. 基于工厂的中间件

该方法具体描述请看官方文档
上文描述的自定义类,其实是按照约定来定义实现的。也可以根据IMiddlewareFactory/IMiddleware 中间件的扩展点来使用:
  1. // 自定义中间件类实现 IMiddleware 接口
  2. public class CustomMiddleware : IMiddleware
  3. {
  4.     public async Task InvokeAsync(HttpContext context, RequestDelegate next)
  5.     {
  6.         // 中间件逻辑
  7.         await next(context);
  8.     }
  9. }
  10. // 自定义中间件工厂类实现 IMiddlewareFactory 接口
  11. public class CustomMiddlewareFactory : IMiddlewareFactory
  12. {
  13.     public IMiddleware Create(IServiceProvider serviceProvider)
  14.     {
  15.         // 在这里可以进行一些初始化操作,如依赖注入等
  16.         return new CustomMiddleware();
  17.     }
  18. }
  19. // 在 Startup.cs 中使用中间件工厂模式添加中间件
  20. public void Configure(IApplicationBuilder app)
  21. {
  22.     app.UseMiddleware<CustomMiddlewareFactory>();
  23. }
复制代码
详细具体的自定义中间件方式请参阅官方文档
三、源码解读中间件

以下是源代码的部分删减和修改,以便于更好地理解
1. 创建主机构建器

为了更好地理解中间件的创建和执行在整个框架中的位置,我们仍然从 Program 开始。在 Program 中使用 CreateBuilder 方法创建一个默认的主机构建器,配置应用程序的默认设置,并注入基础服务。
  1. // 在Program.cs文件中调用
  2. var builder = WebApplication.CreateBuilder(args);
复制代码
CreateBuilder方法返回了WebApplicationBuilder实例
  1. public static WebApplicationBuilder CreateBuilder(string[] args) =>
  2.     new WebApplicationBuilder(new WebApplicationOptions(){ Args = args });
复制代码
在 WebApplicationBuilder 的构造函数中,将配置并注册中间件
  1. internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilder>? configureDefaults = null)
  2. {
  3.     // 创建BootstrapHostBuilder实例
  4.     var bootstrapHostBuilder = new BootstrapHostBuilder(_hostApplicationBuilder);
  5.     // bootstrapHostBuilder 上调用 ConfigureWebHostDefaults 方法,以进行特定于 Web 主机的配置
  6.     bootstrapHostBuilder.ConfigureWebHostDefaults(webHostBuilder =>
  7.     {
  8.         // 配置应用程序包含了中间件的注册过程和一系列的配置
  9.         webHostBuilder.Configure(ConfigureApplication);
  10.     });
  11.     var webHostContext = (WebHostBuilderContext)bootstrapHostBuilder.Properties[typeof(WebHostBuilderContext)];
  12.     Environment = webHostContext.HostingEnvironment;
  13.     Host = new ConfigureHostBuilder(bootstrapHostBuilder.Context, Configuration, Services);
  14.     WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
  15. }
复制代码
ConfigureApplication 方法是用于配置应用程序的核心方法。其中包含了中间件的注册过程。本篇文章只关注中间件,路由相关的内容会在下一篇文章进行详细解释。
  1. private void ConfigureApplication(WebHostBuilderContext context, IApplicationBuilder app)
  2. {
  3.     Debug.Assert(_builtApplication is not null);
  4.     // 在 WebApplication 之前调用 UseRouting,例如在 StartupFilter 中,
  5.     // 我们需要移除该属性并在最后重新设置,以免影响过滤器中的路由
  6.     if (app.Properties.TryGetValue(EndpointRouteBuilderKey, out var priorRouteBuilder))
  7.     {
  8.         app.Properties.Remove(EndpointRouteBuilderKey);
  9.     }
  10.     // ...
  11.     // 将源管道连接到目标管道
  12.     var wireSourcePipeline = new WireSourcePipeline(_builtApplication);
  13.     app.Use(wireSourcePipeline.CreateMiddleware);
  14.     // ..
  15.     // 将属性复制到目标应用程序构建器
  16.     foreach (var item in _builtApplication.Properties)
  17.     {
  18.         app.Properties[item.Key] = item.Value;
  19.     }
  20.     // 移除路由构建器以清理属性,我们已经完成了将路由添加到管道的操作
  21.     app.Properties.Remove(WebApplication.GlobalEndpointRouteBuilderKey);
  22.     // 如果之前存在路由构建器,则重置它,这对于 StartupFilters 是必要的
  23.     if (priorRouteBuilder is not null)
  24.     {
  25.         app.Properties[EndpointRouteBuilderKey] = priorRouteBuilder;
  26.     }
  27. }
复制代码
通过新构建的RequestDelegate委托处理请求,在目标中间件管道中连接源中间件管道
  1. private sealed class WireSourcePipeline(IApplicationBuilder builtApplication)
  2. {
  3.     private readonly IApplicationBuilder _builtApplication = builtApplication;
  4.     public RequestDelegate CreateMiddleware(RequestDelegate next)
  5.     {
  6.         _builtApplication.Run(next);
  7.         return _builtApplication.Build();
  8.     }
  9. }
复制代码
2. 启动主机,并侦听HTTP请求

从Program中app.Run()开始,启动主机,最终会调用IHost的StartAsync方法。
  1. // Program调用Run
  2. app.Run();
  3. // 实现Run();
  4. public void Run([StringSyntax(StringSyntaxAttribute.Uri)] string? url = null)
  5. {
  6.     Listen(url);
  7.     HostingAbstractionsHostExtensions.Run(this);
  8. }
  9. // 实现HostingAbstractionsHostExtensions.Run(this);
  10. public static async Task RunAsync(this IHost host, CancellationToken token = default)
  11. {
  12.     try
  13.     {
  14.         await host.StartAsync(token).ConfigureAwait(false);
  15.         await host.WaitForShutdownAsync(token).ConfigureAwait(false);
  16.     }
  17.     finally
  18.     {
  19.         if (host is IAsyncDisposable asyncDisposable)
  20.         {
  21.             await asyncDisposable.DisposeAsync().ConfigureAwait(false);
  22.         }
  23.         else
  24.         {
  25.             host.Dispose();
  26.         }
  27.     }
  28. }
复制代码
将中间件和StartupFilters扩展传入HostingApplication主机,并进行启动
  1. public async Task StartAsync(CancellationToken cancellationToken)
  2. {
  3.     // ...省略了从配置中获取服务器监听地址和端口...
  4.     // 通过配置构建中间件管道
  5.     RequestDelegate? application = null;
  6.     try
  7.     {
  8.         IApplicationBuilder builder = ApplicationBuilderFactory.CreateBuilder(Server.Features);
  9.         foreach (var filter in StartupFilters.Reverse())
  10.         {
  11.             configure = filter.Configure(configure);
  12.         }
  13.         configure(builder);
  14.         // Build the request pipeline
  15.         application = builder.Build();
  16.     }
  17.     catch (Exception ex)
  18.     {
  19.         Logger.ApplicationError(ex);
  20.     }
  21.     /*
  22.      * application:中间件
  23.      * DiagnosticListener:事件监听器
  24.      * HttpContextFactory:HttpContext对象的工厂
  25.      */
  26.     HostingApplication httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory, HostingEventSource.Log, HostingMetrics);
  27.     await Server.StartAsync(httpApplication, cancellationToken);
  28. }
复制代码
IApplicationBuilder 提供配置应用程序请求管道的机制,Build方法生成此应用程序用于处理HTTP请求的委托。
  1. public RequestDelegate Build()
  2. {
  3.     // 构建一个 RequestDelegate 委托,代表请求的处理逻辑
  4.     RequestDelegate app = context =>
  5.     {
  6.         var endpoint = context.GetEndpoint();
  7.         var endpointRequestDelegate = endpoint?.RequestDelegate;
  8.         if (endpointRequestDelegate != null)
  9.         {
  10.             throw new InvalidOperationException(message);
  11.         }
  12.         return Task.CompletedTask;
  13.     };
  14.     // 逐步构建了包含所有中间件的管道
  15.     for (var c = _components.Count - 1; c >= 0; c--)
  16.     {
  17.         app = _components[c](app);
  18.     }
  19.     return app;
  20. }
复制代码
3. IApplicationBuilder作用及实现

这里对IApplicationBuilder做个整体了解,然后再回归上文流程。
IApplicationBuilder的作用是提供了配置应用程序请求管道的机制。它定义了一组方法和属性,用于构建和配置应用程序的中间件管道,处理传入的 HTTP 请求。

  • 访问应用程序的服务容器(ApplicationServices 属性)。
  • 获取应用程序的服务器提供的 HTTP 特性(ServerFeatures 属性)。
  • 共享数据在中间件之间传递的键值对集合(Properties 属性)。
  • 向应用程序的请求管道中添加中间件委托(Use 方法)。
  • 创建一个新的 IApplicationBuilder 实例,共享属性(New 方法)。
  • 构建处理 HTTP 请求的委托(Build 方法)。
  1. public partial class ApplicationBuilder : IApplicationBuilder
  2.   {
  3.       private readonly List<Func<RequestDelegate, RequestDelegate>> _components = new();
  4.       private readonly List<string>? _descriptions;
  5.       /// <summary>
  6.       /// Adds the middleware to the application request pipeline.
  7.       /// </summary>
  8.       /// <param name="middleware">The middleware.</param>
  9.       /// <returns>An instance of <see cref="IApplicationBuilder"/> after the operation has completed.</returns>
  10.       public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
  11.       {
  12.           _components.Add(middleware);
  13.           _descriptions?.Add(CreateMiddlewareDescription(middleware));
  14.           return this;
  15.       }
  16.       private static string CreateMiddlewareDescription(Func<RequestDelegate, RequestDelegate> middleware)
  17.       {
  18.           if (middleware.Target != null)
  19.           {
  20.               // To IApplicationBuilder, middleware is just a func. Getting a good description is hard.
  21.               // Inspect the incoming func and attempt to resolve it back to a middleware type if possible.
  22.               // UseMiddlewareExtensions adds middleware via a method with the name CreateMiddleware.
  23.               // If this pattern is matched, then ToString on the target returns the middleware type name.
  24.               if (middleware.Method.Name == "CreateMiddleware")
  25.               {
  26.                   return middleware.Target.ToString()!;
  27.               }
  28.               return middleware.Target.GetType().FullName + "." + middleware.Method.Name;
  29.           }
  30.           return middleware.Method.Name.ToString();
  31.       }
  32.       /// <summary>
  33.       /// Produces a <see cref="RequestDelegate"/> that executes added middlewares.
  34.       /// </summary>
  35.       /// <returns>The <see cref="RequestDelegate"/>.</returns>
  36.       public RequestDelegate Build()
  37.       {
  38.           RequestDelegate app = context =>
  39.           {
  40.               // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened.
  41.               // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware.
  42.               var endpoint = context.GetEndpoint();
  43.               var endpointRequestDelegate = endpoint?.RequestDelegate;
  44.               if (endpointRequestDelegate != null)
  45.               {
  46.                   var message =
  47.                       $"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " +
  48.                       $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
  49.                       $"routing.";
  50.                   throw new InvalidOperationException(message);
  51.               }
  52.               // Flushing the response and calling through to the next middleware in the pipeline is
  53.               // a user error, but don't attempt to set the status code if this happens. It leads to a confusing
  54.               // behavior where the client response looks fine, but the server side logic results in an exception.
  55.               if (!context.Response.HasStarted)
  56.               {
  57.                   context.Response.StatusCode = StatusCodes.Status404NotFound;
  58.               }
  59.               // Communicates to higher layers that the request wasn't handled by the app pipeline.
  60.               context.Items[RequestUnhandledKey] = true;
  61.               return Task.CompletedTask;
  62.           };
  63.           for (var c = _components.Count - 1; c >= 0; c--)
  64.           {
  65.               app = _components[c](app);
  66.           }
  67.           return app;
  68.       }
  69.   }
复制代码
回归上文流程,将生成的管道传入HostingApplication中,并在处理Http请求时,进行执行。
  1. // Execute the request
  2. public Task ProcessRequestAsync(Context context)
  3. {
  4.     return _application(context.HttpContext!);
  5. }
复制代码
还是不清楚执行位置的同学,可以翻阅《.NET源码解读kestrel服务器及创建HttpContext对象流程》文章中的这块代码来进行了解。

四、小结

.NET 中间件就是基于管道模式和委托来进行实现。每个中间件都是一个委托方法,接受一个 HttpContext 对象和一个 RequestDelegate 委托作为参数,可以对请求进行修改、添加额外的处理逻辑,然后调用 RequestDelegate 来将请求传递给下一个中间件或终止请求处理。
如果您觉得这篇文章有所收获,还请点个赞并关注。如果您有宝贵建议,欢迎在评论区留言,非常感谢您的支持!
(也可以关注我的公众号噢:Broder,万分感谢_)

来源:https://www.cnblogs.com/Z7TS/archive/2023/06/29/17494203.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具