ASP.NET Core 源碼閱讀筆記(3) ---Microsoft.AspNetCore.Hosting


有關Hosting的基礎知識 

    Hosting是一個非常重要,但又很難翻譯成中文的概念。翻譯成:寄宿,大概能勉強地傳達它的意思。我們知道,有一些病毒離開了活體之后就會死亡,我們把那些活體稱為病毒的宿主。把這種概念應用到托管程序上來,CLR不能單獨存在,它必須依賴於某一個進程,我們把這種狀況稱之為:CLR必須寄宿於某一個進程中,而那個進程就是宿主

   ASP.NET Core的一個大的改變就是就是將Web應用程序改成了自寄宿。什么意思呢?我們知道,在之前的ASP.NET版本中,ASP.NET的Web應用程序都是深度依賴IIS和Windows Server,以至於ASP.NET只能在Windows Server上運行。之所以出現這種情況,就是應為我們開發的所有Web應用程序都是寄宿在IIS進程中的。一般來說,一個進程只能加載一個CLR(不同進程之間可以加載不同的版本的CLR),為了托管多個Web應用程序,IIS使用了應用程序池這種東西來模擬進程的行為,從而為不同的Web程序加載不同的運行時來托管它們。

    有關CLR和寄宿的知識,如果有興趣,可以參閱《CLR via C#》。

    我們可以查看一下以前版本的ASP.NET程序,它是沒有Main()函數的,也就是說它沒有程序入口點,不是單獨的進程。對於應用程序開發來說,這個問題並不大,因為開發者在意的Web程序的邏輯、數據安全等問題,而不是應用程序如何被加載。但對於一個Web框架來說,這個問題非常嚴重,因為它高度依賴IIS和Windows Server,減少了它的適用范圍。如果我們查看ASP.NET Core的程序,你會發現它本質上就是一個控制台程序,如果我們把那些在Main()函數中自動生成的代碼都刪掉(VS2015的模板會自帶一些代碼),加上Console.WriteLine("Hello World!"); 它就會在控制台中打出Hello World!由於ASP.NET Core的程序自身有程序入口點,所以自身就是一個進程,它可以為自己加載合適的CLR來運行Web應用,這種情況就是自寄宿。這么做的最大的好處就是可以脫離IIS,從而脫離Windows Server的桎梏。只要對應操作系統上有符合CLR規范的運行時,那ASP.NET Core的應用就可以部署在那個操作系統上。.NET Core里包含了微軟開發的跨平台CLR運行時,可以運行在Windows,Linux和OSX上,借助它ASP.NET Core的應用程序就可以部署在這些操作系統上。

    說到這里,就只能下最后一個問題,IIS還扮演什么角色?當應用部署在Windows上時,微軟推薦將IIS通過ASP.NET Core Module(之前的HttpPlatformHandler)模塊作為Web應用的反向代理服務器(reverse-proxy server)。這個服務器的作用就是將請求轉發到Web應用真正的服務器:

  • WebListener (只能在Windows平台)
  • Kestrel         (跨平台服務器,比WebListener功能稍弱)

    服務器的問題會在下一篇文章中說明。前面上了那么多開胃菜,終於可以上正菜了。以下出現的所有源碼都可以在Microsoft.AspNetCore.Hosting項目中找到。

