ASP.NET Core之详解服务器-Kestrel、IIS、http.sys

一、前言

  上一章节,我们介绍了服务器相关的基础知识,本章我们继续介绍关于ASP.NET Core中服务器的内容,主要内容包括①实现跨平台的Kestrel服务器,②默认使用Kestrel作为Web服务器,③集成IIS服务器实现进程与进程外的模式,④使用Http.sys的服务器。通过这些内容比较全面的认识在ASP.NET Core中服务器的使用,多样的选择,性能情况。

二、关于Kestrel服务器

  1、Kestrel服务器的定义?

  Kestrel 是一个跨平台的 ASP.NET Core Web 服务器,它默认包含在 ASP.NET Core 项目模板中。作为 ASP.NET Core 应用程序的默认 Web 服务器和托管组件,Kestrel 提供了处理 HTTP 请求和响应的功能,默认处于启用状态。是一个程序内置Kestrel HTTP通信组件。

  2、Kestrel服务器的关键点?

  ①跨平台,Kestrel可以在 Windows、macOS 和 Linux 上运行,这使得它成为构建跨平台 ASP.NET Core 应用程序的理想选择,与.NET Framework的ASP.NET只能部署在Windows的IIS环境不同,提供多个平台的支持,真正的跨平台。

  ②轻量级,指Kestrel服务器与其他IIS、Apache、Ngnix、Tomcat等服务器相比,其主要专注于处理 HTTP 请求和响应,不包含上述服务器的相同的完整 Web 服务器功能集,但是对于ASP.NET Core应用程序来说足够满足需求。而且可以通过代理服务器的方式来弥补其不足的部分,所以Kestrel相对于其它服务器来属于轻量级服务器。

  ③可扩展,指Kestrel是轻量服务器,但是通过使用反向代理服务器的方式扩展其功能,包括额外的安全性、性能等。

  ④可配置,指Kestrel服务器是内置在ASP.NET Core应用,所以在Program.cs 或 Startup.cs 文件中进行,或者通过环境变量和命令行参数进行,提供入口对服务器的参数配置调优等需求。可以配置 Kestrel来监听特定的 IP 地址和端口,设置 TLS/SSL 选项,以及调整其他网络相关的设置。

  ⑤ASP.NET Core的紧密集成,Kestrel 与 ASP.NET Core 框架紧密集成,因此你可以使用 ASP.NET Core 的所有功能,如 MVC、Razor Pages、Web API 等。

  ⑥性能,被设计为高性能的服务器,能够处理大量的并发请求。它使用异步 I/O 操作来减少线程使用并提高吞吐量。(是一个基于libuv的跨平台HTTP服务器,libuv是一个跨平台的异步I/O库)。

  ⑦安全性,Kestrel提供了许多安全性功能,如 TLS/SSL 支持、HTTP/2 支持和请求大小限制。还可以配置 Kestrel 以使用 HTTP 严格传输安全(HSTS)和其他安全相关的 HTTP 头。

  3、Kestrel服务器的应用场景?

  ①开发环境,在IDE中提供开发 ASP.NET Core 应用程序时,Kestrel 可以作为内置的 Web 服务器使用,在本地机器上启动和运行应用程序,无需额外的Web服务器软件。

  ②自托管的应用程序,对于需要自托管的应用程序(即应用程序本身负责提供 Web 服务),Kestrel 是一个理想的选择。它轻量级、易于配置,并且与 ASP.NET Core 紧密集成。

  ③容器化部署,在ASP.NET Core使用Docker或其他容器部署时候,可以作为容器内的Web服务器,基于轻量级与跨平台非常合适在容器环境内运行。

  ④云原生应用,在构建云原生应用,作为应用的内置服务器,结合 Kubernetes、Service Fabric 等容器编排工具,可以轻松扩展和管理多个 Kestrel 实例。

  ⑤小型或者中型的生产环境,在一些小型到中型生产环境中,可以将Kestrel直接作为Web服务器使用,不必使用反向代理或负载均衡器,其高性能完全满足需求,但是对于大型的生产环境,则必须使用更强大的 Web 服务器或负载均衡器来应对更高的并发请求和更复杂的网络需求。

  ⑥内部应用,对于不需要直接暴露给外部世界的内部应用程序(如内部 API、后台任务等),Kestrel 可以提供足够的功能和安全性,这些应用通过内网或者VPN提供服务。

  ⑦物联网或嵌入式应用,在物联网(IoT)和嵌入式系统领域,可以在资源受限的设备上运行,并与 ASP.NET Core 应用程序一起提供 Web 服务。

  通过上述的定义、关键点、应用场景基本对Kestrel比较清晰的认识了,具体这个组件的设计方式,实现方式可以通过查看微软提供开源代码阅读了解,当然在.NET Core的发展过程中,这个组件一样不断完善提供更好性能与功能,不仅仅作为应用服务器或者说Web服务器,我们要关注的是集成在ASP.NET Core中,提供高性能,便捷的能力。

  历史:据说微软的灵感来自Node.js,因为Node.js有一个名为libuv的HTTP服务器。在ASP.NET Core的第一个版本中,微软也使用了libuv,然后在其顶部添加了一个名为Kestrel的层。此时,Node.js 和ASP.NET Core 共享相同的HTTP服务器。随着.NET Core 框架的不断发展和新的.NET Socket的实现,Microsoft基于.NET Socket构建了自己的HTTP服务器,同时删除了libuv,因为libuv不可控也不属于微软,这种重新造轮子的现象在大厂司空见惯。现在,Kestrel已经发展成熟,是一个能运行ASP.NET Core应用的微软系的HTTP服务器。

