Spiga

ASP.NET Core 7 源码阅读2:Http管道和Kestrel

2023-05-29 10:48:46

一、HttpPipeline

1. 理解Http请求流程

Http请求响应流程,也就是浏览器输入个地址,发生了什么事儿:

  1. 浏览器输入地址, www.xxx.com
  2. DNS解析,找到IP+Port,然后浏览器向该地址发Http报文---纯文本
  3. Nginx/IIS/Kestrel监听端口,收到报文解析得到HttpContext
  4. 将请求转发到业务代码处理---怎么进入到controller+action?
  5. 处理结果由服务器回发到客户端,浏览器解析报文完成渲染

所谓HttpPipeline就是程序如何处理请求的全过程---

理解Http管道所处的位置: Web服务器解析报文之后,在Web服务器回发报文之前

Http管道和controller-action的关系:其实是包含关系,任何处理动作都是管道的一部分

2. 理解Http管道

HttpPipeline本质是个啥?

接受HttpContext,然后做一系列的处理(Cookie Session 鉴权授权 缓存 路由 MVC)---最终将结果保存在HttpContext的Response里面

然后在ASP.NET Core里面,就被抽象成为一个委托—RequestDelegate---接受一个HttpContext,然后执行一系列操作

但是开发中,管道模型是很复杂的呀?因为Http请求的处理并不简单,包含了很多环节---Cookie/Session/鉴权授权/缓存/https/静态文件-------还有就是各种不同的业务处理需求(M-V-C)-----还有开发者的扩展需求(限流—黑名单白名单)-----所以需要一套扩展性的框架

这套实现,就是ASP.NET Core的HttpPipeline

3. 管道模型上帝视角

  1. 先创建个WebApplicationBuilder()---然后各种配置IOC/Logging/Configuration---然后Build()完成了各种初始化---得到WebApplication
  2. WebApplication去Use添加添加中间件(委托)---实际上是ApplicationBuilder在Use------就是把中间件(Func委托)保存到一个集合
  3. 最后框架在Run的时候,会执行ApplicationBuilder的Build方法,将委托集合遍历调用,组装成最终的RequestDelegate,也就是Http管道

4. 源码阅读

  1. 其他各种初始化,IOC注册等,在WebApplicationBuilder.Build()得到WebApplication(这时候中间件还没出场)----然后是Run之前会BuildApplication---IApplicationFactory—得到ApplicationBuilder
    • IOC获取全部的StartupFilter---然后把中间件配置动作包装成一个委托,然后倒序遍历StartupFilter,分别执行Configure方法,得到一个委托---
    • 最后执行这个委托,等同于先执行Filter里面的配置,再执行默认Use中间件配置
    • 然后就Build

​ 结论:这里就调用了各种Use,且在Use配置前面,又留下了一个扩展点

  1. ApplicationBuilder的Use方法,就是把中间件保存到_components对象ApplicationBuilder的Build方法,就是倒序遍历components,执行外层Func委托,最终得到其实就是RequestDelegate---这就是管道模型

    这个RequestDelegate里面是嵌套了一系列的委托,请求来了会执行一堆动作

    这真的是一个很cool的设计,只是利用了委托的嵌套双层委托

    Use时是2层委托---Build就执行外层所以流程就可以随意组装-随意扩展

    public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegatee> middleware)
    {
    	_components.Add(middleware);
    	return this;
    }
    
    public RequestDelegate Build()
    {
    	RequestDelegate app = context => ...//屏蔽细节,什么都不处理的时候返回404
        for(var c = _components.Count - 1; c >= 0; c--)
        {
        	app = _components[c](app);
        }
        return app;
    }
    

3. HttpPipeLine设计

后面还有各种封装,各种中间件的用法,但其实都是在基础Use上封装的,理解下ASP.NET Core HttpPipeline的设计?!

  1. 多层委托嵌套组装,最终呈现为一个委托
  2. 达到俄罗斯套娃效果,方便扩展

4. ASP.NET/MVC HttpPipeline

ASP.NET/MVC时代,Http管道是如何设计的,如何满足扩展性?

那里面也有一个Application对象---请求的全部环节都是它处理---考虑到开发者的扩展,提供了19个事件,外部可以通过+=来扩展动作-----请求处理时,按顺序去触发事件这样框架就具备了扩展性---

要扩展动作,就是通过HttpModule

5. 全家桶 VS 自选

ASP.NET和ASP.NET MVC是一套管道,属于全家桶----管道的固定动作都写死了,比如一定检测/生成Session----要也得要,不要也得要

ASP.NET Core管道---完全自选式,没有任何的事先写死(只有那个404)---需要任何东西,都是自行配置—要什么组装什么---Pay for what you use

所以:ASP.NET Core的性能会更高,对使用者的要求也更高

6. IStartupFilter扩展

  1. 执行在Use之前,可以直接组装中间件
  2. 扩展CustomStartupFilter
  3. 注册IOC容器
  4. 可自行尝试下,放在前后的不同

