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


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

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

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

    一、重新认识中间件

    1. 中间件的实现方式

    在介绍中间件之前,让我们先了解一下管道设计模式:

    管道设计模式是一种常见的软件设计模式,用于将一个复杂的任务或操作分解为一系列独立的处理步骤。每个步骤按特定顺序处理数据并传递给下一个步骤,形成线性的处理流程。每个步骤都是独立且可重用的组件。

    在.NET中,针对每个HTTP请求的处理和响应任务被分解为可重用的类或匿名方法,这些组件被称为中间件。中间件的连接顺序是特定的,它们在一个管道中按顺序连接起来,形成一个处理流程。这种设计方式可以根据需求自由地添加、删除或重新排序中间件。

    中间件的实现非常简单,它基于一个委托,接受一个HttpContext对象和一个回调函数(表示下一个中间件)作为参数。当请求到达时,委托执行自己的逻辑,并将请求传递给下一个中间件组件。这个过程会持续进行,直到最后一个中间件完成响应并将结果返回给客户端。

    /*
     * 入参1 string:代表HttpContext
     * 入参2 Func:下一个中间件的方法
     * 结果返回 Task:避免线程阻塞
     * **/
    Func<string, Func, Task> middleware = async (context, next) =>
    {
        Console.WriteLine($"Before middleware: {context}");
    
        await next(); // 调用下一个中间件
    
        Console.WriteLine($"After middleware: {context}");
    };
    
    Func 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> _middlewares = 
            new List>();
    
        public void Use(Func 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等方法,可以直接看官方文档

    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.IsNullOrWhiteSpace(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.IsNullOrWhiteSpace(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();
        }
    }
    

    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();
    }
    
    

    详细具体的自定义中间件方式请参阅官方文档

    三、源码解读中间件

    以下是源代码的部分删减和修改,以便于更好地理解

    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? configureDefaults = null)
    {
        // 创建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 null);
    
        // 在 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 null)
        {
            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 = null)
    {
        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 = null;
        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 != null)
            {
                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> _components = new();
          private readonly List<string>? _descriptions;
    
          /// 
          /// Adds the middleware to the application request pipeline.
          /// 
          /// The middleware.
          /// An instance of  after the operation has completed.
          public IApplicationBuilder Use(Func middleware)
          {
              _components.Add(middleware);
              _descriptions?.Add(CreateMiddlewareDescription(middleware));
    
              return this;
          }
    
          private static string CreateMiddlewareDescription(Func middleware)
          {
              if (middleware.Target != null)
              {
                  // 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();
          }
    
          /// 
          /// Produces a  that executes added middlewares.
          /// 
          /// The .
          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 != 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);
                  }
    
                  // 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 中间件就是基于管道模式和委托来进行实现。每个中间件都是一个委托方法,接受一个 HttpContext 对象和一个 RequestDelegate 委托作为参数,可以对请求进行修改、添加额外的处理逻辑,然后调用 RequestDelegate 来将请求传递给下一个中间件或终止请求处理。

    如果您觉得这篇文章有所收获,还请点个赞并关注。如果您有宝贵建议,欢迎在评论区留言,非常感谢您的支持!

    (也可以关注我的公众号噢:Broder,万分感谢_)


    __EOF__

  • 本文作者: Broder
  • 本文链接: https://www.cnblogs.com/Z7TS/p/17494203.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    第一个python&selenium自动化测试实战项目
    牛客刷题<11>4位数值比较器电路
    Altair:Python数据可视化库的魅力之旅
    表单修饰符、过滤器、内置指令和自定义指令
    网络协议二
    SpringBoot项目实现日志打印SQL明细(包括SQL语句和参数)几种方式
    论文阅读-Joint Audio-Visual Deepfake Detection(音视频Deepfake检测)
    协程概述讲解
    SpringMVC的概念和使用以及bean加载控制
    MySQL的概念
  • 原文地址:https://www.cnblogs.com/Z7TS/p/17494203.html