asp.net core-管道和中间件

管道和中间件

看下面一张图,http请求进入管道,然后会通过一个个中间件,然后返回时也会继续通过一个个中间件,假如在某个中间件短路了,那么会回到它的上一个中间件,直到第一个中间件,然后返回htt响应。

 

 

 中间件和过滤器很像,比如它们都可以实现权限验证或者日志记录,它们都是AOP(面向切面编程的产物),两者的定位不同。

过滤器关注的是实现某类非业务功能。

中间件是asp.net core管道模型中的重要组成部分,担负着从请求到响应的整个处理流程,过滤器实现的功能只是它顺带表现出来的特性。

自定义中间件

我们通过一个例子来简单展示下中间件

        // 构建管道
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.Use(async (context, next) =>
            {
                await context.Response.WriteAsync("start\r\n");
                await next();
                await context.Response.WriteAsync("end\r\n");
            });

            // 没有next,用来添加终端中间件,放在最后面,来短路请求管道
            app.Run(async context =>
            {
                await context.Response.WriteAsync("hello\r\n");
            });
}    

如图所示,我们通过在startup里面的Configure方法里面添加app.Use来实现中间件,app.Run来实现终端中间件。

最终的执行结果如下

start
hello
end

 

下面通过自定义一个记录日志的log中间件

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace InitiativeTeamZone.Api
{
    public class LoggerMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger<LoggerMiddleware> _logger;
        public LoggerMiddleware(RequestDelegate next, ILogger<LoggerMiddleware> logger)
        {
            _next = next;
            _logger = logger;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            context.Request.EnableBuffering();
            var requestReader = new StreamReader(context.Request.Body);

            var requestContent = await requestReader.ReadToEndAsync();
            _logger.LogInformation($"Request Body: {requestContent}");
            context.Request.Body.Position = 0;


            Stream originalBody = context.Response.Body;
            try
            {
                using (var ms = new MemoryStream())
                {
                    context.Response.Body = ms;
                    var fWatch = new Stopwatch();
                    fWatch.Start();
                    await _next(context);
                    fWatch.Stop();
                    ms.Position = 0;
                    string responseBody = new StreamReader(ms).ReadToEnd();
                    _logger.LogInformation($"Response Body: {responseBody}");
                    ms.Position = 0;
                    await ms.CopyToAsync(originalBody);
                }
            }
            finally
            {
                context.Response.Body = originalBody;
            }
        }
    }
}

在startup里面使用

app.UseMiddleware<LoggerMiddleware>(); // log中间件输出body

 

定义一个中间件有2个标准

1.  选择是否将请求(也就是HttpContext,这里面包含了所有http请求的信息)传递到下一个中间件

2. 在管道中下一个中间件前后执行工作。这句话的意思是我们把一个请求经过中间件分为2个步骤,第一次经过中间件,然后向下一个中间件前进。第二次返回中间件,然后向上一个中间件前进。图中的RequestDelegate就代表一个中间件。也就是说在next执行的前后完成所需要做的工作。

源码分析

我们暴力点,不看源码了,直接按源码原理实现一个管道,所有的逻辑和方法名跟源码一样

using System;
using System.Threading.Tasks;

namespace PipelineDemo
{
    public delegate Task RequestDelegate(HttpContext context);

