在《歷數依賴注入的N種玩法》演示系統自動注冊服務的實例中,我們會發現輸出的列表包含兩個特殊的服務,它們的對應的服務接口分別是IApplicationLifetime和IHostingEnvironment,我們將分別實現這兩個接口的服務統稱在ApplicationLifetime和HostingEnvironment。我們從其命名即可以看出ApplicationLifetime與應用的聲明周期有關,而HostingEnvironment則用來表示當前的執行環境,本篇文章我們著重來了解ApplicationLifetime與整個AASP.NET Core應用的生命周期有何關系。[本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、ApplicationLifetime
二、WebHost的Run方法
三、遠程關閉應用
從命名的角度來看,ApplicationLifetime貌似是對當前應用生命周期的描述,而實際上它存在的目的僅僅是在應用啟動和關閉時對相關組件發送相應的信號或者通知而已。如下面的代碼片段所示,IApplicationLifetime接口具有三個CancellationToken類型的屬性(ApplicationStarted、ApplicationStopping和ApplicationStopped),如果需要在應用自動和終止前後執行某種操作,我們可以注冊相應的回調在這三個CancellationToken對象上。除了這三個類型為CancellationToken的屬性,IApplicationLifetime接口還定義了一個StopApplication方法,我們可以調用這個方法發送關閉應用的信號,並最終真正地關閉應用。
1: public interface IApplicationLifetime
2: {
3: CancellationToken ApplicationStarted { get; }
4: CancellationToken ApplicationStopping { get; }
5: CancellationToken ApplicationStopped { get; }
6:
7: void StopApplication();
8: }
ASP.NET Core默認使用的ApplicationLifetime是具有如下定義的一個同名類型。可以看出它實現的三個屬性返回的CancellationToken對象是通過三個對應的CancellationTokenSource生成。除了實現IApplicationLifetime接口的StopApplication方法用於發送“正在關閉”通知之外,這個類型還定義了額外兩個方法(NotifyStarted和NotifyStopped)用於發送“已經開啟/關閉”的通知。
1: public class ApplicationLifetime : IApplicationLifetime
2: {
3: private readonly CancellationTokenSource _startedSource = new CancellationTokenSource();
4: private readonly CancellationTokenSource _stoppedSource = new CancellationTokenSource();
5: private readonly CancellationTokenSource _stoppingSource = new CancellationTokenSource();
6:
7: public CancellationToken ApplicationStarted
8: {
9: get { return _startedSource.Token; }
10: }
11: public CancellationToken ApplicationStopped
12: {
13: get { return _stoppedSource.Token; }
14: }
15: public CancellationToken ApplicationStopping
16: {
17: get { return _stoppingSource.Token; }
18: }
19:
20: public void NotifyStarted()
21: {
22: _startedSource.Cancel(false);
23: }
24: public void NotifyStopped()
25: {
26: _stoppedSource.Cancel(false);
27: }
28: public void StopApplication()
29: {
30: _stoppingSource.Cancel(false);
31: }
32: }
當WebHost因Start方法的執行而被開啟的時候,它最終會調用ApplicationLifetime的NotifyStarted方法對外發送應用被成功啟動的信號。不知道讀者朋友們又被注意到,WebHost僅僅定義了啟動應用的Start方法,並不曾定義終止應用的Stop或者Close方法,它僅僅在Dispose方法中調用了ApplicationLifetime的StopApplication方法。
1: public class WebHost : IWebHost
2: {
3: private ApplicationLifetime _applicationLifetime;
4: public IServiceProvider Services { get;}
5:
6: public void Start()
7: {
8: ...
9: _applicationLifetime.NotifyStarted();
10: }
11:
12: public void Dispose()
13: {
14: _applicationLifetime.StopApplication();
15: (this.Services as IDisposable)?.Dispose();
16: _applicationLifetime.NotifyStopped();
17: }
18: ...
19: }
我們知道啟動應用最終是通過調用作為宿主的WebHost的Start方法來完成的,但是我們之前演示的所有實例都不曾顯式地調用過這個方法,我們調用的是它的擴展方法Run。毫無疑問,WebHost的Run方法肯定會調用Start方法來開啟WebHost,但是除此之外,這個Run方法還有何特別之處呢?
Run方法的目的除了啟動WebHost之外,它實際上會阻塞當前進程直到應用關閉。我們知道應用的關閉的意圖是通過利用ApplicationLifetime發送相應信號的方式實現的,所以這個Run方法在啟動WebHost的時候,會以阻塞當前線程的方式等待直至接收到這個信號。如下所示的代碼片段基本上體現了這兩個擴展方法Run的實現邏輯。
1: public static class WebHostExtensions
2: {
3: public static void Run(this IWebHost host)
4: {
5: using (CancellationTokenSource cts = new CancellationTokenSource())
6: {
7: //Ctrl+C: 關閉應用
8: Console.CancelKeyPress += (sender, args) =>
9: {
10: cts.Cancel();
11: args.Cancel = true;
12: };
13: host.Run(cts.Token);
14: }
15: }
16:
17: public static void Run(this IWebHost host, CancellationToken token)
18: {
19: using (host)
20: {
21: //顯示應用基本信息
22: host.Start();
23: IApplicationLifetime applicationLifetime = host.Services.GetService<IApplicationLifetime>();
24: token.Register(state => ((IApplicationLifetime)state).StopApplication(), applicationLifetime);
25: applicationLifetime.ApplicationStopping.WaitHandle.WaitOne();
26: }
27: }
28: }
上面這個代碼片段還體現了另一個細節。雖然WebHost實現了IDisposable接口,原則上我們需要在關閉的時候顯式地調用其Dispose方法。針對這個方法的調用非常重要,因為它的ServiceProvider只能在這個方法被調用時才能被回收釋放。但是之前所有演示的實例都沒有這麼做,因為Run方法會自動幫助回收釋放掉指定的這個WebHost。
既然WebHost在啟動之後會利用ApplicationLifetime等待Stopping信號的發送,這就意味著組成ASP.NET Core管道的服務器和任何一個中間件都可以在適當的時候調用ApplicationLifetime的StopApplication來關閉應用。對於《服務器在管道中的“龍頭”地位》介紹的KestrelServer,我們知道在構造這個對象的時候必須指定一個ApplicationLifetime對象,其根本的目的在於當發送某些無法恢復的錯誤時,它可以利用這個對象關閉應用。
接下來我們通過實例的方式來演示如何在一個中間件中利用這個ApplicationLifetime對象實現對應用的遠程關閉,為此我們將這個中間件命名為RemoteStopMiddleware。RemoteStopMiddleware實現遠程關閉應用的原理很簡單,我們遠程發送一個Head請求,並且在該請求中添加一個名為“Stop-Application”的報頭傳到希望關閉應用的意圖,該中間件接收到這個請求之後會關閉應用,而響應中會添加一個“Application-Stopped”報頭表明應用已經被關閉。
1: public class RemoteStopMiddleware
2: {
3: private RequestDelegate _next;
4: private const string RequestHeader = "Stop-Application";
5: private const string ResponseHeader = "Application-Stopped";
6:
7: public RemoteStopMiddleware(RequestDelegate next)
8: {
9: _next = next;
10: }
11:
12: public async Task Invoke(HttpContext context, IApplicationLifetime lifetime)
13: {
14: if (context.Request.Method == "HEAD" && context.Request.Headers[RequestHeader].FirstOrDefault() == "Yes")
15: {
16: context.Response.Headers.Add(ResponseHeader, "Yes");
17: lifetime.StopApplication();
18: }
19: else
20: {
21: await _next(context);
22: }
23: }
24: }
如上所示的代碼片段是RemoteStopMiddleware這個中間件的完整定義,實現邏輯很簡單,完全沒有必要再贅言解釋。我們在一個控制台應用中采用如下的程序啟動一個Hello World應用,並注冊此RemoteStopMiddleware中間件。在啟動這個應用之後,我們借助Fiddler發送向目標地址發送三次請求,其中第一次和第三次普通的GET請求,而第二次則是為了遠程關閉應用的HEAD請求。如下所示的是三次請求與響應的內容,由於應用被第二次請求關閉,所以第三次請求會返回一個狀態碼為502的響應。
1: //第1次請求與響應
2: GET http://localhost:5000/ HTTP/1.1
3: User-Agent: Fiddler
4: Host: localhost:5000
5:
6: HTTP/1.1 200 OK
7: Date: Sun, 06 Nov 2016 06:15:03 GMT
8: Transfer-Encoding: chunked
9: Server: Kestrel
10:
11: Hello world!
12:
13: //第2次請求與響應
14: HEAD http://localhost:5000/ HTTP/1.1
15: Stop-Application: Yes
16: User-Agent: Fiddler
17: Host: localhost:5000
18:
19: HTTP/1.1 200 OK
20: Date: Sun, 06 Nov 2016 06:15:34 GMT
21: Server: Kestrel
22: Application-Stopped: Yes
23:
24: //第3次請求與響應
25: GET http://localhost:5000/ HTTP/1.1
26: User-Agent: Fiddler
27: Host: localhost:5000
28:
29: HTTP/1.1 502 Fiddler - Connection Failed
30: Date: Sun, 06 Nov 2016 06:15:44 GMT
31: Content-Type: text/html; charset=UTF-8
32: Connection: close
33: Cache-Control: no-cache, must-revalidate
34: Timestamp: 14:15:44.790
35:
36: [Fiddler] The connection to 'localhost' failed...