你的位置:首页 > ASP.net教程

[ASP.net教程]学习ASP.NET Core,怎能不了解请求处理管道[2]: 服务器在管道中的龙头地位


ASP.NET Core管道由注册的服务器和一系列中间件构成。我们在上一篇中深入剖析了中间件,现在我们来了解一下服务器。服务器是ASP .NET Core管道的第一个节点,它负责完整请求的监听和接收,最终对请求的响应同样也由它完成。[本文已经同步到《ASP.NET Core框架揭秘》之中]

服务器是我们对所有实现了IServer接口的所有类型以及对应对象的统称。如下面的代码片段所示,这个接口具有一个只读属性Features返回描述自身特性集合的FeatureCollection对象,另一个Start方法用于启动服务器。

  1: public interface IServer : IDisposable
  2: {
  3:   IFeatureCollection Features { get; }
  4:   void Start<TContext>(IHttpApplication<TContext> application);  
  5: }


当我们Start方法启动指定的Server的时候,必须指定一个类型为IHttpApplication<TContext>的参数,我们将实现才接口的所有类型及其对应对象统称为HttpApplication。当服务器在接收到抵达的请求之后,它会直接交给这个HttpApplication对象来处理,所以我们需要先来认识一下这个对象。

一、HttpApplication

对于ASP.NET Core管道来说,HttpApplication对会接管服务器接收的请求,后续的请求完全由它来负责。如下图所示,HttpApplication从服务器获得请求之后,会利用注册的中间件注册对请求进行处理,并最终将请求递交给应用程序。HttpApplication针对请求的处理实际上会在一个执行上下文中完成,这个上下文为应用对单一请求的整个处理过程定义了一个边界。单纯描述HTTP请求的HttpContext是这个执行上下文中最为核心的部分,除此之外,我们还可以根据需要将其他相关的信息定义其中,所以IHttpApplication<TContext>接口采用泛型参数的形式来表示定义这个上下文的类型。

image

HttpApplication不仅仅需要在这个执行上下文中处理服务器转发给它的请求,这个上下文对象的创建和回收释放同样需要由它来完成。如下面的代码片段所示,IHttpApplication<TContext>接口的CreateContext和DisposeContext方法分别体现了针对执行上下文的创建和释放,CreateContext方法的参数contextFeatures表示描述原始上下文的特性集合。在此上下文中针对请求的处理实现在另一个方法ProcessRequestAsync之中。

  1: public interface IHttpApplication<TContext>
  2: {
  3:   TContext CreateContext(IFeatureCollection contextFeatures);
  4:   void   DisposeContext(TContext context, Exception exception);
  5:   Task   ProcessRequestAsync(TContext context);
  6: }


在默认情况下创建的HttpApplication是一个HostingApplication对象。对于HostingApplication来说,它创建的执行上下文的类型是一个具有如下定义的结构Context。对于这个Context对象表示的针对当前请求的执行上下文来说,描述当前HTTP请求的HttpContext是最为核心的部分。除了这个HttpContext属性之外,Context还具有额外两个属性,其中Scope是为追踪诊断而创建的日志上下文范围,该范围将针对同一个请求的多项日志记录进行关联,而另一个属性StartTimestamp表示应用开始处理请求的时间戳。

  1: public class HostingApplication : IHttpApplication<Context>
  2: {
  3:   //省略成员
  4:   public struct Context
  5:   {
  6:     public HttpContext   HttpContext { get; set; }
  7:     public IDisposable   Scope { get; set; }
  8:     public long      StartTimestamp { get; set; }
  9:   }
 10: }


由于HostingApplication针对请求的处理是通过注册的中间件来完成的,而这些中间件最终会利用上面介绍的ApplicationBuilder对象转换成一个类型为RequestDelegate的委托对象,所有中间件对请求的处理通过执行这个委托对象来完成。我们在创建HostingApplication的时候需要提供这么一个RequestDelegate对象。由HostingApplication创建的Context对象包含表示HTTP上下文的HttpContext对象,而后者是通过对应的工厂HttpContextFactory创建的,所以HttpContextFactory在创建时也是必须要提供的。如下面的代码片段所示,HostingApplication类型的构造函数需要将这两个对象作为输入参数,至于另外两个参数(logger和diagnosticSource),它们与日志记录有关。

  1: public class HostingApplication : IHttpApplication<HostingApplication.Context>
  2: {
  3:   private readonly RequestDelegate     _application;
  4:   private readonly DiagnosticSource    _diagnosticSource;
  5:   private readonly IHttpContextFactory   _httpContextFactory;
  6:   private readonly ILogger         _logger;
  7:  
  8:   public HostingApplication(RequestDelegate application, ILogger logger, DiagnosticSource diagnosticSource, IHttpContextFactory httpContextFactory)
  9:   {
 10:     _application     = application;
 11:     _logger        = logger;
 12:     _diagnosticSource   = diagnosticSource;
 13:     _httpContextFactory  = httpContextFactory;
 14:   }
 15: }


