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

这篇具有很好参考价值的文章主要介绍了【.NET源码解读】深入剖析中间件的设计与实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

.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等方法,可以直接看官方文档

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

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

三、源码解读中间件

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

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

      /// <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 != 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源码解读】深入剖析中间件的设计与实现

四、小结

.NET 中间件就是基于管道模式和委托来进行实现。每个中间件都是一个委托方法,接受一个 HttpContext 对象和一个 RequestDelegate 委托作为参数,可以对请求进行修改、添加额外的处理逻辑,然后调用 RequestDelegate 来将请求传递给下一个中间件或终止请求处理。

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

(也可以关注我的公众号噢:Broder,万分感谢_)文章来源地址https://www.toymoban.com/news/detail-508463.html

到了这里,关于【.NET源码解读】深入剖析中间件的设计与实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【ASP.NET Core 基础知识】--中间件--内置中间件的使用

    ASP.NET Core 中包含很多内置的中间件,我们不可能对每一个内置的中间件进行一一讲解,并且中间件的使用步骤大致一样,因此本文讲解几个常用的内置中间件以及使用中间件的步骤,希望读者们可以举一反三。 一、内置中间件的介绍 1.1 静态文件中间件 在ASP.NET Core中,静态

    2024年01月17日
    浏览(40)
  • 【ASP.NET Core 基础知识】--中间件--创建自定义中间件

    一、为什么需要自定义中间件 自定义中间件在ASP.NET Core中的应用主要有以下几个原因: 满足特定需求: 默认情况下,ASP.NET Core提供了许多内置的中间件来处理常见的任务,如身份验证、授权、静态文件服务等。然而,某些项目可能有特定的需求,需要定制化的处理流程,这

    2024年01月17日
    浏览(52)
  • .net core 中间件

    先说一下 管道 这个概念: 在ASP.NET Core中,管道(Pipeline)是一个由多个中间件组成的处理请求和生成响应的机制。请求从第一个中间件开始,经过一系列中间件的处理,然后生成最终的响应。 每个中间件都会处理请求,并将请求传递给下一个中间件,直到达到最后一个中间

    2024年02月14日
    浏览(35)
  • .net core 中什么是中间件

    在 .NET Core 中,中间件(Middleware)是 ASP.NET Core 应用程序处理请求和响应的组件。中间件位于应用程序的请求处理管道中,它可以截获请求,执行一些逻辑,并将请求传递给下一个中间件或终止请求的执行。 中间件的主要作用是实现横切关注点,处理跨请求的功能和任务,例

    2024年01月17日
    浏览(42)
  • 测试 ASP.NET Core 中间件

            正常情况下,中间件会在主程序入口统一进行实例化,这样如果想单独测试某一个中间件就很不方便,为了能测试单个中间件,可以使用 TestServer 单独测试。 这样便可以: 实例化只包含需要测试的组件的应用管道。 发送自定义请求以验证中间件行为。 这样测试

    2024年01月20日
    浏览(43)
  • 基于.NET6的自定义中间件

    中间件基础: 在.net6.0在请求在响应给请求者之前会通过请求管道再处理服务端的逻辑然后再响应给请求者,而请求管道则是由一系列中间件组成的有点类似于过滤器,为了更直观的了解,我们请看下图:  它可以决定是否将请求传递给请求管道中下一个中间件,也可以处理下一个中

    2023年04月27日
    浏览(30)
  • 深入了解 RabbitMQ:高性能消息中间件

    在现代分布式系统中,消息队列成为了实现系统间异步通信、削峰填谷以及解耦组件的重要工具。而RabbitMQ作为一个高效可靠的消息队列解决方案,已经成为许多企业广泛采用的选择。本文将介绍RabbitMQ的基本概念、主要特性以及常见应用场景。 RabbitMQ 是一个开源的高性能、

    2024年02月08日
    浏览(36)
  • 深入理解Java消息中间件-组件-消息队列

    引言: 消息中间件在现代分布式系统中扮演着至关重要的角色,它解决了系统之间异步通信和解耦的需求。而在消息中间件的架构中,核心组件之一就是消息队列。本文将深入探讨消息队列的架构组件,帮助读者加深对消息中间件的理解和应用。 一、什么是消息队列 消息队列

    2024年04月27日
    浏览(31)
  • .Net Core核心概念——依赖注入和中间件

    1. 为什么要用依赖注入(DI) 什么是依赖注入,为什么要使用呢?简单通俗说就是一个类需要另一个类来协助工作,就产生了依赖,所以需要的依赖项就要【注入】过来一起来协同完成工作。 软件设计原则中有一个依赖倒置原则(DIP)讲的是要依赖于抽象,不要依赖于具体,高层

    2024年02月08日
    浏览(32)
  • Django中间件的源码解析流程(上)——中间件载入的前置

    目录 1. ​前言​ 2. 请求的入口 3. 中间件加载的入口 4. 源码中的闭包实现 5. 最后 哈喽,大家好,我是 小K ,今天咋们分享的内容是:在学会Django中间件之后, 我们继续深入底层源码。 在执行中间件时请求到来总是从前往后逐一匹配,但当响应返回时,执行的中间件顺序往往

    2024年04月22日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包