• kestrel Server的源码分析


    今天这一篇博客讲的是.net core 自带的kestrel server,当你开发微服务k8s部署在linux环境下,一般默认开启这个高性能服务,如果大家之前看过我的owin katana的博客,会发现.net core 的好多实现在之前.net standard 的版本已经实现过了,当时开发的asp.net 程序与IIS紧紧耦合在一起,后来的微软团队意识到这个问题并尝试将asp.net 解耦server,制定了owin标准并启动了一个开源项目katana,这个项目的结果并没有带动社区效应,但是此时微软已经制订了.net core的开发,并在katana文档暗示了.net vnext 版本,这个就是。net core 与owin katana 的故事。强烈建议大家有时间看看owin katana,里面有一些 dependency inject, hash map, raw http 协议等等实现。非常收益。说到这些我们开始步入正题吧。原代码在github上的asp.net core 源码。

     上图大致地描述了一个asp.net core 的请求过程,但是我们会发现appication 依赖了server,所以我们需要一个Host 的去解耦server 和aplication 的实现,只要server符合host标准可以任意更换,解耦之后的代码与下图所示。

     所以我们的代码都是创建一个web host然后使用usekestrel,如下所示。

                var host = new WebHostBuilder()
                    .UseKestrel(options =>
                    {
                        options.Listen(IPAddress.Loopback, 5001);
                    })
                    .UseStartup<Startup>();

     首先我们知道一个server 实现需要网络编程,所以我们需要socket库来快速编程,它已经帮你实现了tcp与udp协议,不需要自己重复的造轮子。首先我们需要看UseKestrel的方法做了什么。

     1    public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder)
     2         {
     3             return hostBuilder.ConfigureServices(services =>
     4             {
     5                 // Don't override an already-configured transport
     6                 services.TryAddSingleton<IConnectionListenerFactory, SocketTransportFactory>();
     7 
     8                 services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>();
     9                 services.AddSingleton<IServer, KestrelServer>();
    10             });
    11         }

    依赖注入注册了三个对象,一个连接池,一个配置类还有一个是server,会和web host注册了IServer 的实现类,然后我们继续看一下,当你调用run的时候会将控制权从web host 转移给server,如下代码第18行所示。

     1   public virtual async Task StartAsync(CancellationToken cancellationToken = default)
     2         {
     3             HostingEventSource.Log.HostStart(); 6 
     7             var application = BuildApplication();
     8 
    12             // Fire IHostedService.Start
    13             await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);//启动后台服务
    14 
    15             var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>();
    16             var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
    17             var hostingApp = new HostingApplication(application, _logger, diagnosticSource, httpContextFactory);
    18             await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);//socket 启动
    19             _startedServer = true;
    20 
    21             // Fire IApplicationLifetime.Started
    22             _applicationLifetime?.NotifyStarted();
    23 
    24 
    25             _logger.Started();
    26 
    27             // Log the fact that we did load hosting startup assemblies.
    28             if (_logger.IsEnabled(LogLevel.Debug))
    29             {
    30                 foreach (var assembly in _options.GetFinalHostingStartupAssemblies())
    31                 {
    32                     _logger.LogDebug("Loaded hosting startup assembly {assemblyName}", assembly);
    33                 }
    34             }
    35 
    36             if (_hostingStartupErrors != null)
    37             {
    38                 foreach (var exception in _hostingStartupErrors.InnerExceptions)
    39                 {
    40                     _logger.HostingStartupAssemblyError(exception);
    41                 }
    42             }
    43         }

    当我们转进到StartAsync方法时会看到如下代码

     1       public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
     2         {
     3             try
     4             {
    19                // ServiceContext.Heartbeat?.Start();//一个是连接池一个是日期时间
    20 
    21                 async Task OnBind(ListenOptions options)
    22                 {
    23                     // Add the HTTP middleware as the terminal connection middleware
    24                     options.UseHttpServer(ServiceContext, application, options.Protocols);//注册中间件
    25 
    26                     var connectionDelegate = options.Build();
    27 
    28                     // Add the connection limit middleware
    29                     if (Options.Limits.MaxConcurrentConnections.HasValue)
    30                     {
    31                         connectionDelegate = new ConnectionLimitMiddleware(connectionDelegate, Options.Limits.MaxConcurrentConnections.Value, Trace).OnConnectionAsync;
    32                     }
    33 
    34                     var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate);
    35                     var transport = await _transportFactory.BindAsync(options.EndPoint).ConfigureAwait(false);
    36 
    37                     // Update the endpoint
    38                     options.EndPoint = transport.EndPoint;
    39                     var acceptLoopTask = connectionDispatcher.StartAcceptingConnections(transport);
    40 
    41                     _transports.Add((transport, acceptLoopTask));
    42                 }
    43 
    44                 await AddressBinder.BindAsync(_serverAddresses, Options, Trace, OnBind).ConfigureAwait(false);
    45             }
    46             catch (Exception ex)
    47             {51             }
    52         }
    
    

    AddressBinder就是server绑定的ip地址,这个可以在StartUp方法或者环境变量里面配置,里面传了一个回调方法OnBind, 在第24行的UseHttpServer会注册server 内部的中间件去处理这个请求,在第35行socet会绑定地址,用tcp协议,默认使用512个最大pending队列,在接受socket会有多处异步编程和开启线程,建议大家在调试的时候可以修改代码用尽可能少的线程来进行调试。accept 的代码如下图所示

     1      private void StartAcceptingConnectionsCore(IConnectionListener listener)
     2         {
     3             // REVIEW: Multiple accept loops in parallel?
     4             _ = AcceptConnectionsAsync();
     5 
     6             async Task AcceptConnectionsAsync()
     7             {
     8                 try
     9                 {
    10                     while (true)
    11                     {
    12                         var connection = await listener.AcceptAsync();
    13 19 
    20                         // Add the connection to the connection manager before we queue it for execution
    21                         var id = Interlocked.Increment(ref _lastConnectionId);
    22                         var kestrelConnection = new KestrelConnection(id, _serviceContext, _connectionDelegate, connection, Log);
    23 
    24                         _serviceContext.ConnectionManager.AddConnection(id, kestrelConnection);27 
    28                         ThreadPool.UnsafeQueueUserWorkItem(kestrelConnection, preferLocal: false);
    29                        }
    30                 }
    31                 catch (Exception ex)
    32                 {
    33                     // REVIEW: If the accept loop ends should this trigger a server shutdown? It will manifest as a hang
    34                     Log.LogCritical(0, ex, "The connection listener failed to accept any new connections.");
    35                 }
    36                 finally
    37                 {
    38                     _acceptLoopTcs.TrySetResult(null);
    39                 }
    40             }
    41         }

    接收到accept socket的时候,会创建一个kestrelconnection 对象,这个对象实现线程方法,然后它会重新去等待一个请求的到来,而用户代码的执行则交给线程池执行。在第14行就是之前kerstrel server 内部的中间件build生成的方法,他的主要功能就是解析socket的携带http信息。

     1     internal async Task ExecuteAsync()
     2         {
     3             var connectionContext = TransportConnection;
     4 
     5             try
     6             {
    10                 using (BeginConnectionScope(connectionContext))
    11                 {
    12                     try
    13                     {
    14                         await _connectionDelegate(connectionContext);
    15                     }
    16                     catch (Exception ex)
    17                     {
    18                         Logger.LogError(0, ex, "Unhandled exception while processing {ConnectionId}.", connectionContext.ConnectionId);
    19                     }
    20                 }
    21             }
    22             finally
    23             {
    34                 _serviceContext.ConnectionManager.RemoveConnection(_id);
    35             }
    36         }

    由于http协议版本的不一致导致解析方式的不同,如果有兴趣的小伙伴可以具体查看这一块的逻辑。

     1                 switch (SelectProtocol())
     2                 {
     3                     case HttpProtocols.Http1:
     4                         // _http1Connection must be initialized before adding the connection to the connection manager
     5                         requestProcessor = _http1Connection = new Http1Connection<TContext>(_context);
     6                         _protocolSelectionState = ProtocolSelectionState.Selected;
     7                         break;
     8                     case HttpProtocols.Http2:
     9                         // _http2Connection must be initialized before yielding control to the transport thread,
    10                         // to prevent a race condition where _http2Connection.Abort() is called just as
    11                         // _http2Connection is about to be initialized.
    12                         requestProcessor = new Http2Connection(_context);
    13                         _protocolSelectionState = ProtocolSelectionState.Selected;
    14                         break;
    15                     case HttpProtocols.None:
    16                         // An error was already logged in SelectProtocol(), but we should close the connection.
    17                         break;
    18                     default:
    19                         // SelectProtocol() only returns Http1, Http2 or None.
    20                         throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2 or None.");
    21                 }

    然后server解析完请求之后所做的重要的一步就是创建httpContext,然后server在第40行将控制权转给web host,web host 会自动调用application code 也就是用户代码。

      1      private async Task ProcessRequests<TContext>(IHttpApplication<TContext> application)
      2         {
      3             while (_keepAlive)
      4             {
     33                 var context = application.CreateContext(this);
     34 
     35                 try
     36                 {
     37                     KestrelEventSource.Log.RequestStart(this);
     38 
     39                     // Run the application code for this request
     40                     await application.ProcessRequestAsync(context);
     41  55                 }
     56                 catch (BadHttpRequestException ex)
     57                 {
     58                     // Capture BadHttpRequestException for further processing
     59                     // This has to be caught here so StatusCode is set properly before disposing the HttpContext
     60                     // (DisposeContext logs StatusCode).
     61                     SetBadRequestState(ex);
     62                     ReportApplicationError(ex);
     63                 }
     64                 catch (Exception ex)
     65                 {
     66                     ReportApplicationError(ex);
     67                 }
     68 
     69                 KestrelEventSource.Log.RequestStop(this);129             }
    130         }

    到这里server 的工作大部分都结束了,在之前的描述中我们看到web host 怎么将控制权给到server 的, server 创建好httpContext规则后又是如何将控制权给到web host , web host 又如何去调用application code的, web host 实际上build 的时候将用户的中间件定义为链表结构暴露一个入口供web host调用,其他的有时间我会再写博客描述这一块。谢谢大家今天的阅读了。欢迎大家能够留言一起讨论。如果有任何不懂的地方可以私信我。

  • 相关阅读:
    MapInfo 文件格式说明
    一个经典编程面试题的“隐退”
    Polar 投影c#版本移植
    关于 tf.nn.softmax_cross_entropy_with_logits 及 tf.clip_by_value
    系列解读Dropout
    python删除所有的中文字符、非ASCII或非英文字符,检查字符串是否包含非ASCII
    Python使用split使用多个字符分隔字符串
    Convolutional Neural Networks for Visual Recognition
    【TensorFlow】tf.nn.conv2d是怎样实现卷积的?
    [TensorFlow] tf.nn.softmax_cross_entropy_with_logits的用法
  • 原文地址:https://www.cnblogs.com/neilhu/p/14673493.html
Copyright © 2020-2023  润新知