下面给出的代码片段基本体现了HostingApplication创建和释放Context对象,以及在此上下文中处理请求的逻辑。在CreateContext方法中,它直接利用初始化提供的HttpContextFactory创建一个HttpContext并将其作为Context对象的同名属性,至于Context额外两个属性(Scope和StartTimestamp)该作何设置,我们会在本节后续部分对此作专门介绍。实现在ProcessRequestAsync方法中针对请求的处理最终体现在对构造时指定的这个RequestDelegate对象的执行。当DisposeContext方法被执行的时候,Context的Scope属性会率先被释放,在此之后HttpContextFactory的Dispose方法被调用以完成对Context对象自身的回收释放。

  1: public class HostingApplication : IHttpApplication<HostingApplication.Context>
  2: {
  3:   public Context CreateContext(IFeatureCollection contextFeatures)
  4:   {
  5:     //省略其他实现代码
  6:     return new Context
  7:     {
  8:        HttpContext   = _httpContextFactory.Create(contextFeatures),
  9:        Scope      = ...,
 10:        StartTimestamp  = ...
 11:     };
 12:   }
 13:  
 14:   public Task ProcessRequestAsync(Context context)
 15:   {
 16:     Return _application(context.HttpContext);
 17:   }
 18:  
 19:   public void DisposeContext(Context context, Exception exception)
 20:   {    
 21:     //省略其他实现代码
 22:     context.Scope.Dispose();
 23:     _httpContextFactory.Dispose(context.HttpContext);
 24:   }
 25: }


二、KestrelServer

跨平台是ASP.NET Core一个显著的特性,而KestrelServer是目前微软推出了唯一一个能够真正跨平台的服务器。KestrelServer利用一个名为KestrelEngine的网络引擎实现对请求的监听、接收和响应。KetrelServer之所以具有跨平台的特质,源于KestrelEngine是在一个名为libuv的跨平台网络库上开发的。说起libuv,就不得不谈谈libev,后者是Unix系统一个针对事件循环和事件模型的网络库。libev因其具有的高性能成为了继lievent和Event perl module之后一套最受欢迎的网络库。由于Libev不支持Windows,有人在libev之上创建了一个抽象层以屏蔽平台之间的差异,这个抽象层就是libuv。libuv在Windows平台上是采用IOCP的形式实现的,下图揭示了libuv针对Unix和Windows的跨平台实现原理。到目前为止,libuv支持的平台已经不限于Unix和Windows了,包括Linux(2.6)、MacOS和Solaris (121以及之后的版本)在内的平台在libuv支持范围之内。

4

如下所示的代码片段体现了KestrelServer这个类型的定义。除了实现接口IServer定义的Features属性之外,KestrelServer还具有一个类型为KestrelServerOptions的只读属性Options。这个属性表示对KestrelServer所作的相关设置,我们在调用构造函数时通过输入参数options所代表的IOptions<KestrelServerOptions>对象对这个属性进行初始化。构造函数还具有另两个额外的参数,它们的类型分别是IApplicationLifetime和ILoggerFactory,后者用于创建记录日志的Logger,前者与应用的生命周期管理有关。

  1: public class KestrelServer : IServer
  2: {  
  3:   public IFeatureCollection   Features { get; }
  4:   public KestrelServerOptions  Options { get; }
  5:  
  6:   public KestrelServer(IOptions<KestrelServerOptions> options, IApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory);
  7:   public void Dispose();
  8:   public void Start<TContext>(IHttpApplication<TContext> application);
  9: }


注册的KetrelServer在管道中会以依赖注入的方式被创建,并采用构造器注入的方式提供其构造函数的参数options,由于这个参数类型为IOptions<KestrelServerOptions>,所以我们利用Options模型以配置的方式来指定KestrelServerOptions对象承载的设置。比如我们可以将KestrelServer的相关配置定义在如下一个JSON文件中。

  1: {
  2:  "noDelay"      : false,
  3:  "shutdownTimeout"  : "00:00:10",
  4:  "threadCount"    : 10
  5: }


