摘要
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群也可以找到我,歡迎騷擾,一起學習,一起進步。今天的分享就到這里啦,