Loading

AspNetCore管道

当我们像下面这样添加一个管道时发生了什么?

app.Use(async (httpcontext, next) =>
{
    Console.WriteLine("做一些业务处理");
    // do sth here...
    // 调用下一个管道
    await next();
});

接着我们来看下Use的源码,首先需要知道的是,.Net Core 源码内部ApplicationBuilder类维护了一个管道的委托调用链:

private readonly List<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();

public delegate Task RequestDelegate(HttpContext context);




// 上面的Use先调用了这个
public static IApplicationBuilder Use(
    this IApplicationBuilder app,
    Func<HttpContext, Func<Task>, Task> middleware)
{
    // 这里的Use调用下面的Use
    return app.Use((Func<RequestDelegate, RequestDelegate>) (next => (RequestDelegate) (context =>
                                                                                        {
                                                                                            Func<Task> func = (Func<Task>) (() => next(context));
                                                                                            return middleware(context, func);
                                                                                        })));
}

// 真正的Use
public IApplicationBuilder Use(
    Func<RequestDelegate, RequestDelegate> middleware)
{
    this._components.Add(middleware);
    return (IApplicationBuilder) this;
}

从上面四段代码我们可以得到这样的信息:一个管道其实就是一个Func<RequestDelegate, RequestDelegate>类型的委托,接收一个RequestDelegate类型的委托,同时也返回一个RequestDelegate类型的委托,这个RequestDelegate委托的参数又是一个HttpContext,每次调用Use添加中间件时都会往_components这个数组的最后添加这个新的管道,下面这段是核心代码:

app.Use((Func<RequestDelegate, RequestDelegate>) (next => (RequestDelegate) (context =>
                                                                                        {
                                                                                            Func<Task> func = (Func<Task>) (() => next(context));
                                                                                            return middleware(context, func);
                                                                                        })));

这个Func<RequestDelegate, RequestDelegate>中第一个参数是入参,也就是调用下一个管道的委托,第二个参数是返回值,也就是调用当前管道的委托,这里比较难理解,我们需要分解来看:

context =>
{
    Func<Task> func = (Func<Task>) (() => next(context));
    return middleware(context, func);
}

这是一个RequestDelegate委托,里面做的事情就是把调用下一个管道的操作封装成立一个Func委托,然后传入当前的Func<HttpContext, Func<Task>, Task> middleware委托中,也就是我们最开始写的那一段代码,调用下一个管道的委托对应了我们自己代码中的next委托,在知道这部分的作用后,我们简化一下,记作:A

(Func<RequestDelegate, RequestDelegate>) (next => (RequestDelegate) (A))

简化之后替换一下,再看,就会发现这是一个Func<RequestDelegate, RequestDelegate>委托,第一个参数是调用下一个管道的委托,第二个是返回值,代表了调用当前管道的委托(套娃操作,这一步最难理解,需要多看几遍),这样做有什么用呢?继续往下看

最后在构建WebHostBuilder的时候,内部Build函数会利用这个来组装整个调用链:

/// <summary>
    /// Produces a <see cref="T:Microsoft.AspNetCore.Http.RequestDelegate" /> that executes added middlewares.
    /// </summary>
    /// <returns>The <see cref="T:Microsoft.AspNetCore.Http.RequestDelegate" />.</returns>
    public RequestDelegate Build()
    {
      RequestDelegate requestDelegate = (RequestDelegate) (context =>
      {
        Endpoint endpoint = context.GetEndpoint();
        if (endpoint?.RequestDelegate != null)
          throw new InvalidOperationException("The request reached the end of the pipeline without executing the endpoint: '" + endpoint.DisplayName + "'. Please register the EndpointMiddleware using 'IApplicationBuilder.UseEndpoints(...)' if using routing.");
        context.Response.StatusCode = 404;
        return Task.CompletedTask;
      });
      for (int index = this._components.Count - 1; index >= 0; --index)
        requestDelegate = this._components[index](requestDelegate);
      return requestDelegate;
    }

要注意的是这里是从后往前遍历的,这里解释一下上面这段代码的作用:

  1. 首先先使用GetEndPoint()函数获取管道的终结点,可以简单理解为找出http请求最终要调用到的方法
  2. 上一步中得到了管道的最后一个位置,然后开始从后往前遍历所有的管道,并且把调用下一个管道的委托传入上一个管道作为参数

理解了上面说的话之后其实一切都已经清晰了,在委托链组装完毕后,第一个RequestDelegate内部的逻辑其实就是处理自己管道内部的业务,然后调用下一个requestDelegate(也就是next),然后一直调用下去直到到达最后的终点,或者中间没有调用next(),再总结一下:

利用上面已经组装好的委托链,当一个http请求进来的时候,.Net Core内部就会把这个http请求转换成一个HttpContext类型的参数,然后从一个管道开始处理,当一个管道处理完成后,会调用下一个委托并传入这个HttpContext继续处理相应逻辑,当然,如果你自定义的管道没有调用触发下一个管道的RequestDelegate委托,那么请求就到此中断了

我们可以发起一个请求debug调试看看是不是这样,在发起一个http请求后,我们会发现调用到了下面这段代码:

var context = application.CreateContext(this);

try
{
    KestrelEventSource.Log.RequestStart(this);

    // 重点!!
    // Run the application code for this request
    await application.ProcessRequestAsync(context); 

    // Trigger OnStarting if it hasn't been called yet and the app hasn't
    // already failed. If an OnStarting callback throws we can go through
    // our normal error handling in ProduceEnd.
    // https://github.com/aspnet/KestrelHttpServer/issues/43
    if (!HasResponseStarted && _applicationException == null && _onStarting?.Count > 0)
    {
        await FireOnStarting();
    }

    if (!_connectionAborted && !VerifyResponseContentLength(out var lengthException))
    {
        ReportApplicationError(lengthException);
    }
}

ProcessRequestAsync这个函数的调用如下:

private readonly RequestDelegate _application;

//...其他省略代码

public Task ProcessRequestAsync(HostingApplication.Context context) => this._application(context.HttpContext);

从这里我们就可以看出,_application是第一个委托,调用到这个之后很自然的就跟我们上面说得一样了,一直往下调用就完事了

总的来说,其实管道调用链组装部分其实不难理解,最难理解的是Use那段核心代码,委托套委托,每次看都会绕晕,得花很长时间才能理清里面的逻辑,如果看完之后还是觉得没看懂的话,可以自己建一个.NET 6版本的Minimal Api的Webapi项目打开源码慢慢分析,相信多花些时间肯定还是能看明白的

posted @ 2023-01-12 09:24  李正浩  阅读(83)  评论(0编辑  收藏  举报