为了让应用加载这么一个配置文件(文件名假设为“KestrelServerOptions.json”),我们只需要按照如下的方式利用ConfigurationBuilder加载这个配置文件并生成相应的Configuration对象,最后按照Options模型的编程方式完成KestrelServerOptions类型和该对象的映射即可。针对KestrelServerOptions的服务注册也可以定义在启动类型的ConfigureServices方法中。

  1: IConfiguration config = new ConfigurationBuilder()
  2:   .AddJsonFile("KestrelServerOptions.json")
  3:   .Build();
  4:  
  5: new WebHostBuilder()
  6:   .UseKestrel()
  7:   .ConfigureServices(services=>services.Configure<KestrelServerOptions>(config))
  8:   .Configure(app => app.Run(async context => await context.Response.WriteAsync("Hello World")))
  9:   .Build()
 10:   .Run();


我们一般通过调用WebHostBuilder的扩展方法UseKestrel方法来完成对KestrelServer的注册。如下面的代码片段所示,UseKestrel方法具有两个重载,其中一个具有同一个类型为Action<KestrelServerOptions>的参数,我们可以利用这个参数直接完成对KestrelServerOptions的设置。

  1: public static class WebHostBuilderKestrelExtensions
  2: {
  3:   public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder);
  4:   public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder, Action<KestrelServerOptions> options);
  5: }


由于服务器负责请求的监听、接收和响应,所以Server是影响整个Web应用响应能力和吞吐量最大的因素之一,为了更加有效地使用服务器,我们往往针对具体的网络负载状况对其作针对性的设置。对于KestrelServer来说,在构造函数中作为参数指定的KestrelServerOptions对象代表针对它所做的设置。我们针对KestrelServer所做的设置主要体现在KestrelServerOptions类型的如下5个属性上。

  1: public class KestrelServerOptions
  2: {  
  3:   //省略其他成员
  4:   public int     MaxPooledHeaders { get; set; }
  5:   public int     MaxPooledStreams { get; set; }
  6:   public bool     NoDelay { get; set; }
  7:   public TimeSpan   ShutdownTimeout { get; set; }
  8:   public int     ThreadCount { get; set; }
  9: }


三、ServerAddressesFeature

在演示的实例中,我们实际上并不曾为注册的KestrelServer指定一个监听地址,从运行的效果我们不难看出,WebHost在这种情况下会指定“http://localhost:5000”为默认的监听地址。服务器的监听地址自然可以显式指定。在介绍如何通过编程的方式为服务器指定监听地址之前,我们有先来认识一个名为ServerAddressesFeature的特性。

我们知道表示服务器的接口IServer中定义了一个类型为IFeatureCollection 的只读属性Features,它表示用于描述当前服务器的特性集合,ServerAddressesFeature作为一个重要的特性,就包含在这个集合之中。我们所说的ServerAddressesFeature对象是对所有实现了IServerAddressesFeature接口的所有类型及其对应对象的统称,该接口具有一个唯一的只读属性返回服务器的监听地址列表。ASP.NET Core默认使用的ServerAddressesFeature是具有如下定义的同名类型。

  1: public interface IServerAddressesFeature
  2: {
  3:   ICollection<string> Addresses { get; }
  4: }
  5:  
  6: public class ServerAddressesFeature : IServerAddressesFeature
  7: {
  8:   public ICollection<string> Addresses { get; }
  9: }


对于WebHost在通过依赖注入的方式创建的服务器,由它的Features属性表示的特性集合中会默认包含这么一个ServerAddressesFeature对象。如果没有一个合法的监听地址被添加到这个 ServerAddressesFeature对象的地址列表中,WebHost会将显式指定的地址(一个或者多个)添加到该列表中。我们显式指定的监听地址实际上是作为WebHost的配置保存在一个Configuration对象上,配置项对应的Key为“urls”,WebHostDefaults的静态只读属性ServerUrlsKey返回的就是这么一个Key。

  1: new WebHostBuilder()
  2:   .UseSetting(WebHostDefaults.ServerUrlsKey, "http://localhost:3721/")
  3:   .UseMyKestrel()
  4:   .UseStartup<Startup>()
  5:   .Build()
  6:   .Run();


WebHost的配置最初来源于创建它的WebHostBuilder,后者提供了一个UseSettings方法来设置某个配置项的值,所以我们可以采用如上的方式来指定监听地址(“http://localhost:3721/”)。不过,针对监听地址的显式设置,最直接的编程方式还是调用WebHostBuilder的扩展方法UseUrls,如下面的代码片段所示,该方法的实现逻辑与上面完全一致。

  1: public static class WebHostBuilderExtensions
  2: {
  3:   public static IWebHostBuilder UseUrls(this IWebHostBuilder hostBuilder, params string[] urls) 
  4:   =>hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, string.Join(ServerUrlsSeparator, urls)) ;  
  5: }