三、ASP.NET Core中默认Kestrel服务器

  Kestrel服务器紧密的集成到ASP.NET Core中,并且是默认的服务器,那么在ASP.NET Core是如何集成的?是如何设置成默认服务器?是如何对服务器进行配置?我们通过阅读ASP.NET Core的源码来探索相关的内在本质与答案,通过实践Demo来验证答案,最终理解Kestrel运行原理。而且.NET Core从1.0、2.0、3.0到.NET 5.0/.NET 6.0~8.0的版本迭代中基于默认服务器没有变化,但是语法、扩展功能都进行调整,所以在探索的时候会基于版本来区分学习。

  1、如何集成在ASP.NET Core?

  在学习ASP.NET Core的program中要构建一个宿主(WebHost),具体内容参考前一章节关于.NET Core的宿主,在入口函数构建WebHost主要目的是①负责应用程序启动和生存期管理;②主机也是封装应用程序资源的对象;具体表现在①依赖注入(DI),使用.NET Core框架自身提供的依赖注入或者第三方;②Logging,配置,初始化日志对象;③Configuration,加载系统配置项(json/xml文件);④IHostedService 实现,是启动HTTP服务器实现Web功能,这里默认Kestrel服务器,代码如下所示

复制代码
namespace DemoNetCore2
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }
        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
        
    }
}

namespace DemoNetCore2
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.Run(async (context) =>
            {
                context.Response.ContentType = "text/plain; charset=utf-8";
                await context.Response.WriteAsync("Hello World!我是基于WebHostBuilder构建的宿主环境!!");
            });
            app.UseMvc();
        }
    }
}
复制代码

  上述代码在program中通过WebHost对象,使用静态的构建方法CreateDefaultBuilder,创建一个WebHost对象,并且加载启动类Startup。在类Startup中通过ConfigureServices方法依赖注入服务,在Configure中主要启用重要的中间件;基本实现上面提到的构建WebHost的目的。但是可以发现代码并没有对Kestrel服务器进行默认,所以必须阅读相关源码寻找答案。在ASP.NET Core源码的aspnet-core\src\DefaultBuilder文件夹的WebHost类中,截取如下主要代码,一个初始化方法CreateDefaultBuilder,在program中调用,一个ConfigureWebDefaults方法默认配置。

复制代码
/// <returns>The initialized <see cref="IWebHostBuilder"/>.</returns>
    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.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())
            {
                if (!string.IsNullOrEmpty(env.ApplicationName))
                {
                    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, loggingBuilder) =>
        {
            loggingBuilder.Configure(options =>
            {
                options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
                                                    | ActivityTrackingOptions.TraceId
                                                    | ActivityTrackingOptions.ParentId;
            });
            loggingBuilder.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
            loggingBuilder.AddConsole();
            loggingBuilder.AddDebug();
            loggingBuilder.AddEventSourceLogger();
        }).
        UseDefaultServiceProvider((context, options) =>
        {
            options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
        });

        ConfigureWebDefaults(builder);

        return builder;
    }

    internal static void ConfigureWebDefaults(IWebHostBuilder builder)
    {
        builder.ConfigureAppConfiguration((ctx, cb) =>
        {
            if (ctx.HostingEnvironment.IsDevelopment())
            {
                StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
            }
        });

        ConfigureWebDefaultsWorker(
            builder.UseKestrel(ConfigureKestrel),
            services =>
            {
                services.AddRouting();
            });

        builder
            .UseIIS()
            .UseIISIntegration();
    }

    internal static void ConfigureWebDefaultsSlim(IWebHostBuilder builder)
    {
        ConfigureWebDefaultsWorker(builder.UseKestrelCore().ConfigureKestrel(ConfigureKestrel), configureRouting: null);
    }

    private static void ConfigureKestrel(WebHostBuilderContext builderContext, KestrelServerOptions options)
    {
        options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
    }

    private static void ConfigureWebDefaultsWorker(IWebHostBuilder builder, Action<IServiceCollection>? configureRouting)
    {
        builder.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>();
            services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>();
            services.AddTransient<IConfigureOptions<ForwardedHeadersOptions>, ForwardedHeadersOptionsSetup>();

            // Provide a way for the default host builder to configure routing. This probably means calling AddRouting.
            // A lambda is used here because we don't want to reference AddRouting directly because of trimming.
            // This avoids the overhead of calling AddRoutingCore multiple times on app startup.
            if (configureRouting == null)
            {
                services.AddRoutingCore();
            }
            else
            {
                configureRouting(services);
            }
        });
    }
复制代码

  在源码中build对象通过ConfigureAppConfiguration委托构建系统配置项(appsettings.json),ConfigureLogging委托构建系统日志对象;ConfigureWebDefaultsWorker设置了默认服务器builder.UseKestrel(ConfigureKestrel),包括服务器配置项builderContext.Configuration.GetSection("Kestrel"),所以在构造方法已经将服务器进行默认,在使用时默认启用该服务器。

  我们在开发环境或者生产环境,选择使用IIS Express启动调试,或者使用IIS部署,会发现在program中没有指定IIS作为服务器,应用程序一样运行,不会抛出异常,与我们上述确定的Kestrel为默认服务器不一致;原因是在源代码中已经集成了IIS服务器,在netcoreapp2.1版本前并未集成,在netcoreapp3.1版本中已经集成,可以使用IIS作为服务器,主要代码builder.UseIIS().UseIISIntegration();

  2、ASP.NET Core使用Kestrel默认服务如何运行?

  在VS创建一个应用程序,不修改启动项,然后编译发布,可以在cmd使用CLI的dotnet build,dotnet run按照默认参数启动应用程序。

   在发布文件publish中运行程序,在资源管理器可以看到.NET Host进程,没有使用IIS或者其他Web服务器,然后在浏览器访问http://localhost:5000,成功返回请求结果。

 