Main函數里發生了什么

    如果新建一個ASP.NET Core應用,那么最先和我們打交道的就是Main函數中的WebHostBuilder,我們先來看看它的源碼:

 1     //所有方法刪除了錯誤處理,只保留主邏輯
 2     public class WebHostBuilder : IWebHostBuilder  3  {  4         private readonly IHostingEnvironment _hostingEnvironment;  5         private readonly List<Action<IServiceCollection>> _configureServicesDelegates;  6         private readonly List<Action<ILoggerFactory>> _configureLoggingDelegates;  7 
 8         private IConfiguration _config = new ConfigurationBuilder().AddInMemoryCollection().Build();  9         private ILoggerFactory _loggerFactory; 10         private WebHostOptions _options; 11         
12         //_config里面存儲的是每個應用都有的東西,比如根目錄,StartUp類的程序集公鑰等。
13         public IWebHostBuilder UseSetting(string key, string value) 14  { 15             _config[key] = value; 16             return this; 17  } 18         public IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices) 19  { 20  _configureServicesDelegates.Add(configureServices); 21             return this; 22  } 23         public IWebHost Build() 24  { 25             var hostingServices = BuildHostingServices();//在字段中,服務是以委托的形式存在,這個方法將配置的服務放入ServiceCollection,同時加入很多其他服務。
26             var hostingContainer = hostingServices.BuildServiceProvider(); 27 
28             var host = new WebHost(hostingServices, hostingContainer, _options, _config);//構造一個WebHost
29 
30             host.Initialize();//這個方法很重要,后面講
31 
32             return host; 33  } 34        //在這個方法中,_options會根據_config字段生成,StartUp類會以IStartUp->StartUp的形式注冊到依賴注入容器中,即使StartUp不實現接口 35        //注意這個方法只在Build()方法中被調用,所以UseStartUp()方法應該在Build()方法之前被調用
36        private IServiceCollection BuildHostingServices(){...}

     在每個ASP.NET Core程序的Main函數里面,都有很多UseXXX()的擴展方法,這些方法最終會調用WebHostBuilder.UseSetting()或者ConfigureService()方法。這兩個東西的區別在於,_config里面存的東西是每個Web程序都有的部分,比如應用的根目錄,StartUp類的程序集信息等等;而Service是Web應用的可選部分,比如UseKestrel()最終調用的是ConfigureServices,因為服務器並不是必須的,可以開發符合Owin規范的程序使應用部分和服務器部分分開來。還有一個區別在於_config中的內容比較簡單,比如存儲的應用根目錄這些東西不必以服務的形式存在,當然有關StartUp的信息還是會以服務的形式注冊到依賴注入容器。

    WebHostBuilder的這些信息最終會傳給WebHost,注意構造WebHost之后,還調用了它的Initialize()方法。我們來看看WebHost類的源碼。

 1     public class WebHost : IWebHost  2  {  3         private readonly IServiceCollection _applicationServiceCollection;//UseStartUp等服務
 4         private IStartup _startup;//StartUp
 5 
 6         private readonly IServiceProvider _hostingServiceProvider;//上面那個服務集合生成的Provider
 7         private readonly ApplicationLifetime _applicationLifetime;//為了異步能取消
 8         private readonly WebHostOptions _options;  9         private readonly IConfiguration _config; 10 
11         private IServiceProvider _applicationServices; //StartUp ConfigureService方法生成的服務+原本applicationService
12         private RequestDelegate _application; 13         private IServer Server { get; set; } //服務器字段,包含了處理的請求的信息 14         //下面是相關方法
15         public void Initialize()//轉發給BuildApplication()方法
16  { 17             if (_application == null) 18  { 19                 _application = BuildApplication(); 20  } 21  } 22         private void EnsureApplicationServices()//把在StartUp.ConfigureServices()方法中注冊的服務加到集合中
23  { 24             if (_applicationServices == null) 25  { 26  EnsureStartup(); 27                 _applicationServices = _startup.ConfigureServices(_applicationServiceCollection); 28  } 29  } 30         private void EnsureStartup()//構造StartUp服務類
31  { 32             _startup = _hostingServiceProvider.GetRequiredService<IStartup>(); 33  } 34 
35         //構造委托鏈_application ,ApplicationService存儲現在注冊的服務
36         private RequestDelegate BuildApplication() 37  { 38             try
39  { 40  EnsureApplicationServices(); 41  EnsureServer(); 42 
43                 var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>(); 44                 var builder = builderFactory.CreateBuilder(Server.Features); 45                 builder.ApplicationServices = _applicationServices; 46 
47                 var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>(); 48                 Action<IApplicationBuilder> configure = _startup.Configure; 49                 foreach (var filter in startupFilters.Reverse()) 50  { 51                     configure = filter.Configure(configure);//委托鏈
52  } 53 
54  configure(builder); 55 
56                 return builder.Build();//Use方法最終都會ApplicationBuilder.Use()方法,以Fun<RequestDelegate, RequestDelegate>的形式存在List中 57                                        //Build的時候利用上面的List,會生成一個委托鏈。 58                                        //在每一個RequestDelegate中,會用反射生成對應的Middleware類,然后調用Invoke()方法
59  } 60             catch (Exception ex) when (_options.CaptureStartupErrors) 61             {//省略出錯的處理,返回HTTP500狀態碼}
62         }

     直接看BuildApplication()方法,

  1. 調用EnsureApplicationServices()方法,實際上就是調用StartUp.ConfigureServices()這個方法,這個方法大家肯定很熟,就是把那些在ConfigureServices()注冊的服務放到_applicationServices字段里;
  2. 調用EnsureServer()方法,確保Server存在,並監聽正確的端口,默認是 http://localhost:5000 ,這個方法這里沒有列出。
  3. 構造一個ApplicationBuilder,並把注冊的服務轉移給它;
  4. 用StartUp.Configure以及StartUpFilters.Configure構造一個服務委托鏈;
  5. 引發服務委托鏈,相當於調用里面的UseXXX()方法,注意看我注釋的解釋,此時所有服務都以使用順序、以Func<RequestDelegate, RequestDelegate>形式存儲在一個ApplicationBuilder的List字段中
  6. Invoke List字段中的所有對象,生成一個委托鏈,就是我們所說的請求管道(Pipeline)。

    請求管道已經生成完畢,剩下的就是請HttpContext進入這個管道了,我們看看WebHost.Run()發生了什么

Host.Run()方法中發生了什么?

     Host.Run()內部會調用Host.Start()方法,然后再在控制台輸出一些信息,並且傳入一個CancellationToken 允許隨時中斷程序。那么看來得去瞧瞧Host.Start()方法了。

1         public virtual void Start() 2  { 3             //省略一些參數構造,以及有關logger的代碼
4             Server.Start(new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory)); //只有這句是關鍵 
5         }

     它調用了Server.Start()方法,從方法名上大概能猜出來是干嘛,具體細節留在有關Server的文章里面講。去看看HostingApplication這個類:

 1     public class HostingApplication : IHttpApplication<HostingApplication.Context>
 2  {  3         private readonly RequestDelegate _application;  4         private readonly ILogger _logger;  5         private readonly DiagnosticSource _diagnosticSource;  6         private readonly IHttpContextFactory _httpContextFactory;  7 
 8         public Context CreateContext(IFeatureCollection contextFeatures){...}//創建一個上下文
 9 
10         public void DisposeContext(Context context, Exception exception){...} 11 
12         public Task ProcessRequestAsync(Context context) 13  { 14             return _application(context.HttpContext); 15  } 16 
17         public struct Context 18  { 19             public HttpContext HttpContext { get; set; } 20             public IDisposable Scope { get; set; } 21             public long StartTimestamp { get; set; } 22         }

     顯然它的作用就是配合Server去創建上下文,大概的過程就是

  1. 服務器把請求的信息放入一個IFeatureCollection的變量里面;
  2. 利用上面的信息構造上下文;
  3. 調用ProcessRequestAsync()方法處理請求,此時請求進入處理管道(Pipeline)。

總結

  1. 首先使用WebHostBuildler注冊基本信息,比如用哪個服務器?根目錄是哪個?StartUp的元數據等等;
  2. 在WebHost = WebHostBuildler.Build()過程中,添加大量基本服務+StartUp.ConfigureServices()方法中的服務;
  3. 在WebHost.Initialize()方法中,利用StartUp.Configure()方法中使用的服務+一些默認使用的服務組建請求管道,並存儲在_application字段中;
  4. 使用_application構造一個HostingApplication,並傳入WebHost.Run()->WebHost.Start()->Server.Start()方法;
  5. Server使用HostingApplication來構造HttpContext,並使用請求管線(Pipeline)處理它。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM