[ASP.NET Core 3框架揭秘]服务承载系统[3]:总体设计[上篇]
前面的实例演示了服务承载的基本编程模式,接下来我们从设计的角度来重新认识服务承载模型。总的来说,服务承载模型主要由如下图所示的三个核心对象组成:多个通过IHostedService接口表示的服务被承载于通过IHost接口表示的宿主上,IHostBuilder接口表示IHost对象的构建者。
一、IHostedService
承载的服务总是会被定义成IHostedService接口的实现类型。如下面的代码片段所示,该接口仅定义了两个用来启动和关闭自身服务的方法。当作为宿主的IHost对象被启动的时候,它会利用依赖注入框架激活每个注册的IHostedService服务,并通过调用StartAsync方法来启动它们。当服务承载应用程序关闭的时候,作为服务宿主的IHost对象会被关闭,由它承载的每个IHostedService服务对象的StopAsync方法也随之被调用。
public interface IHostedService { Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); }
承载系统无缝集成了.NET Core的依赖注入框架,在服务承载过程中所需的依赖服务,包括承载服务自身和它所依赖的服务均由此框架提供,承载服务注册的本质就是将对应的IHostedService实现类型或者实例注册到依赖注入框架中。由于承载服务大都需要长时间运行直到应用被关闭,所以针对承载服务的注册一般采用Singleton生命周期模式。承载系统为承载服务的注册定义了如下这个AddHostedService<THostedService>扩展方法。由于该方法通过调用TryAddEnumerable扩展方法来注册服务,所以不用担心服务重复注册的问题。
public static class ServiceCollectionHostedServiceExtensions { public static IServiceCollection AddHostedService<THostedService>(this IServiceCollection services) where THostedService: class, IHostedService { services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, THostedService>()); return services; } }
二、IHost
通过IHostedService接口表示的承载服务最终被承载于通过IHost接口表示的宿主上。一般来说,一个服务承载应用在整个生命周期内只会创建一个IHost对象,我们启动和关闭应用程序本质上就是启动和关闭作为宿主的IHost对象。如下面的代码片段所示,IHost接口派生于IDisposable接口,所以当它在关闭之后,应用程序还会调用其Dispose方法作一些额外的资源释放工作。IHost接口的Services属性返回作为依赖注入容器的IServiceProvider对象,该对象提供了服务承载过程中所需的服务实例,其中就包括需要承载的IHostedService服务。定义在IHost接口中的StartAsync和StopAsync方法完成了针对服务宿主的启动和关闭。
public interface IHost : IDisposable { IServiceProvider Services { get; } Task StartAsync(CancellationToken cancellationToken = default); Task StopAsync(CancellationToken cancellationToken = default); }
三、应用生命周期
在前面演示的实例中,在利用HostBuilder对象构建出IHost对象之后,我们并没有调用其StartAsync方法启动它,而是另一个名为Run的扩展方法。Run方法涉及到服务承载应用生命周期的管理,如果想了解该方法的本质,就得先来认识一个名为IHostApplicationLifetime的接口。顾名思义,IHostApplicationLifetime接口体现了服务承载应用程序的生命周期。如下面的代码片段所示,该接口除了提供了三个CancellationToken类型的属性来检测应用何时开启与关闭之外,还提供了一个StopApplication来关闭应用程序。
public interface IHostApplicationLifetime { CancellationToken ApplicationStarted { get; } CancellationToken ApplicationStopping { get; } CancellationToken ApplicationStopped { get; } void StopApplication(); }
如下所示的ApplicationLifetime类型是对IHostApplicationLifetime接口的默认实现。我们可以看到它实现的三个属性返回的CancellationToken对象来源于三个对应的CancellationTokenSource对象,后者对应着三个不同的方法(NotifyStarted、StopApplication和NotifyStopped)。我们可以利用IHostApplicationLifetime服务的三个属性提供的CancellationToken对象得到关于应用被启动和关闭通知,这些通知最初就是由这三个对应的方法发出来的。
public class ApplicationLifetime : IHostApplicationLifetime { private readonly ILogger<ApplicationLifetime> _logger; private readonly CancellationTokenSource _startedSource; private readonly CancellationTokenSource _stoppedSource; private readonly CancellationTokenSource _stoppingSource; public ApplicationLifetime(ILogger<ApplicationLifetime> logger) { _startedSource = new CancellationTokenSource(); _stoppedSource = new CancellationTokenSource(); _stoppingSource = new CancellationTokenSource(); _logger = logger; } private void ExecuteHandlers(CancellationTokenSource cancel) { if (!cancel.IsCancellationRequested) { cancel.Cancel(false); } } public void NotifyStarted() { try { this.ExecuteHandlers(this._startedSource); } catch (Exception exception) { _logger.ApplicationError(6, "An error occurred starting the application",exception); } } public void NotifyStopped() { try { ExecuteHandlers(this._stoppedSource); } catch (Exception exception) { _logger.ApplicationError(8, "An error occurred stopping the application",exception); } } public void StopApplication() { CancellationTokenSource source = this._stoppingSource; lock (source) { try { ExecuteHandlers(this._stoppingSource); } catch (Exception exception) { _logger.ApplicationError(7, "An error occurred stopping the application", exception); } } } public CancellationToken ApplicationStarted => _startedSource.Token; public CancellationToken ApplicationStopped => _stoppedSource.Token; public CancellationToken ApplicationStopping => _stoppingSource.Token; }
四、利用IHostApplicationLifetime关闭应用
我们接下来通过一个简单的实例来演示如何利用IHostApplicationLifetime服务来关闭整个承载应用程序。我们在一个控制台应用程序中定义了如下这个承载服务FakeHostedService。在FakeHostedService类型的构造函数中,我们注入了IHostApplicationLifetime服务。在得到其三个属性返回的CancellationToken对象之后,我们在它们上面分别注册了一个回调,回调操作通过在控制台上输出相应的文字使我们可以知道应用程序何时被启动和关闭。
public sealed class FakeHostedService : IHostedService { private readonly IHostApplicationLifetime _lifetime; private IDisposable _tokenSource; public FakeHostedService(IHostApplicationLifetime lifetime) { _lifetime = lifetime; _lifetime.ApplicationStarted.Register(() => Console.WriteLine("[{0}]Application started", DateTimeOffset.Now)); _lifetime.ApplicationStopping.Register(() => Console.WriteLine("[{0}]Application is stopping.", DateTimeOffset.Now)); _lifetime.ApplicationStopped.Register(() => Console.WriteLine("[{0}]Application stopped.", DateTimeOffset.Now)); } public Task StartAsync(CancellationToken cancellationToken) { _tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token.Register(_lifetime.StopApplication); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { _tokenSource?.Dispose(); return Task.CompletedTask; } }
在实现的StartAsync方法中,我们采用如上的方式在5秒之后调用IHostApplicationLifetime服务的StopApplication方法来关闭整个应用程序。这个FakeHostedService服务最后采用如下的方式承载于当前应用程序中。
class Program { static void Main() { new HostBuilder() .ConfigureServices(svcs => svcs.AddHostedService<FakeHostedService>()) .Build() .Run(); } }
该程序运行之后会在控制台上输出如下图所示的结果,从三条消息产生的时间间隔我们可以确定当前应用程序正是承载FakeHostedService通过调用IHostApplicationLifetime服务的StopApplication方法关闭的。(源代码从这里下载)
五、Run扩展方法
如果我们调用IHost对象的扩展方法Run,它会在内部调用StartAsync方法,接下来它会持续等待下去直到接收到应用被关闭的通知。当IHost对象对象利用IHostApplicationLifetime服务接收到关于应用关闭的通知后,它会调用自身的StopAsync方法,针对Run方法的调用此时才会返回。启动IHost对象直到应用关闭这一实现体现在如下这个WaitForShutdownAsync扩展方法上。
public static class HostingAbstractionsHostExtensions { public static async Task WaitForShutdownAsync(this IHost host, CancellationToken token = default) { var applicationLifetime = host.Services.GetService<IHostApplicationLifetime>(); token.Register(state => ((IHostApplicationLifetime)state).StopApplication(), applicationLifetime); var waitForStop = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); applicationLifetime.ApplicationStopping.Register(state => { var tcs = (TaskCompletionSource<object>)state; tcs.TrySetResult(null); }, waitForStop); await waitForStop.Task; await host.StopAsync(); } }
如下所示的WaitForShutdown方法是上面这个WaitForShutdownAsync方法的同步版本。同步的Run方法和异步的RunAsync方法的实现也体现在下面的代码片段中。除此之外,下面的代码片段还提供了Start和StopAsync这两个扩展方法,前者可以视为StartAsync方法的同步版本,后者可以在关闭IHost对象的时候指定一个超时时限。
public static class HostingAbstractionsHostExtensions { public static void WaitForShutdown(this IHost host) => host.WaitForShutdownAsync().GetAwaiter().GetResult(); public static void Run(this IHost host) => host.RunAsync().GetAwaiter().GetResult(); public static async Task RunAsync(this IHost host, CancellationToken token = default) { try { await host.StartAsync(token); await host.WaitForShutdownAsync(token); } finally { host.Dispose(); } } public static void Start(this IHost host) => host.StartAsync().GetAwaiter().GetResult(); public static Task StopAsync(this IHost host, TimeSpan timeout) => host.StopAsync(new CancellationTokenSource(timeout).Token); }
服务承载系统[1]: 承载长时间运行的服务[上篇]
服务承载系统[2]: 承载长时间运行的服务[下篇]
服务承载系统[3]: 总体设计[上篇]
服务承载系统[4]: 总体设计[下篇]
服务承载系统[5]: 承载服务启动流程[上篇]
服务承载系统[6]: 承载服务启动流程[下篇]