StartupFilter:管道外面包一层(可前可后)---黑白名单/反爬虫/缓存/限流

HostingStartup:更早执行,支持更多配置----无侵入式扩展

不仅中间件前后可以随意扩展---这还增加了不同的扩展中间件的方式

7. Middleware扩展演示

除了最原生app.Use,还有多种扩展使用方式:

  1. app.Run
  2. app.Use---无Next
  3. app.Use其他
  4. app.UseWhen
  5. app.Map
  6. app.MapWhen

总结下:无非就是在委托加一层包装,再就是加一层判断,最终还是靠ApplIcationBuider的Use那套东西

8. UseMiddleware

app.UseMiddleware()这个跟前面的稍微有点区别,分4种情况:

  1. FirstMiddleware:构造函数带个委托+Invoke/invokeAsync(只能有一个)方法即可,然后直接app.UseMiddleware():
  2. SecondMiddleware:实现IMiddleware接口,需要IOC注册
  3. SecondMiddleware+ SecondMiddlewareFactory:是在2的情况下,再做工厂替换builder.Services.Replace(ServiceDescriptor.Singleton<IMiddlewareFactory, SecondMiddlewareFactory>());
  4. ThirdMiddleware:中间件要做注入

9. Middleware类源码

UseMiddleware会检测类型是否实现接口,然后走不通的分支。普通Middleware类,没有实现IMiddleware接口,是怎么调用的?

其实就是各种检测---然后各种硬编码赋值构造函数,最终还是调用的app.Use非常多的小细节,可以自己去细看

  1. 没有接口约束,所以各种检查校验
  2. 对象初始化靠反射,但是构造参数支持依赖注入
  3. 构造函数支持传入参数---第一个参数是Next方法名字Invoke---InvokeAsync

10. IMiddleware源码

实现IMiddleware接口,有啥区别?

  1. 实现IMiddleware接口---需要注册IOC---每次请求来了都要重新生成,而且可以支持资源释放,可以写一个Dispose方法
  2. 依赖IMiddlewareFactory,默认是MiddlewareFactory,可以扩展,注册---然后会自动释放资源

对比总结

  1. 普通Middleware类是启动时,在组装管道模型时实例化,嵌套委托常驻内存---------初始化支持传参数
  2. 接口实现Middleware类是响应请求的时候才实例化,每次请求来了才实例化,支持主动释放---------默认实例化不能传参数

更多时候,用的是第1种----只要中间件自身不考虑资源释放

11. 标准中间件封装

中间件的源码层面,学得很透彻了----下面是应用封装层面

框架有各式各样的中间件,当下基本原理已经清楚了,下面也按照标准模式来一套封装:

  1. 基本结构认知:UseMiddleware + AddIOC
  2. UseMiddleware封装一套
  3. 3 AddIOC封装一套
  4. Options

12. 常见Middleware扩展

  1. 全部请求都要执行的-----日志/性能监控/跨域/压缩/前端缓存---为所有的请求添加通用操作

  2. 包裹在外层做请求拦截---黑白名单/反爬虫/限流/链路追踪---可装配可拆卸的这种操作

  3. 应对特殊条件的请求-----UseWhen/MapWhen---Robot/RSS/防盗链/静态文件中间件----只针对某种请求

    备注: 1和2可以合并

13. Stream读取问题

Http的响应应该是先Header然后再Body---写完之后不能随意改动---是框架限制的----因为请求响应已完成,里面的Content-Length已固定,所以不能改-----所以,中间件的扩展其实是有限制的,不能随意读取和修改请求内容,也不能随意读取和修改响应内容---那怎么改? ---OnStarting可以修改Header---但是我想读写Request和Response—就需要特殊的处理办法

自定义StreamReadMiddleware 和 StreamWriteMiddleware源码

二、Kestrel

1. 理解Web服务器

Http请求响应流程:

  1. 浏览器输入地址---DNS解析---IP:Port---发送请求报文,其实就是固定格式的字符串
  2. Nginx/IIS/Kestrel监听端口---解析Http请求报文,得到HttpContext
  3. 将HttpContext交给HttpPipeline执行,把各种信息写入response对象---服务器再把内容转成响应报文,回发浏览器
  4. 浏览器接受响应报文,解析,渲染

2. 理解Kestrel服务器

  1. Kestrel是开源的事件驱动的异步I / O服务器, .NET Core程序,开源地址:https://github.com/aspnet/KestrelHttpServer

  2. IIS不能跨平台的,而Kestrel是跨平台---.NET Core能跨平台,一是运行时环境能支持,再就是Web服务器能跨平台,以前是Mono+Jexus

  3. Web服务器跨平台最核心就是异步IO实现,这里用的是libuv

    libuv is a multi-platform support library with a focus on asynchronous I/O. It was primarily developed for use by Node.js---Kestrel

3. Kestrel使用建议

