有關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()方法,
- 調用EnsureApplicationServices()方法,實際上就是調用StartUp.ConfigureServices()這個方法,這個方法大家肯定很熟,就是把那些在ConfigureServices()注冊的服務放到_applicationServices字段里;
- 調用EnsureServer()方法,確保Server存在,并監聽正確的端口,默認是 http://localhost:5000 ,這個方法這里沒有列出。
- 構造一個ApplicationBuilder,并把注冊的服務轉移給它;
- 用StartUp.Configure以及StartUpFilters.Configure構造一個服務委托鏈;
- 引發服務委托鏈,相當于調用里面的UseXXX()方法,注意看我注釋的解釋,此時所有服務都以使用順序、以Func<RequestDelegate, RequestDelegate>形式存儲在一個ApplicationBuilder的List字段中;
- 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去創建上下文,大概的過程就是
- 服務器把請求的信息放入一個IFeatureCollection的變量里面;
- 利用上面的信息構造上下文;
- 調用ProcessRequestAsync()方法處理請求,此時請求進入處理管道(Pipeline)。
總結
- 首先使用WebHostBuildler注冊基本信息,比如用哪個服務器?根目錄是哪個?StartUp的元數據等等;
- 在WebHost = WebHostBuildler.Build()過程中,添加大量基本服務+StartUp.ConfigureServices()方法中的服務;
- 在WebHost.Initialize()方法中,利用StartUp.Configure()方法中使用的服務+一些默認使用的服務組建請求管道,并存儲在_application字段中;
- 使用_application構造一個HostingApplication,并傳入WebHost.Run()->WebHost.Start()->Server.Start()方法;
- Server使用HostingApplication來構造HttpContext,并使用請求管線(Pipeline)處理它。
文章列表