一个Mini的ASP.NET Core框架的实现

原文:一个Mini的ASP.NET Core框架的实现

一、ASP.NET Core Mini

  在2019年1月的微软技术(苏州)俱乐部成立大会上,蒋金楠老师(大内老A)分享了一个名为“ASP.NET Core框架揭秘”的课程,他用不到200行的代码实现了一个ASP.NET Core Mini框架,重点讲解了7个核心对象,围绕ASP.NET Core最核心的本质—由服务器和若干中间件构成的管道来介绍。我在腾讯视频上看到了这个课程的录像,看了两遍之后结合蒋金楠老师的博客《200行代码,7个对象—让你了解ASP.NET Core框架的本质》一文进行了学习并下载了源代码进行研究,然后将其改成了基于.NET Standard的版本,通过一个.NET Framework和一个.NET Core的宿主端来启动一个ASP.NET Core的Server,并将其放到了GitHub上,欢迎Clone学习。

  

  ASP.NET Core Mini是一个十分值得学习的小项目,它真实模拟了ASP.NET Core的核心,而且又足够简单(不到200行代码),最重要的是它可以执行(我们可以通过Debug的方式一步一步地查看)。本文基于蒋金楠老师的那篇博客,基于学习者的视角Run一遍这个ASP.NET Core Mini框架,一步一步地了解它的流程,了解中间件在ASP.NET Core中的作用。当然,最好先看看蒋金楠老师的博客和ASP.NET Core Mini的代码,本文只是我的一个学习总结,部分文字来源于蒋金楠老师的博文。

二、Run起来看流程

2.1 项目结构与整体流程一览

  这个示例项目由三部分组成:

  第一部分是AspNetCore.Mini.Core,这是一个ASP.NET Core框架的Mini实现,封装在了一个.NET Standard 2.0的类库中,可以供.NET Framework和.NET Core应用程序使用;

  第二部分是AspNetCore.Mini.App,这是一个基于.NET Framework 4.6.1的控制台应用程序,它是一个使用了AspNetCore.Mini.Core的宿主程序,可以直接执行;

  第三部分是AspNetCore.Mini.AppCore,这是一个基于.NET Core 2.1的控制台应用程序,它是一个使用了AspNetCore.Mini.Core的宿主程序,可以直接执行;

  宿主程序的核心启动代码如下所示:

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args)
                .Build()
                .Run();

            Console.ReadKey();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args)
        {
            return new WebHostBuilder()
                .UseHttpListener()
                .Configure(app => app
                    .Use(FooMiddleware)
                    .Use(BarMiddleware)
                    .Use(BazMiddleware));
        }

        #region 自定义中间件

        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"); 

        #endregion
    }

  整个项目的运行流程大致如下图所示:

  首先,会通过一个WebHostBuilder来构造一个WebHost,这个过程会经历指定具体的Server(比如ASP.NET Core中的Kestrel或IIS等等),然后指定要注册的中间件(比如MVC中间件,文件服务中间件,内存缓存中间件等等)。构造好了WebHost之后,便会启动这个WebHost,启动这个WebHost的核心就在于启动刚刚注册的Server,让它绑定指定的端口开始监听(这部分内容涉及到Socket网络程序,不熟悉的朋友可以看看我的这一篇《自己动手模拟开发一个简单的Web服务器》)请求,当有请求到达时便会进行相应的请求处理流程。而这里的请求处理流程主要是封装请求上下文,依次调用注册的中间件进行处理,然后结束请求处理流程,这时候用户就可以在浏览器中看到响应的内容了。

  上面介绍了一个大概的流程,下面我们就来具体看看每一步的具体内容。