Comparing Kestrel Web Server vs IIS----MATT WATSONIIS does almost everything. Kestrel does as little as possible. Because of this, Kestrel is much faster but also lacks a lot of functionality. I would think of Kestrel as really more of an application server. It is recommended to use another web server in front of it for public web applications. Kestrel is designed to run ASP.NET as fast as possible. It relies on a full fledged web server to do deal with things like security, management, etc.

https://stackify.com/kestrel-web-server-asp-net-core-kestrel-vs-iis/

4. ASP.NET Core部署

  1. ASP.NET Core脚本启动----裸Kestrel
  2. IIS+ASP.NET Core部署----编译目录不行,需要发布一下,且IIS要安装ASP.NETCore Module v2------编译和发布就差个web.config
  3. Nginx部署----

推荐:

  • 如果不对外,直接Kestrel,足够快
  • 如果对外, Windows就IIS,一套技术栈,是Linux自然是Nginx
  • 需要负载均衡,推荐Nginx

5. Kestrel配置

WebApplication.CreateBuilder(args);默认启用Kestrel

  1. 可以通过builder.WebHost.ConfigureKestrel配置KestrelServerOptions
  2. 通过配置文件完成配置

通常就是用的默认配置

文档:https://learn.microsoft.com/zhcn/aspnet/core/fundamentals/servers/kestrel/options?view=aspnetcore-7.0

6. Kestrel源码-上帝视角

Kestrel源码,这里分为2层来解析,含Kestrel启动过程和Kestrel工作过程

启动过程(asp.net core如何初始化+启动的)上帝视角:

  1. builder.Build()时,完成Kestrel服务注入IServer, KestrelServerImpl
  2. 然后组装Http管道,然后app.Run()时,将Http管道(委托)交付给Kestrel
  3. Ketrel根据配置的监听信息,启动线程死循环监听,有请求再交给Http管道处理

工作过程(Kestrel自身的实现源码)上帝视角:

  1. ASP.NET Core程序将日志/IOC/Http管道/HttpContextFactory等打包给Kestrel,Kestrel检查参数然后完成StartAsync
  2. Kestrel循环监听端口,绑定处理动作:根据不同的Http协议,创建不同的处理管道KestrelConnection,然后Http管道是其中的一环
  3. 开启线程监听请求,每个请求分配id,然后交给KestrelConnection管道处理,包括自身动作,以及Http报文解析, Http管道处理等

7. Kestrel启动过程源码

只关注asp.net core如何初始化+启动的关键点

  1. WebApplication.CreateBuilder(args)用委托注册, builder.Build()完成IOC注册,services.AddSingleton<IServer, KestrelServerImpl>();
  2. app.Run()最终是到了WebHost.StartAsync(),里面组装好了Http管道,还有各种信息,包括HttpContextFactory等,打包成一个HostingApplication,交给KestrelServer
  3. KestrelServer根据监听信息,通过线程池启动死循环监听,(ConnectionDispatcher. StartAcceptingConnections)
  4. 监听到请求数据,启动线程再丢给Kestrel管道模型处理(KestrelConnection),里面其实包含了Http管道

8. Kestrel工作过程源码

Kestrel自身的实现源码,这个要复杂很多,简约点解析---

  1. 从KestrelServerImpl的StartAsync开始,参数检查-做个开关-心跳检测,然后把各种信息打包丢进去
  2. 各种Bind调用,包括获取配置监听信息,循环Address,进行绑定---其实是绕回到KestrelServerImpl的StartAsync方法中的Bind方法:会组装ConnectionMiddleware(连接中间件)+ connectionDelegate连接管道—管道里面包含了Http管道------
  3. 去开启监听: TransportManager---Dispatcher---开启线程来监听请求
  4. 监听到请求: KestrelConnection的ExecuteAsync,里面后就是把信息交给_connectionDelegate连接管道---这里面就包括httpcontext解析, http管道----最后还有finally做全部资源释放

9. 总结Kestrel服务器

Kestrel服务器做了啥?

  1. ASP.NET Core中的Kestrel服务器只是对Socket的简单封装,
  2. 直接用socket通过while(true)的方式来循环接收socket请求,
  3. 然后直接放入clr线程池中来等待线程池调度处理。

ASP.NET Core做了啥?

  1. 准备了Kestrel监听Socket请求
  2. 准备了Http管道处理请求

Kestrel基本不用扩展—只需要配置一下

10. 委托传递

ASP.NET Core应用程序 和 Kestrel之间的纠葛描述:

最初是ASP.NET Core去调用Kestrel,然后是Kestrel里面要调用ASP.NET Core,如果各自一个类库,其实就是个循环引用怎么解决?

用的是委托传递逻辑,解决循环调用:

A要调用B, B里面又要调用A的X逻辑,就可以把A的X逻辑封装成委托,传递给B---然后B,在需要时,直接调用委托

类似的其实特别多,组件封装+Options