ASPNETCORE6.0简单解读SERVER

最近在做系统优化,不得已还是down了.NET6的源码,说实话一点都不想看这东西,微软更新的太快,两年前看的还是.NETCORE3.0的代码,现在.NET7.0都发了预览版了,哎,这节奏也是没谁了。今天先简单聊聊ASPNETCORE服务端SERVER的实现吧。
 
SERVER,在整个ASPNETCORE请求处理服务模型里面,由一个SERVER服务器和一组中间件构成,SERVER作为第一个请求处理模块,它的职责主要是负责服务端的请求监听和响应,它是面向传输的,也就是面向TCP传输层。其实这个也很好理解,我们以WEB服务器为例,当我们通过浏览器HTTP协议访问ASPNETCOREWEB服务器,首先它会通过DNS域名解析服务,解析出目标SERVERIP,然后基于目标IP与SERVER建立TCP连接,最后浏览器发送HTTP请求报文到SERVER并获取响应,所以我们的ASPNETCORESERVER其实就是一个TCP服务端。ASPNETCORE6.0官方支持三种SERVER类型,分别为IIS、HTTP.SYS、KESTREL。
 
 
IIS,这个对于玩.NET的朋友来说应该很熟悉了,WINDOWS平台上面的WEB服务容器,我们的ASPNETCORE服务如果没有跨平台的需求,也是可以选择IIS容器进行部署,如果选择IIS部署有个细节需要注意,我们需要额外下载IISMODULE模块进行安装,其实就是IIS交互ASPNETCOREAPP的一个扩展模块ANCM,ANCM模块是C++写的,源码在ASPNETCORE->SERVERS目录下面,有兴趣的朋友可以看看,下载地址https://dotnet.microsoft.com/zh-cn/download/dotnet/6.0。对于CORE的部署它有两种模式,In-Process(进程内托管)和Out-of-Process(进程外托管)。
  1. In-Process(进程内托管),我们可以通过一张简单的图看看IIS、ASPNETCORE 模块和进程内托管的ASPNETCOREAPP之间的关系,
 
如上图详细描述了模块作为IIS与ASPNETCOREAPP交互的桥梁,其实上图还有一个独立核心的组件没有画出来,那就是HTTP.SYS,WINDOWS平台的网络驱动程序,这个组件是运行在内核态下面的,在IIS6.+被引入的,实际情况是HTTP请求先到内核态的HTTP.SYS(这里只是讨论它和IIS),再由它路由到IIS,接着由IIS管道里面的ANCM模块接管请求唤醒IISHTTPSERVER,最终我们的ASPNETCOREAPP得以执行。
  1. Out-of-Process(进程外托管),同样我们通过一张简图看看它和进程内托管有啥区别?
 
如上图,有两个区别,首先有了独立进程dotnet.exe,这个进程就是我们的APP,其次IIS在这种模型下面感觉有点像反代的角色,IIS需要把请求转发给KESTREL服务器,这里是有一定的性能损失的,IIS需要跟KESTREL建立TCP连接,同时它还是个回环网络访问。简单总结这两种部署方案都有一点缺陷,个人看法。
 
HTTP.SYS,它是Windows网络子系统的一部分,建立在Windows网络子系统针对TCPIP协议栈的驱动(TCPIP.SYS)之上,是一个在内核模式下运行的网络驱动程序,技术成熟,并且可以抵御攻击,提供可靠、安全、可伸缩的全功能 Web 服务器,只能部署在WINDOWS平台之上,如果没有跨平台的需求,它是WINDOWS平台上面的首选方案,下面我们直接通过一张简图看看它和ASPNETCOREAPP之间的关系。
HTTP.SYS监听到请求封装Context上下文,直接传递给ASPNETCORE请求管道,完成请求并响应,一点都不含糊,一气呵成。接下来我们简单分析下源代码的实现。
HTTP.SYS SERVER实现,在CORE6.0框架里面,所有SERVER都实现了具有如下定义的接口ISERVER,
 
