如何远程关闭一个ASP.NET Core应用?

在《历数依赖注入的N种玩法》演示系统自动注册服务的实例中,我们会发现输出的列表包含两个特殊的服务,它们的对应的服务接口分别是IApplicationLifetime和IHostingEnvironment,我们将分别实现这两个接口的服务统称在ApplicationLifetime和HostingEnvironment。我们从其命名即可以看出ApplicationLifetime与应用的声明周期有关,而HostingEnvironment则用来表示当前的执行环境,本篇文章我们着重来了解ApplicationLifetime与整个AASP.NET Core应用的生命周期有何关系。[本文已经同步到《ASP.NET Core框架揭秘》之中]

目录 一、ApplicationLifetime 二、WebHost的Run方法 三、远程关闭应用

一、ApplicationLifetime

从命名的角度来看,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的Run方法

我们知道启动应用最终是通过调用作为宿主的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...

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏林德熙的博客

Roslyn 使用 Target 替换占位符方式生成 nuget 打包

在项目文件的相同文件夹可以放一个 nuspec 用来告诉 VisualStudio 如何打包

1252
来自专栏大内老A

.NET Core采用的全新配置系统[10]: 配置的同步机制是如何实现的?

配置的同步涉及到两个方面:第一,对原始的配置文件实施监控并在其发生变化之后从新加载配置;第二,配置重新加载之后及时通知应用程序进而使后者能够使用最新的配置。要了...

1978
来自专栏PHP在线

YII运行原理

应用执行流程: 浏览器向服务器发送 Http Request | 控制器(protected/controllers) | |—> Action | 创建模型 ...

3316
来自专栏Java编程技术

高并发编程必备基础(上)

借用Java并发编程实践中的话"编写正确的程序并不容易,而编写正常的并发程序就更难了",相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没...

1012
来自专栏章鱼的慢慢技术路

一步步使用Code::Blocks进行设置断点调试程序

1773
来自专栏小灰灰

Java可以如何实现文件变动的监听

应用中使用logback作为日志输出组件的话,大部分会去配置 logback.xml 这个文件,而且生产环境下,直接去修改logback.xml文件中的日志级别...

2418
来自专栏上善若水

L001 Linux和android ndk 外部程序调用popen 和system的用法

popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。这个进程必须由 pclose() 函数关闭...

1162
来自专栏Golang语言社区

Golang Template 简明笔记

作者:人世间 链接:https://www.jianshu.com/p/05671bab2357 來源:简书 前后端分离的Restful架构大行其道,传统的模板...

8706
来自专栏ASP.NETCore

通过修改CoreCLR中的ClrHost实现自托管程序

上一篇我们讲了如何在windows和Linux上编译CoreClr的问题 虽然文章使用的是windows 10 (Bash)环境,但是也可以做为ubuntu环境...

1463
来自专栏我的博客

Thinkphp3.2.3FTP上传文件同名覆盖问题

//原代码在\ThinkPHP\Library\Think\Upload\Driver\Ftp.class.php //大概在94行左右 /** ...

36410

扫码关注云+社区

领取腾讯云代金券