.NET Core开发日志——从ASP.NET Core Module到KestrelServer

ASP.NET Core程序现在变得如同控制台(Console)程序一般,同样通过Main方法启动整个应用。而Main方法要做的事情很简单,创建一个WebHostBuilder类,调用其Build方法生成一个WebHost类,最后启动之。

实现代码一目了然:

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

要想探寻其内部究竟做了哪些操作,则需要调查下WebHost类中CreateDefaultBuilder静态方法:

public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
    var builder = new WebHostBuilder();

    if (string.IsNullOrEmpty(builder.GetSetting(WebHostDefaults.ContentRootKey)))
    {
        builder.UseContentRoot(Directory.GetCurrentDirectory());
    }
    if (args != null)
    {
        builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build());
    }

    builder.UseKestrel((builderContext, options) =>
        {
            options.Configure(builderContext.Configuration.GetSection("Kestrel"));
        })
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            var env = hostingContext.HostingEnvironment;

            config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                  .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

            if (env.IsDevelopment())
            {
                var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                if (appAssembly != null)
                {
                    config.AddUserSecrets(appAssembly, optional: true);
                }
            }

            config.AddEnvironmentVariables();

            if (args != null)
            {
                config.AddCommandLine(args);
            }
        })
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
            logging.AddConsole();
            logging.AddDebug();
        })
        .ConfigureServices((hostingContext, services) =>
        {
            // Fallback
            services.PostConfigure<HostFilteringOptions>(options =>
            {
                if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
                {
                    // "AllowedHosts": "localhost;127.0.0.1;[::1]"
                    var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                    // Fall back to "*" to disable.
                    options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
                }
            });
            // Change notification
            services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(
                new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));

            services.AddTransient<IStartupFilter, HostFilteringStartupFilter>();
        })
        .UseIISIntegration()
        .UseDefaultServiceProvider((context, options) =>
        {
            options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
        });

    return builder;
}

代码稍微有点多,但这里只关心WebHostBuilder类的创建,以及该builder使用了UseKestrel方法。

UseKestrel方法内部通过IoC的方式注入了KestrelServer类:

public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder)
{
    return hostBuilder.ConfigureServices(services =>
    {
        // Don't override an already-configured transport
        services.TryAddSingleton<ITransportFactory, SocketTransportFactory>();

        services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>();
        services.AddSingleton<IServer, KestrelServer>();
    });
}

由此可以知道当一个ASP.NET Core应用程序运行起来时,其内部会有KestrelServer。

那么为什么会需要这个KestrelServer?因为它可以做为一个反向代理服务器,帮助ASP.NET Core实现跨平台的需要。

以传统Windows系统上的IIS为例,如下图所示,ASP.NET Core应用程序中的代码已经不再直接依赖于IIS容器,而是通过KestrelServer这个代理将HTTP请求转换为HttpContext对象,再对此对象进行处理。

图中的ASP.NET Core Module也是由ASP.NET Core的诞生而引入的新的IIS模块。它的主要功能是将Web请求重定向至ASP.NET Core应用程序。并且由于ASP.NET Core应用程序独立运行于IIS工作进程之外的进程,它还负责对进程的管理。

ASP.NET Core Module的源码由C++编写,入口是main文件中的RegisterModule函数。

其函数内部实例化了CProxyModuleFactory工厂类。

pFactory = new CProxyModuleFactory;

而由这个工厂类创建的CProxyModule实例中有一个关键的CProxyModule::OnExecuteRequestHandler方法。它会创建FORWARDING_HANDLER实例,并调用其OnExecuteRequestHandler方法。

__override
REQUEST_NOTIFICATION_STATUS
CProxyModule::OnExecuteRequestHandler(
    IHttpContext *          pHttpContext,
    IHttpEventProvider *
)
{
    m_pHandler = new FORWARDING_HANDLER(pHttpContext);
    if (m_pHandler == NULL)
    {
        pHttpContext->GetResponse()->SetStatus(500, "Internal Server Error", 0, E_OUTOFMEMORY);
        return RQ_NOTIFICATION_FINISH_REQUEST;
    }

    return m_pHandler->OnExecuteRequestHandler();
}

在此方法里就有那些核心的处理HTTP请求的操作。

// 实例化应用程序管理器
pApplicationManager = APPLICATION_MANAGER::GetInstance();

// 取得应用程序实例
hr = pApplicationManager->GetApplication(m_pW3Context, &m_pApplication);

// 取得该应用程序的进程
hr = m_pApplication->GetProcess(m_pW3Context, pAspNetCoreConfig, &pServerProcess);

// 创建HTTP请求
hr = CreateWinHttpRequest(pRequest,
        pProtocol,
        hConnect,
        &struEscapedUrl,
        pAspNetCoreConfig,
        pServerProcess);

//  发送HTTP请求
if (!WinHttpSendRequest(m_hRequest,
    m_pszHeaders,
    m_cchHeaders,
    NULL,
    0,
    cbContentLength,
    reinterpret_cast<DWORD_PTR>(static_cast<PVOID>(this))))
{
    hr = HRESULT_FROM_WIN32(GetLastError());
    DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO,
        "FORWARDING_HANDLER::OnExecuteRequestHandler, Send request failed");
    goto Failure;
}