2.2 WebHost与WebHostBuilder

  WebHostBuilder,看这个名字就应该知道它采用了Builder模式,它的目的也很明确:创建作为应用宿主的WebHost。而在创建WebHost的时候,需要提供注册的服务器和由所有注册中间件构建而成的请求处理委托集,其接口IWebHostBuilder定义如下:

    public interface IWebHostBuilder
    {
        IWebHostBuilder UseServer(IServer server);
        IWebHostBuilder Configure(Action<IApplicationBuilder> configure);
        IWebHost Build();
    }

  其中,UserServer方法用来指定要运行的Server,在ASP.NET Core中我们经常用到的是UseKestrel()方法来指定要运行的Server是Kestrel,这是一个基于libuv的跨平台ASP.NET Core web服务器。Configure方法则主要用来注册中间件,其中IApplicationBuilder是一个请求处理的核心构造器接口,它是注册和使用中间件的入口。

  下面是在示例项目中实现的一个WebHostBuilder类:

    public class WebHostBuilder : IWebHostBuilder
    {
        private IServer _server;
        private readonly List<Action<IApplicationBuilder>> _configures = new List<Action<IApplicationBuilder>>();

        /// <summary>
        /// 配置中间件
        /// </summary>
        /// <param name="configure">中间件</param>
        /// <returns>IWebHostBuilder</returns>
        public IWebHostBuilder Configure(Action<IApplicationBuilder> configure)
        {
            _configures.Add(configure);
            return this;
        }

        /// <summary>
        /// 指定要使用的具体Server
        /// </summary>
        /// <param name="server">具体Server</param>
        /// <returns>IWebHostBuilder</returns>
        public IWebHostBuilder UseServer(IServer server)
        {
            _server = server;
            return this;
        }

        /// <summary>
        /// 构造具体WebHost应用宿主
        /// </summary>
        /// <returns>IWebHost</returns>
        public IWebHost Build()
        {
            var builder = new ApplicationBuilder();
            foreach (var configure in _configures)
            {
                configure(builder);
            }

            return new WebHost(_server, builder.Build());
        }
    }

  可以看到,其核心就在于Build方法:创建一个WebHost实例,这个WebHost实例会关联到指定的Server以及注册的中间件集合。

  那么,这个WebHost又长啥样呢?先来看看IWebHost接口:只定义了一个Run方法,它是启动Server的入口。

    public interface IWebHost
    {
        Task Run();
    }

  下面是WebHost的实现,其核心就在于将中间件传递给Server并启动Server:

    public class WebHost : IWebHost
    {
        private readonly IServer _server;
        private readonly RequestDelegate _handler;

        public WebHost(IServer server, RequestDelegate handler)
        {
            _server = server;
            _handler = handler;
        }

        /// <summary>
        /// 调用Server的启动方法进行启动
        /// </summary>
        /// <returns></returns>
        public Task Run() => _server.RunAsync(_handler);
    }

2.3 基于HttpListener的Server

  刚刚在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对象的核心属性。

2.4 Middleware与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中间件便不会得到触发处理的机会。

  下图是最后的执行结果:

  

三、小结

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

参考资料

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

蒋金楠,《Inside ASP.NET Core Framework

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

原文地址:https://www.cnblogs.com/lonelyxmas/p/10713792.html

时间: 2024-10-10 18:22:39

一个Mini的ASP.NET Core框架的实现的相关文章

了解ASP.NET Core框架的本质

ASP.NET Core自身的运行原理和设计思想创建了一个 “迷你版” 的ASP.NET Core框架,并且利用这个 “极简” 的模拟框架阐述了ASP.NET Core框架最核心.最本质的东西.整个框架涉及到的核心代码不会超过200行,涉及到7个核心的对象. PPT下载源代码下载 目录1. 从Hello World谈起2. ASP.NET Core Mini3. Hello World 24. 第一个对象:HttpContext5. 第二个对象:RequetDelegate6. 第三个对象:Mi

ASP.NET Core框架深度学习(一) Hello World

对于学习Core的框架,对我帮助最大的一篇文章是Artech的<200行代码,7个对象——让你了解ASP.NET Core框架的本质>,最近我又重新阅读了一遍该文.本系列文章就是结合我的阅读心得,一起来搭建一个迷你的Core框架. 本文相关代码在码云上,链接如下 https://gitee.com/qixinbo/MyKestrelServer/tree/master/CoreMini/CoreMini 还有部分是core的源码,链接如下 https://github.com/aspnet/A

ASP.NET Core框架揭秘 链接

将金楠老师的ASP.NET Core框架揭秘 http://www.cnblogs.com/artech/p/inside-asp-net-core-1.html 第一部分 编程基础 如果没有Visual Studio 2015,我们如何创建.NET Core项目 ?通过几个Hello World感受.NET Core全新的开发体验难道.NET Core到R2连中文编码都不支持吗? 第二部分 基础框架 依赖注入        控制反转(IoC)        依赖注入(DI)        服务

一步步完成“迷你版” 的ASP.NET Core框架

一 前言 Artech 分享了 200行代码,7个对象--让你了解ASP.NET Core框架的本质 . 用一个极简的模拟框架阐述了ASP.NET Core框架最为核心的部分. 这里一步步来完成这个迷你框架. 二 先来一段简单的代码 这段代码非常简单,启动服务器并监听本地5000端口和处理请求. static async Task Main(string[] args) { HttpListener httpListener = new HttpListener(); httpListener.

3.介绍ASP.NET Core框架

介绍ASP.NET Core框架 在这篇文章中,我将要向你们简短介绍一下ASP.NET Core 框架.当今社会,当提到软件开发,每个人都是讨论着开源以及跨平台开发.总所周知,微软是以它的基于Windows产品出名的,比如Windows系统,Office办公套件等.现在我们处在新时代软件开发的潮流中,一个新的革命性的产品,被微软推出市场,那就是-----ASP.NET Core.作为本文的一部分,我将详细述说下面几点. ASP.NET的历史 什么是ASP.NET Core ASP.NET Cor

ASP.NET Core 框架源码地址

ASP.NET Core 框架源码地址 https://github.com/dotnet/corefx 这个是.net core的 开源项目地址 https://github.com/aspnet 这个下面是asp.net core 框架的地址,里面有很多仓库. https://github.com/aspnet/EntityFrameworkCore  EF Core源码 https://github.com/aspnet/Configuration 配置模块源码 https://githu

[ASP.NET Core 3框架揭秘] 依赖注入:一个Mini版的依赖注入框架

在前面的章节中,我们从纯理论的角度对依赖注入进行了深入论述,我们接下来会对.NET Core依赖注入框架进行单独介绍.为了让读者朋友能够更好地理解.NET Core依赖注入框架的设计与实现,我们按照类似的原理创建了一个简易版本的依赖注入框架,也就是我们在前面多次提及的Cat. 源代码下载 普通服务的注册与消费泛型服务的注册与消费多服务实例的提供服务实例的生命周期 一.编程体验 虽然我们对这个名为Cat的依赖注入框架进行了最大限度的简化,但是与.NET Core框架内部使用的真实依赖注入框架相比,

ASP.NET Core框架揭秘(持续更新中…)

之前写了一系列关于.NET Core/ASP.NET Core的文章,但是大都是针对RC版本.到了正式的RTM,很多地方都发生了改变,所以我会将之前发布的文章针对正式版本的.NET Core 1.0进行改写.除此之外,我还会撰写一系列与此相关的文章,这些文章以ASP.NET Core为核心,我个人将它们分成三个主要的部分,即编程基础.支撑框架和管道详解.其中编程基础主要涉及与ASP.NET Core独特的编程模型和相关编程技巧.支撑框架则介绍支撑ASP.NET Core的多个独立的框架,比如依赖

通过极简模拟框架让你了解ASP.NET Core MVC框架的设计与实现[上篇]

<200行代码,7个对象--让你了解ASP.NET Core框架的本质>让很多读者对ASP.NET Core管道有了真实的了解.在过去很长一段时间中,有很多人私信给我:能否按照相同的方式分析一下MVC框架的设计与实现原理,希望这篇文章能够满足你们的需求.在对本章内容展开介绍之前,顺便作一下广告:<ASP.NET Core 3框架揭秘>已经开始销售,现时5折优惠还有最后4天,有兴趣的从这里入群购买. 目录一.Action元数据的解析     ActionDescriptor