asp.net core启动源码以及监听,到处理请求响应的过程
摘要
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群也可以找到我,欢迎骚扰,一起学习,一起进步。今天的分享就到这里啦,