摘要
asp.net core釋出至今已經將近6年了,很多人對於這一塊還是有些陌生,或者說沒接觸過;接觸過的,對於asp.net core整個啟動過程,監聽過程,以及請求過程,響應過程也是一知半解,可能有的同學在面試中有被問過整個的啟動過程;對此,有個想法就是針對於之前沒有接觸過core的,後續會持續輸出asp.net core方面的基礎,包括IOC,中介軟體,主機,日誌,以及伺服器,配置,options等方面的入門講解;本篇部落格先粗略的講解一下,asp.net core整個程式啟動過程,以及啟動之後都幹了什麼,我們的請求是如何到達我們的介面的。
WebApplicationBuilder
在asp.net core6,我們預設建立一個專案之後,已經是沒有了Main啟動方法了,映入眼簾的是去寫我們的啟動程式碼,配置服務中介軟體的程式碼,在第一行,我們看到直接去構建了一個名為builder的一個物件,這個物件其實就是WebApplicationBuilder的一個物件,在CreateBuilder方法裡,直接去new了一個這個類的例項,然後返回給我們。
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); app.UseSwagger(); app.UseSwaggerUI(); app.UseAuthorization(); app.MapControllers(); app.Run();
在構建了這個類的例項後,這個類的建構函式為我們去構造我們程式執行所必須的一些環境配置,主機配置,以及以來注入的配置,並且有細心的同學可以發現,在3.1以及5的版本中,中介軟體管理哪裡是有自動新增UseRouteing,UseDeveloperExceptionPage和UseEndpoint的方法的,在6中是沒有了,其實這個是在構建這個類的例項的時候,預設為我們把這個新增進去了,並且在配置WebHostDefault的時候,已經注入了Routing相關的服務,把我們的需要用的伺服器型別,IIS或者Kestrel配置並且注入到容器中去,在原始碼中,有個ConfigureApplication的方法在執行了配置WebHostBuilder的預設中介軟體,這其中就包括了路由和終結點以及異常頁方面的中介軟體配置,並且將WebApplication裡面新增的中介軟體新增到我們構建的applicationbuilder中,這樣我們的請求可以走到applicationbuilder中介軟體去並且在走到我們的WebApplication所新增的中介軟體,並且在構建WebHostBuilder的實現GenericWebHostBuilder的時候,向我們的容器注入了我們啟動需要的HttpContext的工廠實現IHttpContextFactory,以及中介軟體IMiddlewareFactory,以及我們的ApplicationBuilderFactory的服務,這個服務是用來建立ApplicationBuilder,這個類用來存放我們的中介軟體並且構建我們整個程式執行的中介軟體去進行傳遞,如果有用到UseStartup的話 也會去建立指定的類,然後去呼叫startup裡面的方法,方法參考之前5版本里面的startup;在上述步驟結束後,建立我們WebApplicationBuilder裡面的Host物件和WebHost的物件的例項;這其中涉及到了幾個重要的類和方法,ConfigurationManager是我們程式的配置檔案相關的類,BootstrapHostBuilder用來配置預設的ConfigureWebHostDefaults,並且在初始化完成之後會將HostBuilderContext傳遞到我們ConfigureHostBuilder這個類去,這個類是我們builder.host的型別,ConfigureWebHostBuilder用來配置web主機啟動的時候的一些配置
var configuration = new ConfigurationManager(); configuration.AddEnvironmentVariables(prefix: "ASPNETCORE_"); _hostApplicationBuilder = new HostApplicationBuilder(new HostApplicationBuilderSettings { Args = options.Args, ApplicationName = options.ApplicationName, EnvironmentName = options.EnvironmentName, ContentRootPath = options.ContentRootPath, Configuration = configuration, }); // Set WebRootPath if necessary if (options.WebRootPath is not null) { Configuration.AddInMemoryCollection(new[] { new KeyValuePair<string, string?>(WebHostDefaults.WebRootKey, options.WebRootPath), }); } // Run methods to configure web host defaults early to populate services var bootstrapHostBuilder = new BootstrapHostBuilder(_hostApplicationBuilder); // This is for testing purposes configureDefaults?.Invoke(bootstrapHostBuilder); bootstrapHostBuilder.ConfigureWebHostDefaults(webHostBuilder => { // Runs inline. webHostBuilder.Configure(ConfigureApplication); webHostBuilder.UseSetting(WebHostDefaults.ApplicationKey, _hostApplicationBuilder.Environment.ApplicationName ?? ""); webHostBuilder.UseSetting(WebHostDefaults.PreventHostingStartupKey, Configuration[WebHostDefaults.PreventHostingStartupKey]); webHostBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, Configuration[WebHostDefaults.HostingStartupAssembliesKey]); webHostBuilder.UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey, Configuration[WebHostDefaults.HostingStartupExcludeAssembliesKey]); }, options => { // We've already applied "ASPNETCORE_" environment variables to hosting config options.SuppressEnvironmentConfiguration = true; }); // This applies the config from ConfigureWebHostDefaults // Grab the GenericWebHostService ServiceDescriptor so we can append it after any user-added IHostedServices during Build(); _genericWebHostServiceDescriptor = bootstrapHostBuilder.RunDefaultCallbacks(); // Grab the WebHostBuilderContext from the property bag to use in the ConfigureWebHostBuilder. Then // grab the IWebHostEnvironment from the webHostContext. This also matches the instance in the IServiceCollection. var webHostContext = (WebHostBuilderContext)bootstrapHostBuilder.Properties[typeof(WebHostBuilderContext)]; Environment = webHostContext.HostingEnvironment; Host = new ConfigureHostBuilder(bootstrapHostBuilder.Context, Configuration, Services); WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
Debug.Assert(_builtApplication is not null); // UseRouting called before WebApplication such as in a StartupFilter // lets remove the property and reset it at the end so we don't mess with the routes in the filter if (app.Properties.TryGetValue(EndpointRouteBuilderKey, out var priorRouteBuilder)) { app.Properties.Remove(EndpointRouteBuilderKey); } if (context.HostingEnvironment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // Wrap the entire destination pipeline in UseRouting() and UseEndpoints(), essentially: // destination.UseRouting() // destination.Run(source) // destination.UseEndpoints() // Set the route builder so that UseRouting will use the WebApplication as the IEndpointRouteBuilder for route matching app.Properties.Add(WebApplication.GlobalEndpointRouteBuilderKey, _builtApplication); // Only call UseRouting() if there are endpoints configured and UseRouting() wasn't called on the global route builder already if (_builtApplication.DataSources.Count > 0) { // If this is set, someone called UseRouting() when a global route builder was already set if (!_builtApplication.Properties.TryGetValue(EndpointRouteBuilderKey, out var localRouteBuilder)) { app.UseRouting(); } else { // UseEndpoints will be looking for the RouteBuilder so make sure it's set app.Properties[EndpointRouteBuilderKey] = localRouteBuilder; } } // Wire the source pipeline to run in the destination pipeline app.Use(next => { _builtApplication.Run(next); return _builtApplication.BuildRequestDelegate(); }); if (_builtApplication.DataSources.Count > 0) { // We don't know if user code called UseEndpoints(), so we will call it just in case, UseEndpoints() will ignore duplicate DataSources app.UseEndpoints(_ => { }); } // Copy the properties to the destination app builder foreach (var item in _builtApplication.Properties) { app.Properties[item.Key] = item.Value; } // Remove the route builder to clean up the properties, we're done adding routes to the pipeline app.Properties.Remove(WebApplication.GlobalEndpointRouteBuilderKey); // reset route builder if it existed, this is needed for StartupFilters if (priorRouteBuilder is not null) { app.Properties[EndpointRouteBuilderKey] = priorRouteBuilder; }
WebApplication
上面我們講了WebApplicationBuilder類,在這個類裡面我們開始構建了Hostbuilder的類的例項,然後我們在我們的程式碼中呼叫了這個類的Builder的方法,這個方法是將Hostbuilder裡面build的方法呼叫之後返回的IHost引數傳遞到WebApplication類中去,通過建構函式傳入,同時這個類 IHost, IApplicationBuilder, IEndpointRouteBuilder分別實現了這三個介面,第一個IHost是我們程式執行時所以來的主機通過啟動主機我們去啟動我們的服務,不管是IIS還是Kestrel,第二個的話就是我們的管道中介軟體配置介面,所有的我們使用的中介軟體都最終呼叫這個介面下面的Use方法新增到中介軟體集合中去,第三個介面則是指定了我們所有路由終結點的Endpoint的資料來源以及,依賴注入的服務提供者。在這個類裡面,我們可以獲取到我們的服務提供者以及日誌Logger相關,配置,等相關介面的例項,這些在我們CreateBuilder的時候都以及配置和注入好了,在這裡我們就可以直接配置我們所需要的各種中介軟體。同時剛才也說了,這個類實現了IApplicationBuilder,所以我們也可以直接呼叫Use方法新增我們的中介軟體,並且也有許多擴充的方法供我們去向IApplicationBuilder新增中介軟體。
在所有的配置都就緒好之後,我們便可以去啟動我們的主機,從而去啟動我們的web主機,可以看到,我們最後的程式碼是app.run,這個方法就是在呼叫我們WebApplication建構函式傳入的IHost裡面的StartAsync方法,接下來我們看這個類裡面的實現。
MapControllers
這裡需要著重講一下這個方法,我們都知道,我們所有的請求都會走到useendpoint的中介軟體去,那在這個中介軟體之前我們是需要把我們的所有的路由資訊新增到一個EndpointSource的集合中去的,這裡麵包含了你的方法名稱,後設資料以及RequestDelegate的資訊,包含了你的方法請求的路由等資訊,所以在MapController方法,其實就是在構建我們所有的路由請求的一個RequestDelegate,然後在每次請求的時候,在EndpointMiddleWare中介軟體去執行這個RequestDelegate,從而走到我們的介面中去。簡而言之,這個方法就是將我們的所有路由資訊新增到一個EndpointDataSource的抽象類的實現類中去,預設是ControllerActionEndpointDataSource這個類,在這個類中有一個基類ActionEndpointDataSourceBase,ControllerActionEndpointDataSource初始化的時候會訂閱所有的Endpoint的集合的變化,每變化一次會向EndpointSource集合新增Endpoint,從而在請求的時候可以找到這個終結點去呼叫,
public static ControllerActionEndpointConventionBuilder MapControllers(this IEndpointRouteBuilder endpoints) { if (endpoints == null) { throw new ArgumentNullException(nameof(endpoints)); } EnsureControllerServices(endpoints); return GetOrCreateDataSource(endpoints).DefaultBuilder; }
private static ControllerActionEndpointDataSource GetOrCreateDataSource(IEndpointRouteBuilder endpoints) { var dataSource = endpoints.DataSources.OfType<ControllerActionEndpointDataSource>().FirstOrDefault(); if (dataSource == null) { var orderProvider = endpoints.ServiceProvider.GetRequiredService<OrderedEndpointsSequenceProviderCache>(); var factory = endpoints.ServiceProvider.GetRequiredService<ControllerActionEndpointDataSourceFactory>(); dataSource = factory.Create(orderProvider.GetOrCreateOrderedEndpointsSequenceProvider(endpoints)); endpoints.DataSources.Add(dataSource); } return dataSource; }
IHost
在app.run方法之後,最後會呼叫我們建構函式中的Host的StartAsync方法去,可以看一下這裡的呼叫原始碼,在我們run的時候,呼叫了HostingAbstractionsHostExtensions裡面的run方法,然後這個HostingAbstractionsHostExtensions的run方法又呼叫了WebApplication裡面的Runasync方法,WebApplication的RunAsync方法又呼叫了HostingAbstractionsHostExtensions的RunAsync方法,這個HostingAbstractionsHostExtensions的RunAsync方法又呼叫了WebApplication的StartAsync方法,然後去呼叫了我們的Host的StartAsync方法,哈哈,是不是很繞,看到這段呼叫程式碼,我甚至覺得太扯了。我們都知道core的執行其實就是HostedService去啟動我們的Web服務的,所以在這個start方法裡面,他從ServiceProvider去獲取了所有的實現了HostedService介面的例項,然後迴圈去呼叫StartAsync方法,這裡引入我們的泛型主機的一個實現,GenericWebHostService這個類,同樣實現了HostdService的介面,然後我們在Host的startasync方法呼叫之後會走到這個類的StartAsync方法中去,這個類的建構函式中已經傳入了我們所需要的IServer的型別,這個就是我們的執行所以來的web伺服器,是iis或者Kestrel,然後在這個GenericWebHostService的StartAsync方法中去呼叫IServer的StartAsync方法啟動我們的服務監聽。並且在監聽之前,會把我們的所有的中介軟體去build一個RequestDelegate,然後傳遞到IHttpApplication這個泛型介面中去,這個介面其實就是我們所有的請求走中介軟體的地方,並且也是根據我們的Request去建立HttpContext的地方,從而去構建Request和Response例項的地方,
KestrelServerImpl
其實在這個類之上還有一個KestrelServer類,兩個都實現了IServer介面,在上面的Host呼叫IServer的StartAsync方法之後,呼叫了KestrelServer的StartAsync方法,然後在呼叫到了KestrelServerImpl的StartAsync方法,這個類裡面的StartAsync方法,在開始的時候就去開始我們程式的心跳。然後呼叫了一個BindAsync的方法,在此上面我們將我們需要監聽的地址,以及BindAsync之後的回撥傳入到AddressBindContext這個類中;
StartAsync
public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) where TContext : notnull { try { ValidateOptions(); if (_hasStarted) { // The server has already started and/or has not been cleaned up yet throw new InvalidOperationException(CoreStrings.ServerAlreadyStarted); } _hasStarted = true; ServiceContext.Heartbeat?.Start(); async Task OnBind(ListenOptions options, CancellationToken onBindCancellationToken) { var hasHttp1 = options.Protocols.HasFlag(HttpProtocols.Http1); var hasHttp2 = options.Protocols.HasFlag(HttpProtocols.Http2); var hasHttp3 = options.Protocols.HasFlag(HttpProtocols.Http3); var hasTls = options.IsTls; // Filter out invalid combinations. if (!hasTls) { // Http/1 without TLS, no-op HTTP/2 and 3. if (hasHttp1) { hasHttp2 = false; hasHttp3 = false; } // Http/3 requires TLS. Note we only let it fall back to HTTP/1, not HTTP/2 else if (hasHttp3) { throw new InvalidOperationException("HTTP/3 requires HTTPS."); } } // Quic isn't registered if it's not supported, throw if we can't fall back to 1 or 2 if (hasHttp3 && _multiplexedTransportFactory is null && !(hasHttp1 || hasHttp2)) { throw new InvalidOperationException("This platform doesn't support QUIC or HTTP/3."); } // Disable adding alt-svc header if endpoint has configured not to or there is no // multiplexed transport factory, which happens if QUIC isn't supported. var addAltSvcHeader = !options.DisableAltSvcHeader && _multiplexedTransportFactory != null; // Add the HTTP middleware as the terminal connection middleware if (hasHttp1 || hasHttp2 || options.Protocols == HttpProtocols.None) // TODO a test fails because it doesn't throw an exception in the right place // when there is no HttpProtocols in KestrelServer, can we remove/change the test? { if (_transportFactory is null) { throw new InvalidOperationException($"Cannot start HTTP/1.x or HTTP/2 server if no {nameof(IConnectionListenerFactory)} is registered."); } options.UseHttpServer(ServiceContext, application, options.Protocols, addAltSvcHeader); var connectionDelegate = options.Build(); // Add the connection limit middleware connectionDelegate = EnforceConnectionLimit(connectionDelegate, Options.Limits.MaxConcurrentConnections, Trace); options.EndPoint = await _transportManager.BindAsync(options.EndPoint, connectionDelegate, options.EndpointConfig, onBindCancellationToken).ConfigureAwait(false); } if (hasHttp3 && _multiplexedTransportFactory is not null) { options.UseHttp3Server(ServiceContext, application, options.Protocols, addAltSvcHeader); var multiplexedConnectionDelegate = ((IMultiplexedConnectionBuilder)options).Build(); // Add the connection limit middleware multiplexedConnectionDelegate = EnforceConnectionLimit(multiplexedConnectionDelegate, Options.Limits.MaxConcurrentConnections, Trace); options.EndPoint = await _transportManager.BindAsync(options.EndPoint, multiplexedConnectionDelegate, options, onBindCancellationToken).ConfigureAwait(false); } } AddressBindContext = new AddressBindContext(_serverAddresses, Options, Trace, OnBind); await BindAsync(cancellationToken).ConfigureAwait(false); } catch { // Don't log the error https://github.com/dotnet/aspnetcore/issues/29801 Dispose(); throw; } // Register the options with the event source so it can be logged (if necessary) KestrelEventSource.Log.AddServerOptions(Options); }
AddressBindContext
BindAsync
在BindAsync方法我們看到我們呼叫了AddressBinder.BindAsync的方法,
private async Task BindAsync(CancellationToken cancellationToken) { await _bindSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { if (_stopping == 1) { throw new InvalidOperationException("Kestrel has already been stopped."); } IChangeToken? reloadToken = null; _serverAddresses.InternalCollection.PreventPublicMutation(); if (Options.ConfigurationLoader?.ReloadOnChange == true && (!_serverAddresses.PreferHostingUrls || _serverAddresses.InternalCollection.Count == 0)) { reloadToken = Options.ConfigurationLoader.Configuration.GetReloadToken(); } Options.ConfigurationLoader?.Load(); await AddressBinder.BindAsync(Options.ListenOptions, AddressBindContext!, cancellationToken).ConfigureAwait(false); _configChangedRegistration = reloadToken?.RegisterChangeCallback(TriggerRebind, this); } finally { _bindSemaphore.Release(); } }
AddressBinder.Bindasync
在這個方法我們呼叫了IStrategy的BindAsync方法,這個介面有多個實現,但是不管有多少個最後都會呼叫了我們AddressBindContext方法中的CreateBinding委託,可以結合上面的方法看我們的CreateBinding委託實際上就是我們StartAsync中的OnBind方法。
在OnBind方法中,我們判斷我們的Http版本是1,2還是3,不管是哪個版本,這裡的UseHttpServer和UseHttp3Server都是構建了一個在有監聽請求之後的一個ConnectionDelegate,用來監聽到請求之後,去進行處理我們的Request。這裡我們需要著重看一下_transportManager.BindAsync方法,如果我們沒有指定使用其他方式去進行監聽,例如QUIC,預設都是使用Socket進行監聽的,所以IConnectionListenerFactory介面其中的一個實現就是SocketTransportFactory,預設的就走到了SocketTransportFactory.BindAsync方法中去,在這個方法,我們啟動了一個Socket的監聽,然後呼叫了Bind方法去啟動這個監聽,這樣我們便啟動了我們伺服器,然後接下來就是一直等待連線請求,在TransportManager.StartAcceptLoop方法中,我們最主要用的用來處理連線的一個類叫ConnectionDispatcher的類,這個類裡面我們呼叫了StartAcceptingConnections的方法。
var strategy = CreateStrategy( listenOptions.ToArray(), context.Addresses.ToArray(), context.ServerAddressesFeature.PreferHostingUrls); // reset options. The actual used options and addresses will be populated // by the address binding feature context.ServerOptions.OptionsInUse.Clear(); context.Addresses.Clear(); await strategy.BindAsync(context, cancellationToken).ConfigureAwait(false);
OnConnectionAsync
public static IConnectionBuilder UseHttpServer<TContext>(this IConnectionBuilder builder, ServiceContext serviceContext, IHttpApplication<TContext> application, HttpProtocols protocols, bool addAltSvcHeader) where TContext : notnull { var middleware = new HttpConnectionMiddleware<TContext>(serviceContext, application, protocols, addAltSvcHeader); return builder.Use(next => { return middleware.OnConnectionAsync; }); } public static IMultiplexedConnectionBuilder UseHttp3Server<TContext>(this IMultiplexedConnectionBuilder builder, ServiceContext serviceContext, IHttpApplication<TContext> application, HttpProtocols protocols, bool addAltSvcHeader) where TContext : notnull { var middleware = new HttpMultiplexedConnectionMiddleware<TContext>(serviceContext, application, protocols, addAltSvcHeader); return builder.Use(next => { return middleware.OnConnectionAsync; }); }
public Task OnConnectionAsync(ConnectionContext connectionContext) { var memoryPoolFeature = connectionContext.Features.Get<IMemoryPoolFeature>(); var protocols = connectionContext.Features.Get<HttpProtocolsFeature>()?.HttpProtocols ?? _endpointDefaultProtocols; var localEndPoint = connectionContext.LocalEndPoint as IPEndPoint; var altSvcHeader = _addAltSvcHeader && localEndPoint != null ? HttpUtilities.GetEndpointAltSvc(localEndPoint, protocols) : null; var httpConnectionContext = new HttpConnectionContext( connectionContext.ConnectionId, protocols, altSvcHeader, connectionContext, _serviceContext, connectionContext.Features, memoryPoolFeature?.MemoryPool ?? System.Buffers.MemoryPool<byte>.Shared, localEndPoint, connectionContext.RemoteEndPoint as IPEndPoint); httpConnectionContext.Transport = connectionContext.Transport; var connection = new HttpConnection(httpConnectionContext); return connection.ProcessRequestsAsync(_application); }
TransportManager
public async Task<EndPoint> BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig? endpointConfig, CancellationToken cancellationToken) { if (_transportFactory is null) { throw new InvalidOperationException($"Cannot bind with {nameof(ConnectionDelegate)} no {nameof(IConnectionListenerFactory)} is registered."); } var transport = await _transportFactory.BindAsync(endPoint, cancellationToken).ConfigureAwait(false); StartAcceptLoop(new GenericConnectionListener(transport), c => connectionDelegate(c), endpointConfig); return transport.EndPoint; } public async Task<EndPoint> BindAsync(EndPoint endPoint, MultiplexedConnectionDelegate multiplexedConnectionDelegate, ListenOptions listenOptions, CancellationToken cancellationToken) { if (_multiplexedTransportFactory is null) { throw new InvalidOperationException($"Cannot bind with {nameof(MultiplexedConnectionDelegate)} no {nameof(IMultiplexedConnectionListenerFactory)} is registered."); } var features = new FeatureCollection(); // This should always be set in production, but it's not set for InMemory tests. // The transport will check if the feature is missing. if (listenOptions.HttpsOptions != null) { features.Set(HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions)); } var transport = await _multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false); StartAcceptLoop(new GenericMultiplexedConnectionListener(transport), c => multiplexedConnectionDelegate(c), listenOptions.EndpointConfig); return transport.EndPoint; } private void StartAcceptLoop<T>(IConnectionListener<T> connectionListener, Func<T, Task> connectionDelegate, EndpointConfig? endpointConfig) where T : BaseConnectionContext { var transportConnectionManager = new TransportConnectionManager(_serviceContext.ConnectionManager); var connectionDispatcher = new ConnectionDispatcher<T>(_serviceContext, connectionDelegate, transportConnectionManager); var acceptLoopTask = connectionDispatcher.StartAcceptingConnections(connectionListener); _transports.Add(new ActiveTransport(connectionListener, acceptLoopTask, transportConnectionManager, endpointConfig)); }
SocketTransportFactory.BindAsync
public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) { var transport = new SocketConnectionListener(endpoint, _options, _logger); transport.Bind(); return new ValueTask<IConnectionListener>(transport); }
internal void Bind() { if (_listenSocket != null) { throw new InvalidOperationException(SocketsStrings.TransportAlreadyBound); } Socket listenSocket; try { listenSocket = _options.CreateBoundListenSocket(EndPoint); } catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse) { throw new AddressInUseException(e.Message, e); } Debug.Assert(listenSocket.LocalEndPoint != null); EndPoint = listenSocket.LocalEndPoint; listenSocket.Listen(_options.Backlog); _listenSocket = listenSocket; }
StartAcceptingConnections
在這個方法中我們呼叫了StartAcceptingConnectionsCore方法,這個方法中死迴圈呼叫內部定義的AcceptConnectionsAsync等待連線的方法啊,然後如果有監聽到請求,就會呼叫KestrelConnection這個類,這個類實現了IThreadPoolWorkItem介面,所有就會呼叫ExecuteAsync方法,在這個方法中就會去執行我們上面UseHttpServer裡面的ConnectionDelegate的委託,也就是OnConnectionAsync方法,去處理我們的請求,然後呼叫ProcessRequestsAsync方法。
public Task StartAcceptingConnections(IConnectionListener<T> listener) { ThreadPool.UnsafeQueueUserWorkItem(StartAcceptingConnectionsCore, listener, preferLocal: false); return _acceptLoopTcs.Task; } private void StartAcceptingConnectionsCore(IConnectionListener<T> listener) { // REVIEW: Multiple accept loops in parallel? _ = AcceptConnectionsAsync(); async Task AcceptConnectionsAsync() { try { while (true) { var connection = await listener.AcceptAsync(); if (connection == null) { // We're done listening break; } // Add the connection to the connection manager before we queue it for execution var id = _transportConnectionManager.GetNewConnectionId(); var kestrelConnection = new KestrelConnection<T>( id, _serviceContext, _transportConnectionManager, _connectionDelegate, connection, Log); _transportConnectionManager.AddConnection(id, kestrelConnection); Log.ConnectionAccepted(connection.ConnectionId); KestrelEventSource.Log.ConnectionQueuedStart(connection); ThreadPool.UnsafeQueueUserWorkItem(kestrelConnection, preferLocal: false); } } catch (Exception ex) { // REVIEW: If the accept loop ends should this trigger a server shutdown? It will manifest as a hang Log.LogCritical(0, ex, "The connection listener failed to accept any new connections."); } finally { _acceptLoopTcs.TrySetResult(); } } }
void IThreadPoolWorkItem.Execute() { _ = ExecuteAsync(); } internal async Task ExecuteAsync() { var connectionContext = _transportConnection; try { KestrelEventSource.Log.ConnectionQueuedStop(connectionContext); Logger.ConnectionStart(connectionContext.ConnectionId); KestrelEventSource.Log.ConnectionStart(connectionContext); using (BeginConnectionScope(connectionContext)) { try { await _connectionDelegate(connectionContext); } catch (Exception ex) { Logger.LogError(0, ex, "Unhandled exception while processing {ConnectionId}.", connectionContext.ConnectionId); } } } finally { await FireOnCompletedAsync(); Logger.ConnectionStop(connectionContext.ConnectionId); KestrelEventSource.Log.ConnectionStop(connectionContext); // Dispose the transport connection, this needs to happen before removing it from the // connection manager so that we only signal completion of this connection after the transport // is properly torn down. await connectionContext.DisposeAsync(); _transportConnectionManager.RemoveConnection(_id); } }
ProcessRequestsAsync
在這個方法中,他會根據我們的Http版本,建立不同的IRequestProcessor物件,在這個介面中有ProcessRequestsAsync方法,我們的請求都會進入這個方法,在這個方法,不管是http哪個版本最終都會呼叫到其所擁有的ProcessRequestsAsync方法中去,這裡我們著重考慮這個方法具體是幹了什麼,還記得我們在上面傳入的IHttpApplication的物件,這個其實就是我們在GenericWebHostService呼叫Server的StartAsync方法之前定義的IHttpApplication這個介面的例項,這個介面有一個三個方法,CreateContext,ProcessRequestAsync,DisposeContext顧名思義,Context都是構建這個泛型介面的泛型例項,這裡麵包含了HttpContext,以及用完後的釋放,中間哪個則是去呼叫我們的請求管道處理,我們之前講過,我們ApplicationBuilder呼叫Build方法之後,將多個管道結合成一個RequestDelegate,傳入到這個介面的實現中去,然後我們在這個方法則依次呼叫我們的中介軟體管道,從而會走到各種中介軟體,中介軟體這裡我主要講一下UseEndpoing以及UseRouteing這兩個中介軟體,
public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> httpApplication) where TContext : notnull { try { // Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs. _timeoutControl.Initialize(_systemClock.UtcNowTicks); IRequestProcessor? requestProcessor = null; switch (SelectProtocol()) { case HttpProtocols.Http1: // _http1Connection must be initialized before adding the connection to the connection manager requestProcessor = _http1Connection = new Http1Connection<TContext>((HttpConnectionContext)_context); _protocolSelectionState = ProtocolSelectionState.Selected; break; case HttpProtocols.Http2: // _http2Connection must be initialized before yielding control to the transport thread, // to prevent a race condition where _http2Connection.Abort() is called just as // _http2Connection is about to be initialized. requestProcessor = new Http2Connection((HttpConnectionContext)_context); _protocolSelectionState = ProtocolSelectionState.Selected; break; case HttpProtocols.Http3: requestProcessor = new Http3Connection((HttpMultiplexedConnectionContext)_context); _protocolSelectionState = ProtocolSelectionState.Selected; break; case HttpProtocols.None: // An error was already logged in SelectProtocol(), but we should close the connection. break; default: // SelectProtocol() only returns Http1, Http2, Http3 or None. throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2 or None."); } _requestProcessor = requestProcessor; if (requestProcessor != null) { var connectionHeartbeatFeature = _context.ConnectionFeatures.Get<IConnectionHeartbeatFeature>(); var connectionLifetimeNotificationFeature = _context.ConnectionFeatures.Get<IConnectionLifetimeNotificationFeature>(); // These features should never be null in Kestrel itself, if this middleware is ever refactored to run outside of kestrel, // we'll need to handle these missing. Debug.Assert(connectionHeartbeatFeature != null, nameof(IConnectionHeartbeatFeature) + " is missing!"); Debug.Assert(connectionLifetimeNotificationFeature != null, nameof(IConnectionLifetimeNotificationFeature) + " is missing!"); // Register the various callbacks once we're going to start processing requests // The heart beat for various timeouts connectionHeartbeatFeature?.OnHeartbeat(state => ((HttpConnection)state).Tick(), this); // Register for graceful shutdown of the server using var shutdownRegistration = connectionLifetimeNotificationFeature?.ConnectionClosedRequested.Register(state => ((HttpConnection)state!).StopProcessingNextRequest(), this); // Register for connection close using var closedRegistration = _context.ConnectionContext.ConnectionClosed.Register(state => ((HttpConnection)state!).OnConnectionClosed(), this); await requestProcessor.ProcessRequestsAsync(httpApplication); } } catch (Exception ex) { Log.LogCritical(0, ex, $"Unexpected exception in {nameof(HttpConnection)}.{nameof(ProcessRequestsAsync)}."); } }
internal interface IRequestProcessor { Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> application) where TContext : notnull; void StopProcessingNextRequest(); void HandleRequestHeadersTimeout(); void HandleReadDataRateTimeout(); void OnInputOrOutputCompleted(); void Tick(DateTimeOffset now); void Abort(ConnectionAbortedException ex); }
private async Task ProcessRequests<TContext>(IHttpApplication<TContext> application) where TContext : notnull { while (_keepAlive) { if (_context.InitialExecutionContext is null) { // If this is a first request on a non-Http2Connection, capture a clean ExecutionContext. _context.InitialExecutionContext = ExecutionContext.Capture(); } else { // Clear any AsyncLocals set during the request; back to a clean state ready for next request // And/or reset to Http2Connection's ExecutionContext giving access to the connection logging scope // and any other AsyncLocals set by connection middleware. ExecutionContext.Restore(_context.InitialExecutionContext); } BeginRequestProcessing(); var result = default(ReadResult); bool endConnection; do { if (BeginRead(out var awaitable)) { result = await awaitable; } } while (!TryParseRequest(result, out endConnection)); if (endConnection) { // Connection finished, stop processing requests return; } var messageBody = CreateMessageBody(); if (!messageBody.RequestKeepAlive) { _keepAlive = false; } IsUpgradableRequest = messageBody.RequestUpgrade; InitializeBodyControl(messageBody); var context = application.CreateContext(this); try { KestrelEventSource.Log.RequestStart(this); // Run the application code for this request await application.ProcessRequestAsync(context); // Trigger OnStarting if it hasn't been called yet and the app hasn't // already failed. If an OnStarting callback throws we can go through // our normal error handling in ProduceEnd. // https://github.com/aspnet/KestrelHttpServer/issues/43 if (!HasResponseStarted && _applicationException == null && _onStarting?.Count > 0) { await FireOnStarting(); } if (!_connectionAborted && !VerifyResponseContentLength(out var lengthException)) { ReportApplicationError(lengthException); } } catch (BadHttpRequestException ex) { // Capture BadHttpRequestException for further processing // This has to be caught here so StatusCode is set properly before disposing the HttpContext // (DisposeContext logs StatusCode). SetBadRequestState(ex); ReportApplicationError(ex); } catch (Exception ex) { ReportApplicationError(ex); } KestrelEventSource.Log.RequestStop(this); // At this point all user code that needs use to the request or response streams has completed. // Using these streams in the OnCompleted callback is not allowed. try { Debug.Assert(_bodyControl != null); await _bodyControl.StopAsync(); } catch (Exception ex) { // BodyControl.StopAsync() can throw if the PipeWriter was completed prior to the application writing // enough bytes to satisfy the specified Content-Length. This risks double-logging the exception, // but this scenario generally indicates an app bug, so I don't want to risk not logging it. ReportApplicationError(ex); } // 4XX responses are written by TryProduceInvalidRequestResponse during connection tear down. if (_requestRejectedException == null) { if (!_connectionAborted) { // Call ProduceEnd() before consuming the rest of the request body to prevent // delaying clients waiting for the chunk terminator: // // https://github.com/dotnet/corefx/issues/17330#issuecomment-288248663 // // This also prevents the 100 Continue response from being sent if the app // never tried to read the body. // https://github.com/aspnet/KestrelHttpServer/issues/2102 // // ProduceEnd() must be called before _application.DisposeContext(), to ensure // HttpContext.Response.StatusCode is correctly set when // IHttpContextFactory.Dispose(HttpContext) is called. await ProduceEnd(); } else if (!HasResponseStarted) { // If the request was aborted and no response was sent, there's no // meaningful status code to log. StatusCode = 0; } } if (_onCompleted?.Count > 0) { await FireOnCompleted(); } application.DisposeContext(context, _applicationException); // Even for non-keep-alive requests, try to consume the entire body to avoid RSTs. if (!_connectionAborted && _requestRejectedException == null && !messageBody.IsEmpty) { await messageBody.ConsumeAsync(); } if (HasStartedConsumingRequestBody) { await messageBody.StopAsync(); } } }
public Context CreateContext(IFeatureCollection contextFeatures) { Context? hostContext; if (contextFeatures is IHostContextContainer<Context> container) { hostContext = container.HostContext; if (hostContext is null) { hostContext = new Context(); container.HostContext = hostContext; } } else { // Server doesn't support pooling, so create a new Context hostContext = new Context(); } HttpContext httpContext; if (_defaultHttpContextFactory != null) { var defaultHttpContext = (DefaultHttpContext?)hostContext.HttpContext; if (defaultHttpContext is null) { httpContext = _defaultHttpContextFactory.Create(contextFeatures); hostContext.HttpContext = httpContext; } else { _defaultHttpContextFactory.Initialize(defaultHttpContext, contextFeatures); httpContext = defaultHttpContext; } } else { httpContext = _httpContextFactory!.Create(contextFeatures); hostContext.HttpContext = httpContext; } _diagnostics.BeginRequest(httpContext, hostContext); return hostContext; } // Execute the request public Task ProcessRequestAsync(Context context) { return _application(context.HttpContext!); } // Clean up the request public void DisposeContext(Context context, Exception? exception) { var httpContext = context.HttpContext!; _diagnostics.RequestEnd(httpContext, exception, context); if (_defaultHttpContextFactory != null) { _defaultHttpContextFactory.Dispose((DefaultHttpContext)httpContext); if (_defaultHttpContextFactory.HttpContextAccessor != null) { // Clear the HttpContext if the accessor was used. It's likely that the lifetime extends // past the end of the http request and we want to avoid changing the reference from under // consumers. context.HttpContext = null; } } else { _httpContextFactory!.Dispose(httpContext); } HostingApplicationDiagnostics.ContextDisposed(context); // Reset the context as it may be pooled context.Reset(); }
UseRouting
這個中介軟體最後使用的中介軟體型別是EndpointRoutingMiddleware,在這個中介軟體中,我們會根據我們請求的PathValue,去從我們路由中檢索存在不存在,如果存在,則將找到的Endpoint賦值到HttpContext的Endpoint,從而在我們的EndpointMidWare中介軟體裡面可以找到Endpoint然後去呼叫裡面的RequestDelegate。
UseEndpoint
在這個中介軟體,主要是用來去開始執行我們的請求了,這個請求會先到我們MapController裡面建立的EndpointSource裡面的Endpoint的RequestDelegate中去,這個Endpoint是由上面我們所說的ControllerActionEndpointDataSource去呼叫ActionEndpointFactory類裡面的AddPoint方法將我們傳入的集合去進行新增Endpoint,在ActionEndpointFactory這個類裡面我們呼叫IRequestDelegateFactory介面的CreateRequestDelegate方法去為Endpoint建立對應的RequestDelegate,以及在這個類新增Endpoint的後設資料等資訊。IRequestDelegateFactory預設這個介面是有一個ControllerRequestDelegateFactory實現,所以我們在EndpointMidWare中介軟體呼叫的RequestDelegate都是來自ControllerRequestDelegateFactory的CreateRequestDelegate方法的,在這個類裡建立的RequestDelegate方法均是 ResourceInvoker, IActionInvoker呼叫了這兩個所實現的ControllerActionInvoker類中,最終會走入到InvokeAsync方法,去執行我們所定義的Filter,然後走到我們的Action中去,然後返回結果在從這個中介軟體反方向返回,從而響應了整個Request。
最後再說一句,其實不管是IIS還是Kestrel這兩個整體流程都是一樣的IIS監聽之後,是註冊了一個請求回撥的事件,然後監聽之後再去走Kestrel後面走的哪個ProcessRequestAsync方法中去的,此處就需要各位去自我研究啦~
簡單的啟動到監聽到處理請求的流程可以看成下圖。
總結
寫了這麼多,之前看過3.1和5的原始碼,其原理也基本上大同小異,之前3.1和5都是介面隔離,各個介面乾各個的事情,6則是將多個介面聚合一起,在之前的基礎上在包了一層,從而實現了MiniApi,整體看起來也很像node的Express框架,不過後面的話,考慮去直播給暫時不會Core的同學進行基礎的講解,直播過程中,也會從基礎到慢慢深入原始碼的原理的一個講解,有興趣的朋友,也可以下載原始碼去學習也研究,畢竟用了這個框架,我們得深入瞭解學習這個框架。
如果有不明白的地方,可以聯絡我,在各個net群裡如果有叫四川觀察的那就是我,或者加QQ群也可以找到我,歡迎騷擾,一起學習,一起進步。今天的分享就到這裡啦,