一个迷你ASP.NET Core框架的实现(下)

【框架内幕作者 / Edison Zhou

这是恰童鞋骚年的第196篇原创文章


上一篇我们了解了AspNetCore.Mini这个项目的背景及项目结构和流程,这一篇我们继续解析几个核心对象。本文整理自A大(蒋金楠)的主题分享,点击本文底部“阅读原文”可以观看A大的该主题视频分享。

1基于HttpListener的WebServer

刚刚在WebHost中注入了Server,并启动了Server。那么,这个Server长啥样呢?我们知道,在ASP.NET Core中封装了Kestrel和IIS两个Server供我们使用,那么它们肯定有一个抽象层(这里是接口),定义了他们共有的行为,这里我们也写一个IServer:

public interface IServer
{
    Task RunAsync(RequestDelegate handler);
}

IServer接口行为很简单,就是约定一个启动的方法RunAsync,接受参数是中间件(本质就是一个请求处理的委托)。

有了IServer接口,就可以基于IServer封装基于不同平台的WebServer了,这里基于HttpListener实现了一个HttpListenerServer如下(HttpListener简化了Http协议的监听,仅需通过字符串的方法提供监听的地址和端口号以及虚拟路径,就可以开始监听请求):

public class HttpListenerServer : IServer
{
    private readonly HttpListener _httpListener;
    private readonly string[] _urls;


    public HttpListenerServer(params string[] urls)
    {
        _httpListener = new HttpListener();
        // 绑定默认监听地址(默认端口为5000)
        _urls = urls.Any() ? urls : new string[] { "http://localhost:5000/" };
    }


    public async Task RunAsync(RequestDelegate handler)
    {
        Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url));
        if (!_httpListener.IsListening)
        {
            // 启动HttpListener
            _httpListener.Start();
        }
        Console.WriteLine("[Info]: Server started and is listening on: {0}", string.Join(";", _urls));


        while (true)
        {
            // 等待传入的请求,该方法将阻塞进程(这里使用了await),直到收到请求
            var listenerContext = await _httpListener.GetContextAsync();
            // 打印状态行: 请求方法, URL, 协议版本
            Console.WriteLine("{0} {1} HTTP/{2}", 
                listenerContext.Request.HttpMethod, 
                listenerContext.Request.RawUrl, 
                listenerContext.Request.ProtocolVersion);
            // 获取抽象封装后的HttpListenerFeature
            var feature = new HttpListenerFeature(listenerContext);
            // 获取封装后的Feature集合
            var features = new FeatureCollection()
                .Set<IHttpRequestFeature>(feature)
                .Set<IHttpResponseFeature>(feature);
            // 创建HttpContext
            var httpContext = new HttpContext(features);
            Console.WriteLine("[Info]: Server process one HTTP request start.");
            // 开始依次执行中间件
            await handler(httpContext);
            Console.WriteLine("[Info]: Server process one HTTP request end.");
            // 关闭响应
            listenerContext.Response.Close();
        }
    }
}


/// <summary>
/// IWebHostBuilder扩展:使用基于HttpListener的Server
/// </summary>
public static partial class Extensions
{
    public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder, params string[] urls)
        => builder.UseServer(new HttpListenerServer(urls));
}

有了Server,也有了中间件,我们要进行处理的上下文在哪里?熟悉ASP.NET请求处理的童鞋都知道,我们会操作一个叫做HttpContext的东西,它包裹了一个HttpRequest和一个HttpResponse,我们要进行的处理操作就是拿到HttpRequest里面的各种参数进行处理,然后将返回的结果包裹或调用HttpResponse的某些方法进行响应返回。在ASP.NET Core Mini中,也不例外,我们会创建一个HttpContext,然后将这个HttpContext传递给注册的中间件,各个中间件也可以拿到这个HttpContext去做具体的处理了。但是,不同的Server和单一的HttpContext之间需要如何适配呢?因为我们可以注册多样的Server,可以是IIS也可以是Kestrel还可以是这里的HttpListenerServer

这时候,我们又可以提取一个抽象层了,如上图所示,底层是具体的基于不同平台技术的Server,上层是HttpContext共享上下文,中间层是一个抽象层,它是基于不同Server抽象出来的接口,本质是不同Server的适配器,下面就是这个IFeature的定义:

public interface IHttpRequestFeature
{
    Uri Url { get; }


    NameValueCollection Headers { get; }


    Stream Body { get; }
}


public interface IHttpResponseFeature
{
    int StatusCode { get; set; }


    NameValueCollection Headers { get; }


    Stream Body { get; }
}

这里不再解释,下面来看看HttpListener的适配的实现:

public class HttpListenerFeature : IHttpRequestFeature, IHttpResponseFeature
{
    private readonly HttpListenerContext _context;


    public HttpListenerFeature(HttpListenerContext context) => _context = context;


    Uri IHttpRequestFeature.Url => _context.Request.Url;


    NameValueCollection IHttpRequestFeature.Headers => _context.Request.Headers;


    NameValueCollection IHttpResponseFeature.Headers => _context.Response.Headers;


    Stream IHttpRequestFeature.Body => _context.Request.InputStream;


    Stream IHttpResponseFeature.Body => _context.Response.OutputStream;


    int IHttpResponseFeature.StatusCode
    {
        get { return _context.Response.StatusCode; }
        set { _context.Response.StatusCode = value; }
    }
}

可以看出,这是一个典型的适配器模式的应用,通过一个抽象层接口,为不同Server提供HttpRequest和HttpResponse对象的核心属性。

2Middleware与ApplicationBuilder

在启动项目中,定义了三个中间件如下所示:

public static RequestDelegate FooMiddleware(RequestDelegate next)
=> async context =>
{
    await context.Response.WriteAsync("Foo=>");
    await next(context);
};


public static RequestDelegate BarMiddleware(RequestDelegate next)
=> async context =>
{
    await context.Response.WriteAsync("Bar=>");
    await next(context);
};


public static RequestDelegate BazMiddleware(RequestDelegate next)
=> context => context.Response.WriteAsync("Baz");

可以看到,每个中间件的作用都很简单,就是向响应流中输出一个字符串。其中Foo和Bar两个中间件在输出之后,还会调用下一个中间件进行处理,而Baz不会调用下一个中间件进行处理,因此Baz在注册顺序上排在了最后,这也解释了我们为何在ASP.NET Core中进行中间件的注册时,注册的顺序比较讲究,因为这会影响到后面的执行顺序。

刚刚在进行WebHost的创建时,调用了WebHostBuilder的Configure方法进行中间件的注册,而这个Configure方法的输入参数是一个IApplicationBuilder的委托:

public interface IApplicationBuilder
{
    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);


    RequestDelegate Build();
}

可能直接看这个接口定义不是太明白,下面来看看ApplicationBuilder的实现:

public class ApplicationBuilder : IApplicationBuilder
{
    private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares = new List<Func<RequestDelegate, RequestDelegate>>();


    /// <summary>
    /// 构建请求处理管道
    /// </summary>
    /// <returns>RequestDelegate</returns>
    public RequestDelegate Build()
    {
        _middlewares.Reverse(); // 倒置注册中间件集合的顺序
        return httpContext =>
        {
            // 注册默认中间件 => 返回404响应
            RequestDelegate next = _ => {
                _.Response.StatusCode = 404;
                return Task.CompletedTask;
            };


            // 构建中间件处理管道
            foreach (var middleware in _middlewares)
            {
                next = middleware(next);
            }


            return next(httpContext);
        };
    }


    /// <summary>
    /// 注册中间件
    /// </summary>
    /// <param name="middleware">中间件</param>
    /// <returns>ApplicationBuilder</returns>
    public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
    {
        _middlewares.Add(middleware);
        return this;
    }
}

其中,Use方法的作用就是接受中间件进行注册,Build方法的作用就是构建由注册中间件组成的请求处理管道,而Server加上这个由中间件组成的请求处理管道便是ASP.NET Core的核心内容。因此,我们可以说ASP.NET Core Pipeline = Server + Middlewares。此外,我们还可以将多个Middleware构建成一个单一的“HttpHandler”,那么整个ASP.NET Core框架将具有更加简单的表达:Pipeline = Server + HttpHandler

因此,这里的Build方法中做了以下几件事情:

(1)倒置注册中间件集合的顺序

_middlewares.Reverse();

为什么要倒置顺序呢?不是说执行顺序要跟注册顺序保持一致么?别急,且看后面的代码。

(2)注册默认中间件

return httpContext =>
{
        // 注册默认中间件 => 返回404响应
        RequestDelegate next = _ => {
            _.Response.StatusCode = 404;
            return Task.CompletedTask;
        };


        ......
}

这里默认中间件是返回404,在如果没有手动注册任何中间件的情况下生效。

(3)构建一个中间件处理管道 => "HttpHandler"

public RequestDelegate Build()
{
    ......
    return httpContext =>
    {
        ......


        // 构建中间件处理管道
        foreach (var middleware in _middlewares)
        {
            next = middleware(next);
        }


        return next(httpContext);
    };
}

在通过Use方法注册多个中间件到middlewares集合中后,会在这里通过一个遍历组成一个单一的middleware(在这里表示为一个RequestDelegate对象),如下图所示。

对于middleware,它在这里是一个Func<RequestDeletegate, RequestDelegate>对象,它的输入和输出都是RequestDelegate。

对于管道的中的某一个middleware来说,由后续middleware组成的管道体现为一个RequestDelegate对象,由于当前middleware在完成了自身的请求处理任务之后,往往需要将请求分发给后续middleware进行处理,所以它需要将由后续中间件构成的RequestDelegate作为输入。当代表中间件的委托对象执行之后,我们希望的是将当前中间件“纳入”这个管道,那么新的管道体现的RequestDelegate自然成为了输出结果

因此,这里也就解释了为什么要在第一步中进行middleware的顺序的倒置,否则无法以注册的顺序构成一个单一的middleware,下图是示例代码中的所有middleware构成的一个单一的RequestDelegate,经过层层包裹,以达到依次执行各个middleware的效果。需要注意的就是在BazMiddleware中,没有调用下一个中间件,因此404中间件便不会得到触发处理的机会。

下图是最后的执行结果:

3小结

经过蒋金楠老师的讲解以及自己的学习,对这个Mini版的ASP.NET Core框架有了一个初步的理解,正如蒋老师所说,ASP.NET Core的核心就在于由一个服务器和若干中间件构成的管道,了解了这一点,就对ASP.NET Core的核心本质有了大概印象。当然,这个Mini版的ASP.NET Core只是模拟了ASP.NET Core的冰山一角,还有许多的特性都没有,比如基于Starup来注册中间件,依赖注入框架,配置系统,预定义中间件等等等等,但是对于广大ASP.NET Core学习者来说是个绝佳的入门,是进阶学习ASP.NET Core源码的基石读物,最后感谢大内老A的分享!

最后让我们一起期待A大的《ASP.NET Core框架揭秘》早日出版!

4参考资料

蒋金楠,《200行代码,7个对象—让你了解ASP.NET Core框架的本质》

蒋金楠,《Inside ASP.NET Core Framework》

Edison Zhou,https://github.com/EdisonChou/AspNetCore.Mini

????点击阅读原文,观看A大分享

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值