今天這一篇部落格講的是.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呼叫,其他的有時間我會再寫部落格描述這一塊。謝謝大家今天的閱讀了。歡迎大家能夠留言一起討論。最後謝謝大家的閱讀,如果有任何不懂的地方可以私信我。