四、ASP.NET Core中集成使用IIS服务器

  使用默认服务器实现Web服务器的请求,ASP.NET Core已经集成IIS,可以使用IIS作为Web服务器。如上源代码中builder.UseIIS().UseIISIntegration(),其中UseIIS()是使用IIS服务器,UseIISIntegration()使用IIS为代理服务器,所以IIS服务器提供两种部署模式,使用IIS我们都知道是按照管道的方式来处理请求,那么IIS是如何整合IIS?

  1、关于IIS的模块、处理映射程序?

  使用IIS提供的原生Module,ASP.NET CORE Core Module来实现,它利用注册的事件将请求从IIS管道中拦截下来,并转发给ASP.NET CORE管道进行处理。如下是IIS已经安装两个ASP.NET Core Module,一个是基于.NET Core3.1版本,一个.NET 5.0~8.0。而且.NET Framework在IIS中运行一样要是IsapiMoudle模块,所以IIS是基于模块的方式将请求转给具体的应用程序中处理,返回具体的请求结果。

   这个是ASP.NET Core在IIS运行所需的模块。

   这个是ASP.NET在IIS运行所需的模块。

   这个Cgi模块使PHP程序部署在IIS中运行。

  2、关于IIS部署ASP.NET的经典托管模式。

  

  如上图在经典ASP.NET应用程序中,所有一切都托管在IIS工作进程中(w3wp.exe),这也被称为IIS应用程序池。ASP.NET程序被托管在应用程序池中,并且被按照IIS内建的ASP.NET托管特性所实例化。当请求从http.sys传入到ASP.NET应用程序管道时,本地运行时管理器会实例化一个代表应用程序的.NET运行时,同时引入HttpRuntime对象用来处理这个请求。来自http.sys的请求被派送到对应的应用程序池和HttpRuntime实例的托管站点。

  3、关于IIS中部署使用In-Process模式?

  如果ASP.NET Core应用程序是内部进程模式,则无限修改任何代码,将应用程序发布,部署到IIS中。In-Process模式下的ASP.NET CORE应用运行在IIS的工作进程w3wp.exe中(开发环境如果采用IIS Express,工作进程为iisexpress.exe),ASP.NET CORE应用在这种模式下使用的服务器类型是IISHttpServer,上述的ASP.NET CORE Core Module会将原始的请求转发给这个服务器,并将后者生成响应转交给IIS服务器进行回复。

  

 

   如上图,在IIS新建一个网站test,应用程序池使用无托管集成模式,然后在浏览器执行接口请求,应用程序执行首次请求会在资源管理器中新建一个进程名为IIS Worker Process(WAS(Windows Activation Service)的会接收请求,然后按照网站配置项信息,激活一个进程),然后接收请求返回结果,在程序中打印进程名称会返回w3wp的信息,表明是使用了工作进程w3wp.exe。并且通过查看发布生成的webconfig文件可以找到处理请求的模块与模式信息。

复制代码
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet" arguments=".\serverDemo.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="InProcess" />
    </system.webServer>
  </location>
</configuration>
<!--ProjectGuid: BD9F5E0F-0149-4846-87DE-25FD91F61C88-->
复制代码

  4、关于IIS中部署使用out-Process模式?

  如果ASP.NET Core应用程序是外部进程模式,采用KestrelServer的ASP.NET CORE应用运行在独立的dotnet.exe进程中。当IIS接受到针对目标应用的请求时,如果目标应用所在的进程并未启动,ASP.NET CORE Core Module还负责执行dotnet命令激活此进程,相当于充当了WAS(Windows Activation Service)的作用。实际上IIS充当反向代理的作用,可以将IIS换成nginx或者apache来反向代理服务器。

  

  我们将上面的webconfig的hostingModel模式修改成outofProcess模式,然后重新启动网站。如下,后台进程会创建一个.NET Host的程序即图上的dotnet.exe进程和IIS的w3wp.exe,并且浏览器返回的进程是dotnet而不是w3wp。

复制代码
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet" arguments=".\serverDemo.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="OutOfProcess" />
    </system.webServer>
  </location>
</configuration>
<!--ProjectGuid: BD9F5E0F-0149-4846-87DE-25FD91F61C88-->
复制代码

   

  上述就是两种模式在IIS中部署的效果,不论是采用何种部署模式,相关的配置都定义在部署目录下的web.config配置文件,它提供的针对ASP.NET CORE Core Module的映射使我们能够将ASP.NET CORE应用部署在IIS中。在web.config中,与ASP.NET CORE应用部署相关的配置定义在<aspNetCore>配置节中。

五、ASP.NET Core中使用Http.sys服务器

  http.sys服务器是应用程序选择Windows环境部署,不支持跨平台,选择这个服务器是Windows环境下任何针对HTTP的网络监听器/服务器在性能上都无法与HTTP.SYS比肩,在Windows环境中系统针对HTTP的监听、接收、转发和响应大都依赖它

  

  如上图HTTP.SYS是运行在内核态,而IIS或者其他Http的监听则是运行在用户态,所以在性能方面具备先天优势。由于HTTP.SYS是一个底层共享的网络驱动,它有效地解决了端口共享的问题。用户态进程会使用地址前缀(含端口号)“接入”HTTP.SYS,后者利用提供的地址前缀来转发请求,多个用户态进程只要保证提供的地址前缀不同就可以了,所以它们可以使用相同的端口号。端口共享使每个用户进程都可以使用标准的80/443端口。

  1、在ASP.NET Core中如何使用HTTP.SYS?

  我们先创建一个应用程序,然后指定HTTP.SYS为服务器,代替默认服务器,执行请求的监听、响应、返回结果。

复制代码
namespace DemoNetCore8
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // 获取宿主创建对象
            var builder = WebApplication.CreateBuilder(args);

            builder.WebHost.UseHttpSys();
            // 在宿主加入后台任务服务(应用程序)
            builder.Services.AddHostedService<Worker>();

            // 在宿主加入自定义的应用程序
            builder.Services.AddHostedService<UserHostService>();

            // Add services to the container.
            builder.Services.AddAuthorization();

            var app = builder.Build();
            // Configure the HTTP request pipeline.
            app.UseAuthorization();

        string name = Process.GetCurrentProcess().ProcessName;
        string SessionId = Process.GetCurrentProcess().SessionId.ToString();
        string MachineName = Process.GetCurrentProcess().MachineName;
        string osDescription = RuntimeInformation.OSDescription;

        app.Run(context => context.Response.WriteAsync(string.Format("ProcessName:{0}", Process.GetCurrentProcess().ProcessName)));
        app.Run(context => context.Response.WriteAsync(string.Format("SessionId:{0}", Process.GetCurrentProcess().SessionId.ToString())));
        app.Run(context => context.Response.WriteAsync(string.Format("MachineName:{0}", Process.GetCurrentProcess().MachineName.ToString())));
        app.Run(context => context.Response.WriteAsync(string.Format("osDescription:{0}", osDescription)));

            app.Run(async (context) =>
            {
                context.Response.ContentType = "text/plain; charset=utf-8";
                await context.Response.WriteAsync("Hello World!我是基于WebApplicationBuilder构建的宿主环境!!");
            });

            var summaries = new[]
            {
                "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
            };

            app.MapGet("/weatherforecast", (HttpContext httpContext) =>
            {
                var forecast = Enumerable.Range(1, 5).Select(index =>
                    new WeatherForecast
                    {
                        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                        TemperatureC = Random.Shared.Next(-20, 55),
                        Summary = summaries[Random.Shared.Next(summaries.Length)]
                    })
                    .ToArray();
                return forecast;
            });

            app.Run();
        }
    }
}
复制代码

  使用builder.WebHost.UseHttpSys(),因为ASP.NET Core中集成了HTTP.SYS服务器,然后在cmd中运行这个应用程序,如下图所示

   如果在Windows环境下部署,使用HTTP.SYS是一个不错的选择,提供很高的性能体验。

六、总结

  ASP.NET Core应用程序,在默认服务器、IIS服务器、HTTP.SYS服务器的发布部署提供了多种选择,具体使用按照实际的场景来决定,合理配置带来满足需求的体验。所以在.NET Core中对于这一块的设计还是很优秀的,上述只是Demo的实际,在生产环境还是要配置各自参数来适应生产环境的要求。

  参考https://www.cnblogs.com/artech/p/inside-asp-net-core-6-33.html、https://cloud.tencent.com/developer/article/1614524?areaId=106001、https://zhuanlan.zhihu.com/p/535207607、https://cloud.tencent.com/developer/article/1023250?areaId=106001

posted @   tuqunfu  阅读(1604)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
点击右上角即可分享
微信分享提示