    class Program
    {
        static void Main(string[] args)
        {
            var app = new ApplicationBuilder();

            app.Use(async (context, next) =>
            {
                Console.WriteLine("中间件1号 Begin");
                await next();
                Console.WriteLine("中间件1号 End");
            });

            app.Use(async (context, next) =>
            {
                Console.WriteLine("中间件2号 Begin");
                await next();
                Console.WriteLine("中间件2号 End");
            });

            // 这时候管道已经形成,执行第一个中间件,就会依次调用下一个
            // 主机创建以后运行的
            var firstMiddleware = app.Build();

            // 当请求进来的时候,就会执行第一个中间件
            // 主机给的
            firstMiddleware(new HttpContext());
        }
    }
}

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PipelineDemo
{
    public class ApplicationBuilder
    {
        // 中间件,独立的!互相没有关联的,只有一个顺序
        private static readonly IList<Func<RequestDelegate, RequestDelegate>> _components = 
            new List<Func<RequestDelegate, RequestDelegate>>();

        // 扩展Use
        public ApplicationBuilder Use(Func<HttpContext, Func<Task>, Task> middleware)
        {
            return Use(next =>
            {
                return context =>
                {
                    Task SimpleNext() => next(context);
                    return middleware(context, SimpleNext );
                };
            });
        }

        // 原始Use
        public ApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
        {
            // 添加中间件
            _components.Add(middleware);
            return this;
        }
    
        public RequestDelegate Build()
        {
            RequestDelegate app = context =>
            {
                Console.WriteLine("默认中间件");
                return Task.CompletedTask;
            };

            // 上面的代码是一个默认的中间件
            // 重要的是下面几句,这里对Func<RequestDelegate, RequestDelegate>集合进行反转,
            // 逐一执行添加中间件的委托,最后返回第一个中间件委托
            // 这里的作用就是把list里独立的中间件委托给串起来,然后返回反转后的最后一个中间件(实际上的第一个)
            
            // 管道才真正的建立起来,每一个中间件都首尾相连
            foreach (var component in _components.Reverse())
            {
                app = component(app);
            }

            return app;
        }
    }
}

 

using System;
using System.Collections.Generic;
using System.Text;

namespace PipelineDemo
{
    public class HttpContext
    {
    }
}

让我们分析下

首先通过app.Use()方法添加2个中间件,添加完成后2个中间件变成了Func<RequestDelegate, RequestDelegate>类型,并且依次被添加进了_components数组中。

middleware1  通过use 变成  
var requestDelegate1 = async context=> 
{
    Console.WriteLine("中间件1号 Begin");
    await next(context),
    Console.WriteLine("中间件1号 End");
}

middleware2  通过use 变成  
var requestDelegate2 = async context=> 
{
    Console.WriteLine("中间件2号 Begin");
    await next(context);
    Console.WriteLine("中间件2号 End");
}

接着通过app.Build()将数组中的元素反转,将所有的中间件首尾相连,并且返回第一个中间件也就是下面的requestDelegate1 

var requestDelegate2 = async context=> 
{
    Console.WriteLine("中间件2号 Begin");
    await defaultMiddleware(context);
    Console.WriteLine("中间件2号 End");
}

var requestDelegate1 = async context=> 
{
    Console.WriteLine("中间件1号 Begin");
    await requestDelegate2 (context),
    Console.WriteLine("中间件1号 End");
}
假如有2个以上中间件,那么继续往下迭代

最后当有请求进来时,执行firstMiddleware(new HttpContext());

因为HttpContext没有用到,所以直接定义为空的。然后我们可以看下执行过程

首先执行Console.WriteLine("中间件1号 Begin");

然后执行await requestDelegate2(context)跳转到requestDelegate2中

接着执行Console.WriteLine("中间件2号 Begin");

然后执行await defaultRequestDelegate(context)跳转到defaultRequestDelegate中

执行Console.WriteLine("默认中间件");

然后回到requestDelegate2,执行Console.WriteLine("中间件2号 End");

然后回到requestDelegate1,执行Console.WriteLine("中间件1号 End");

我们看下输出结果

如果不明白原理,可以进行调试

有很重要的一点是,当执行IHost.Run的时候会执行Configure方法,然后把所有的中间件添加到管道中。只有当http请求到达的时候,才会执行第一个中间件。

 

阅读源码流程

1. 通过git clone下载源码,源码路径:https://github.com/dotnet/aspnetcore

2. 克隆下来之后,不能直接用visual studio打开,需要执行下文件夹里面的restore.cmd脚本,因为asp.net core使用的dotnet的SDK跟我们的版本是不一样的,通过这个命令可以生成专用的dotnet。

而且源码不提供sln文件来打开整个项目,因为项目实在太复杂,vs处理不了,所以官方将其拆分成一个个项目,每个项目里面都有一个.sln文件,但是这个.sln还是不能直接打开,需要运行startvs.cmd脚本,理由是该项目使用的环境变量跟我们的是不一样的,需要通过这个脚本单独设置环境变量。

 

posted @ 2021-01-07 10:26  小鸡蛋白  阅读(298)  评论(0编辑  收藏  举报