在ASP.NET Core应用程序这端,CreateWebHostBuilder(args).Build().Run();代码执行之后,会调用其对应的异步方法:

private static async Task RunAsync(this IWebHost host, CancellationToken token, string shutdownMessage)
{
    using (host)
    {
        await host.StartAsync(token);

        var hostingEnvironment = host.Services.GetService<IHostingEnvironment>();
        var options = host.Services.GetRequiredService<WebHostOptions>();

        if (!options.SuppressStatusMessages)
        {
            Console.WriteLine($"Hosting environment: {hostingEnvironment.EnvironmentName}");
            Console.WriteLine($"Content root path: {hostingEnvironment.ContentRootPath}");


            var serverAddresses = host.ServerFeatures.Get<IServerAddressesFeature>()?.Addresses;
            if (serverAddresses != null)
            {
                foreach (var address in serverAddresses)
                {
                    Console.WriteLine($"Now listening on: {address}");
                }
            }

            if (!string.IsNullOrEmpty(shutdownMessage))
            {
                Console.WriteLine(shutdownMessage);
            }
        }

        await host.WaitForTokenShutdownAsync(token);
    }
}

该方法中又调用了WebHost的StartAsync方法:

public virtual async Task StartAsync(CancellationToken cancellationToken = default)
{
    HostingEventSource.Log.HostStart();
    _logger = _applicationServices.GetRequiredService<ILogger<WebHost>>();
    _logger.Starting();

    var application = BuildApplication();

    _applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime;
    _hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>();
    var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>();
    var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
    var hostingApp = new HostingApplication(application, _logger, diagnosticSource, httpContextFactory);
    await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);

    // Fire IApplicationLifetime.Started
    _applicationLifetime?.NotifyStarted();

    // Fire IHostedService.Start
    await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);

    _logger.Started();

    // Log the fact that we did load hosting startup assemblies.
    if (_logger.IsEnabled(LogLevel.Debug))
    {
        foreach (var assembly in _options.GetFinalHostingStartupAssemblies())
        {
            _logger.LogDebug("Loaded hosting startup assembly {assemblyName}", assembly);
        }
    }

    if (_hostingStartupErrors != null)
    {
        foreach (var exception in _hostingStartupErrors.InnerExceptions)
        {
            _logger.HostingStartupAssemblyError(exception);
        }
    }
}

BuildApplication方法内部从IoC容器取出KestrelServer的实例:

private void EnsureServer()
{
    if (Server == null)
    {
        Server = _applicationServices.GetRequiredService<IServer>();

        var serverAddressesFeature = Server.Features?.Get<IServerAddressesFeature>();
        var addresses = serverAddressesFeature?.Addresses;
        if (addresses != null && !addresses.IsReadOnly && addresses.Count == 0)
        {
            var urls = _config[WebHostDefaults.ServerUrlsKey] ?? _config[DeprecatedServerUrlsKey];
            if (!string.IsNullOrEmpty(urls))
            {
                serverAddressesFeature.PreferHostingUrls = WebHostUtilities.ParseBool(_config, WebHostDefaults.PreferHostingUrlsKey);

                foreach (var value in urls.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
                {
                    addresses.Add(value);
                }
            }
        }
    }
}

最后调用KestrelServer的StartAsync方法:

public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
{
    try
    {
        if (!BitConverter.IsLittleEndian)
        {
            throw new PlatformNotSupportedException(CoreStrings.BigEndianNotSupported);
        }

        ValidateOptions();

        if (_hasStarted)
        {
            // The server has already started and/or has not been cleaned up yet
            throw new InvalidOperationException(CoreStrings.ServerAlreadyStarted);
        }
        _hasStarted = true;
        _heartbeat.Start();

        async Task OnBind(ListenOptions endpoint)
        {
            // Add the HTTP middleware as the terminal connection middleware
            endpoint.UseHttpServer(endpoint.ConnectionAdapters, ServiceContext, application, endpoint.Protocols);

            var connectionDelegate = endpoint.Build();

            // Add the connection limit middleware
            if (Options.Limits.MaxConcurrentConnections.HasValue)
            {
                connectionDelegate = new ConnectionLimitMiddleware(connectionDelegate, Options.Limits.MaxConcurrentConnections.Value, Trace).OnConnectionAsync;
            }

            var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate);
            var transport = _transportFactory.Create(endpoint, connectionDispatcher);
            _transports.Add(transport);

            await transport.BindAsync().ConfigureAwait(false);
        }

        await AddressBinder.BindAsync(_serverAddresses, Options, Trace, OnBind).ConfigureAwait(false);
    }
    catch (Exception ex)
    {
        Trace.LogCritical(0, ex, "Unable to start Kestrel.");
        Dispose();
        throw;
    }
}

到了这一步,KestrelServer终于可以监听来自ASP.NET Core Module发出的HTTP请求,而ASP.NET Core应用程序也可以开始其自身的任务处理了。