public interface IServer : IDisposable
    {
        IFeatureCollection Features { get; }
        Task StartAsync<TContext>(IHttpApplication<TContext> application,  CancellationToken cancellationToken) where TContext : notnull;
        Task StopAsync(CancellationToken cancellationToken);
    }
 
如上ISERVER接口定义了三个成员,Features属性保存了监听地址前缀等信息(HTTP.SYS可以利用地址前缀来转发请求,实现端口共享),其它两个方法表示服务的启动和关闭。我们的HTTP.SYS服务的实现类型是一个名叫MessagePump的对象,我们简单看下它的具体实现,
 
internal partial class MessagePump : IServer
    {
        // 其他成员...
        private readonly HttpSysOptions _options;  // 服务的配置信息,待会会说
        private readonly int _maxAccepts;  
        public MessagePump(IOptions<HttpSysOptions> options, ILoggerFactory loggerFactory,  IAuthenticationSchemeProvider authentication)
        {
            _options = options.Value;
            Listener = new HttpSysListener(_options, loggerFactory);  // http.sys监听服务
 
            Features = new FeatureCollection(); 
            _serverAddresses = new ServerAddressesFeature();
            Features.Set<IServerAddressesFeature>(_serverAddresses); 
           
            _maxAccepts = _options.MaxAccepts; // 设置并发请求数
        }
    }
 
上面部分代码做了一些注释,我们的服务MessagePump类型的创建需要提供HttpSysOptions配置项,并且该配置项同时也是类型HttpSysListener对象中的属性,HttpSysListener对象的主要职责就是完成HTTP.SYS的监听,接下来我们看下HttpSysOptions类型的定义,
 
public class HttpSysOptions
    {
        // 其他成员....
        private const Http503VerbosityLevel DefaultRejectionVerbosityLevel =  Http503VerbosityLevel.Basic; 
        private const long DefaultRequestQueueLength = 1000;  // http.sys基于队列的方式请求,表示队列长度,注意如果是Attach队列,此设置无效,默认为Create
        internal const int DefaultMaxAccepts =  5 * Environment.ProcessorCount; // 最大的线程数正在拉取请求队列的任务,并将请求发送到线程池进行处理,默认是处理器数量的5倍
        private const long DefaultMaxRequestBodySize = 30000000;
        private Http503VerbosityLevel _rejectionVebosityLevel =  DefaultRejectionVerbosityLevel;
        private long _requestQueueLength = DefaultRequestQueueLength;
        private long? _maxConnections;// 最大连接数
        private RequestQueue? _requestQueue; // 请求队列
        private UrlGroup? _urlGroup;
        private long? _maxRequestBodySize = DefaultMaxRequestBodySize;
        private string? _requestQueueName;
        public HttpSysOptions()
        {
        }
    }
 
SERVER配置项HttpSysOptions我也做了相关注释,如果涉及到服务参数调优可以做一些简单参考,接下来再简单看下SERVER是如何监听REQUEST数据并传递给管道中间件处理的。
 
internal partial class MessagePump : IServer
    {
        // 其他成员...
        private async Task ProcessRequestsWorker() // 处理http请求
        {
            Debug.Assert(RequestContextFactory != null);
            // Allocate and accept context per loop and reuse it for all accepts
            using var acceptContext = new AsyncAcceptContext(Listener,  RequestContextFactory);
            int workerIndex = Interlocked.Increment(ref _acceptorCounts);
            while (!Stopping && workerIndex <= _maxAccepts)
            {
                // Receive a request
                RequestContext requestContext; 
                try
                {
                    requestContext = await Listener.AcceptAsync(acceptContext); // 从httpsys请求队列里面获取原始request请求
                    if (!Listener.ValidateRequest(requestContext))
                    {
                        // Dispose the request
                        requestContext.ReleasePins();
                        requestContext.Dispose();
                        // If either of these is false then a response has already been  sent to the client, so we can accept the next request
                        continue;
                    }
                }
                catch (Exception exception)
                {
                    Debug.Assert(Stopping);
                    if (Stopping)
                    {
                        Log.AcceptErrorStopping(_logger, exception);
                    }
                    else
                    {
                        Log.AcceptError(_logger, exception);
                    }
                    continue;
                }
                try
                {
                    if (_options.UnsafePreferInlineScheduling)
                    {
                        await requestContext.ExecuteAsync();
                    }
                    else
                    {
                        ThreadPool.UnsafeQueueUserWorkItem(requestContext, preferLocal:  false); // 通过线程池的方式处理请求
                    }
                }
                catch (Exception ex)
                {
                }
            }
            Interlocked.Decrement(ref _acceptorCounts);
        }
    }
 
如上代码就是MessagePump服务通过HttpSysListener对象从httpsys服务队列里面获取request请求数据的部分代码,后续的处理将由HostingApplication对象接管,封装httpcontext上下文对象,自此请求进入管道中间件处理,后面的处理已经不属于SERVER的职责范围了。以上就是httpsys服务的执行逻辑,最后个人建议,如果我们的aspnetcore服务没有跨平台的需求,个人建议基于httpsys服务的方式承载,获得更好的性能,并且通过winservice的方式部署。
 
Kestrel,以上介绍的两种SERVER,只能部署在WINDOWS平台,也就是说它们跟WIN平台绑定了,如果我们需要实现跨平台部署的需求,请选择KESTREL服务。 我们先通过.NET6源码简单看下KESTREL服务的目录结构,
 
 
如上图就是KESTREL服务实现的全貌,这里我们主要关注一下它网络库传输相关的内容,我们知道HTTP协议是建立在TCP协议之上的,写过通信的朋友应该了解,要实现高性能、高吞吐的SERVER服务器,背后必须要有强大的网络库支持,并且实现内核提供的高性能网络通信模型。KESTREL支持3种网络库,Libuv、QUIC、SOCKETS。Libuv是一个高性能的,事件驱动的异步I/O库,它本身是由C语言编写的,具有很高的可移植性。libuv封装了不同平台底层对于异步IO模型的实现,所以它还本身具备着Windows, Linux都可使用的跨平台能力,这玩意是开源的,要理解它的源码需要很强的内功,呵呵。可惜了,ASPNETCORE好像从2.1版本开始,默认采用了Socket套接字传输,并且微软计划在ASPNETCORE7.0版本里面会删除这个项目以及相关库,地址:https://docs.microsoft.com/zh-cn/dotnet/core/compatibility/aspnet-core/7.0/libuv-transport-dll-removed。其实在GITHUB以及国外一些论坛上面对于这个网络库的替换是有争议的,同时针对这两个库也做了大量的性能测试,最终大家还是默认了SOCKET传输这个库。QUIC,这是一个牛逼协议,是谷歌制定的一种基于UDP的低时延的互联网传输层协议。在2016年11月国际互联网工程任务组(IETF)召开了第一次QUIC工作组会议,受到了业界的广泛关注。这也意味着QUIC开始了它的标准化过程,成为新一代传输层协议。很期待这个协议能在CORE的SERVER里面实现并推广,目前还是预览版的功能。Sockets不用不用介绍了吧,写过原生套接字通信的朋友肯定对它比较熟悉,KESTREL服务大概就介绍到这吧,服务模型的代码实现跟上面介绍的HTTP.SYS服务差不多,这里面SERVER的主要差别就是异步线程模型,需要了解底层一点的朋友还是需要研究Core运行时Runtime底层Socket的实现,里面包含了Win模型IOCP、异步IO、Linux模型Epoll等等实现,可以说微软在实现Epoll网络模型这方面投入了大量的精力,同时在IOCP模型方面也做了相关优化,追求性能极致。最后贴上Runtime地址:https://github.com/dotnet/runtime。就到这吧。
 
 
 
posted @ 2022-06-12 23:36  小菜 。  阅读(895)  评论(0编辑  收藏  举报