当前动态:【.NET源码解读】深入剖析中间件的设计与实现
NET本身就是一个基于中间件(middleware)的框架,它通过一系列的中间
.NET本身就是一个基于中间件(middleware)的框架,它通过一系列的中间件组件来处理HTTP请求和响应。在之前的文章《.NET源码解读kestrel服务器及创建HttpContext对象流程》中,已经通过源码介绍了如何将HTTP数据包转换为.NET的HttpContext对象。接下来,让我们深入了解一下.NET是如何设计中间件来处理HttpContext对象。
通过本文,您可以了解以下内容:
认识中间件的本质实现自定义中间件源码解读中间件原理一、重新认识中间件1. 中间件的实现方式在介绍中间件之前,让我们先了解一下管道设计模式:
(资料图片仅供参考)
管道设计模式是一种常见的软件设计模式,用于将一个复杂的任务或操作分解为一系列独立的处理步骤。每个步骤按特定顺序处理数据并传递给下一个步骤,形成线性的处理流程。每个步骤都是独立且可重用的组件。
在.NET中,针对每个HTTP请求的处理和响应任务被分解为可重用的类或匿名方法,这些组件被称为中间件。中间件的连接顺序是特定的,它们在一个管道中按顺序连接起来,形成一个处理流程。这种设计方式可以根据需求自由地添加、删除或重新排序中间件。
中间件的实现非常简单,它基于一个委托,接受一个HttpContext对象和一个回调函数(表示下一个中间件)作为参数。当请求到达时,委托执行自己的逻辑,并将请求传递给下一个中间件组件。这个过程会持续进行,直到最后一个中间件完成响应并将结果返回给客户端。
/* * 入参1 string:代表HttpContext * 入参2 Func:下一个中间件的方法 * 结果返回 Task:避免线程阻塞 * **/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调用Runapp.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? _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 requestpublic Task ProcessRequestAsync(Context context){ return _application(context.HttpContext!);}
还是不清楚执行位置的同学,可以翻阅《.NET源码解读kestrel服务器及创建HttpContext对象流程》文章中的这块代码来进行了解。
四、小结.NET 中间件就是基于管道模式和委托来进行实现。每个中间件都是一个委托方法,接受一个 HttpContext 对象和一个 RequestDelegate 委托作为参数,可以对请求进行修改、添加额外的处理逻辑,然后调用 RequestDelegate 来将请求传递给下一个中间件或终止请求处理。
如果您觉得这篇文章有所收获,还请点个赞并关注。如果您有宝贵建议,欢迎在评论区留言,非常感谢您的支持!
(也可以关注我的公众号噢:Broder,万分感谢_)
标签:
NET本身就是一个基于中间件(middleware)的框架,它通过一系列的中间
深圳五险一金缴费基数及比例标准详情,下面跟着社保网小编一起去看看详
来源:智通财经APP美国总统拜登周三表示,计划从去年通过的《通货膨胀
新华社天津6月29日电综述:与会人士热议新技术为世界发展注入新动能新
中水渔业6月29日在互动平台表示,核污水排放对捕捞海域是否存在核污染
1、现在很多城市都实施了开放式选号方式,买车时可以随意组合自己喜欢
进入夏天,天气炎热,要注意预防中暑,同时驾驶人朋友也要警惕车辆“中
为进一步抓实抓牢抓细燃气安全领域的专项隐患排查治理工作,切实保障人
[ 相关新闻 ]