在.NET Core中设计自己的服务宿主(Service Hosting)框架

很多时候我们都会有设计一个后台服务的需求,比如,传统的Windows Service,或者Linux下的守护进程。这类应用的一个共同特点就是后台运行,并且不占用控制台界面。通常情况下,后台服务在提供服务时,会通过日志输出来记录服务处理的详细信息,用户也可以根据具体需要来设置不同的日志级别(Log Level),从而决定日志的输出详细程度。无论是传统的Windows Service还是Linux守护进程,都是开发人员非常熟悉的应用程序形式,开发技术和开发模式都是大家所熟知的,那么,在.NET Core中,又如何专业地实现这类后台服务应用呢?

其实,.NET Core的开发人员应该早就接触过并且使用过某种基于.NET Core的后台服务的开发技术了,它就是ASP.NET Core。ASP.NET Core应用程序在启动后,通过监听端口接受来自客户端的HTTP请求,然后根据路由策略定位到某个控制器(Controller)的某个方法(Action)上,接着将处理结果又以HTTP Response的形式返回给客户端(此处描述省略了Filter等步骤)。ASP.NET Core作为后台服务的一个最大特点是,它是专为HTTP协议定制的,也就是说,ASP.NET Core有着非常强大的处理HTTP协议与通信管道的能力。很显然,在某些场景中,服务端与客户端的通信并非基于HTTP协议,甚至于后台服务仅仅是在本地处理一些批量的事务,并不会涉及与其它服务或者客户端的交互。在这种情况下,使用ASP.NET Core就会显得比较重了。

在上面,我特别强调了“专业地”三个字,如何理解什么叫“专业”?我想,简单地说,就是我们所设计的后台服务程序,在基础设施部分,能够做到与ASP.NET Core相当的编程模型,并且能够达到与ASP.NET Core相当的扩展能力,具体地说,主要有以下几个方面:

  1. 具有非常好的隔离性:开发者只需要关注怎么实现自己的后台服务逻辑即可,不需要关注服务运行的保障体系,比如:如何正常终止服务、如何写入日志、如何管理对象生命周期等等

  2. 具有非常好的编程体验:使用过ASP.NET Core的开发者能够快速上手,直击主题,快速实现业务处理逻辑

  3. 可扩展、可配置的应用程序配置体系

  4. 可扩展、可配置的日志体系

  5. 可扩展、可配置的依赖注入体系

  6. 对服务宿主环境的区分。比如:在ASP.NET Core中,通常分为Development、Test、Staging、Production等环境,不同的环境可以有不同的配置信息等

在.NET Core 2.1以前,要在后台服务中自己实现上述各项是很不容易的,但从.NET Core 2.1开始,我们就可以直接使用.NET Generic Host体系,来实现自己的后台服务程序(也称为服务宿主程序)。根据微软官方文档,服务宿主程序分为两种:Web Hosting和Generic Hosting,前者主要处理HTTP请求,ASP.NET Core就是基于Web Hosting,但在今后,Generic Hosting会一统江湖,以做到能够同时处理HTTP和非HTTP两种不同的使用场景。基于.NET Generic Host,我们可以打造自己的服务宿主(Service Hosting)框架,以便在实际项目中能够基于这个框架来快速实现不同的后台服务应用场景。

设计

从本质上讲,一个.NET Core服务宿主程序只需要实现IHostedService接口,然后在控制台应用程序中通过HostBuilder来建立一个Host实例,并将IHostedService的实例注册到Host中,然后直接运行即可。下面的代码展示了这种最基础的实现方式:


class MyService : IHostedService
{
    public Task StartAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("Host Started");
        return Task.CompletedTask;
    }
    public Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("Host Stopped");
        return Task.CompletedTask;
    }
}
class Program
{
    static async Task Main(string[] args)
    {
        var hostBuilder = new HostBuilder()
            .ConfigureServices(serviceCollection =>
            {
                serviceCollection.AddSingleton<IHostedService, MyService>();
            });
        await hostBuilder.RunConsoleAsync();
    }
}

我们已经成功地实现了一个服务宿主程序,请使用C# 7.2或更高的版本来编译上面的代码,因为使用了异步Main函数。执行程序,会在控制台打印Host Started的字样,并提示目前的执行环境为Production,按下CTRL+C可以结束程序的执行。在按下CTRL+C时,控制台又会输出Host Stopped字样,然后程序退出。

上面的代码最关键的一点就是要将IHostedService的实现类注册到依赖注入框架中,于是,Host Builder在运行主机(Host)的时候,就会通过IHostedService接口类型找到已注册的实例,然后运行服务。通过Host Builder,我们还可以对宿主程序的执行环境、配置信息、日志等各方面进行配置,从而提供更为强大的服务端功能。比如在上面的代码中,仅仅是通过Console.WriteLine的调用来输出信息,这种做法并不好,因为如果服务运行于后台,是不能访问控制台的,我们需要日志发布机制。

由此可见,还有很多工作我们需要完成,总结起来,我们希望有一个简单的框架,在这个框架中,配置、日志、宿主环境等等设置都已遵循常规的标准做法,我们只需要关注于实现上面的StartAsync和StopAsync方法即可,这样的框架基本上也就能够满足大多数的服务宿主应用程序的开发需求。所谓的“遵循常规的标准做法”,意思就是:

  1. 可以通过配置文件、命令行或者环境变量来指定目前的宿主环境(是Development、Test、Staging还是Production)

  2. 可以通过配置文件、命令行或者环境变量来提供程序执行的配置信息

  3. 可以提供基本的日志定义和输出机制,比如可以通过配置文件来配置日志系统,并将日志输出到控制台

  4. 还可以提供一些额外的编程接口,以保证循环任务的合理退出、资源的合理释放等等

根据上述需求分析,以及.NET Core中服务宿主程序的基本实现技术,我做出了如下的设计:

  • 设计一个ServiceHost的类型,它的主要任务就是托管一种后台服务,它包含服务的启动与停止的逻辑。因此,ServiceHost是IHostedService的一种实现

  • 设计一个ServiceRunner的类型,它的主要任务是配置运行环境,并对ServiceHost进行注册。因此,ServiceRunner基本上就类似于ASP.NET Core中Startup类的职责,在里面可以进行各种配置和服务注册,为ServiceHost的执行提供环境

基于这样的设计,当我需要实现一个宿主服务时,我只需要继承ServiceHost类,实现其中的StartAsync和StopAsync方法,然后运行ServiceRunner,即可达到上述“标准做法”的要求。当然还可以继承ServiceRunner,以实现一些运行环境的高级配置。下面的类图展示了这样一种设计:


上面的设计可以看到,ServiceHost类提供了两个抽象方法:StartAsync、StopAsync,这两个方法都可以支持基于任务的异步执行模式(Task-based Asynchronous Pattern,TAP),在实际应用中,只需要实现这两个方法即可。ServiceHost所提供的OnHostStarted、OnHostStopped以及OnHostStopping回调方法,会在ServiceHost的生命周期的特定阶段被调用到,因此,如果有需要在服务启动完成、服务准备停止以及服务完成停止这几个阶段进行额外的处理的话,就可以根据自己的需要来重载这几个方法。

而服务宿主环境的配置,就实现在ServiceRunner中。ServiceRunner提供了类似ASP.NET Core中Startup类的一系列方法,在这些方法中,ServiceRunner完成了对应用程序配置信息、宿主环境配置信息、日志以及类型依赖项的配置工作。同样,开发者也可以根据自己的需要,重载这些方法,来完成额外的配置任务。

综上所述,整体设计既满足了简化开发任务的需求,又满足了提供必要扩展的需要。具体代码这里就不贴了,请直接下载本文的附件,其中包含完整的代码。接下来,我们来了解一下基于该服务宿主框架的几个常用开发模式。

使用

这里介绍几种不同的应用场景下使用我们的服务宿主框架的方法,供大家参考。

基本用法

下面的代码就是最简单的使用方式,可以看到,与上面的代码相比,我们已经可以使用日志来输出信息了,并且更重要的是,应用程序的配置信息都可以放在appsettings.json文件中,不仅如此,宿主程序的运行环境配置在hostsettings.json文件中,还可以根据当前的宿主环境来选择不同的配置文件。这些行为已经跟ASP.NET Core的执行行为非常相似了。更有趣的是,ServiceRunner的ConfigureAppConfiguration方法中默认加入了通过环境变量以及命令行的方式来实现程序的配置,因此,开发出来的服务宿主程序可以很方便地集成在容器环境中。


class MyService : ServiceHost
{
    private readonly ILogger logger;
    public MyService(ILogger<MyService> logger, IApplicationLifetime applicationLifetime) : base(applicationLifetime)
        => this.logger = logger;
    public override Task StartAsync(CancellationToken cancellationToken)
    {
        this.logger.LogInformation("MyService started.");
        return Task.CompletedTask;
    }
    public override Task StopAsync(CancellationToken cancellationToken)
    {
        this.logger.LogInformation("MyService stopped.");
        return Task.CompletedTask;
    }
}
class Program
{
    static async Task Main(string[] args)
    {
        var serviceRunner = new ServiceRunner<MyService>();
        await serviceRunner.RunAsync(args);
    }
}

代码执行效果如下:

640?wx_fmt=png

合理终止无限循环的服务端任务

另一个使用场景,就是当ServiceHost启动的时候,会启动一个后台任务,不停地执行一些处理逻辑,直到用户按下CTRL+C,才会停止这个重复执行的任务并正常终止程序。使用上面的服务宿主框架也很容易实现:


class MyService : ServiceHost
{
    private readonly ILogger logger;
    private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    private readonly List<Task> tasks = new List<Task>();
    public MyService(ILogger<MyService> logger, IApplicationLifetime applicationLifetime) : base(applicationLifetime)
    {
        this.logger = logger;
    }
    public override Task StartAsync(CancellationToken cancellationToken)
    {
        var task = Task.Run(async () =>
        {
            while (!cancellationTokenSource.IsCancellationRequested)
            {
                logger.LogInformation($"Task executing at {DateTime.Now}");
                await Task.Delay(1000);
            }
        });
        tasks.Add(task);
        return Task.CompletedTask;
    }
    public override Task StopAsync(CancellationToken cancellationToken)
    {
        Task.WaitAll(tasks.ToArray(), 5000);
        logger.LogInformation("Host stopped.");
        return Task.CompletedTask;
    }
    protected override void Dispose(bool disposing)
    {
        logger.LogInformation("Host disposed.");
        base.Dispose(disposing);
    }
    protected override void OnHostStopping()
    {
        logger.LogInformation("Host stopping requested.");
        this.cancellationTokenSource.Cancel();
    }
}
class Program
{
    static async Task Main(string[] args)
    {
        var serviceRunner = new ServiceRunner<MyService>();
        await serviceRunner.RunAsync(args);
    }
}

主要思路就是在MyService中定义一个CancellationTokenSource,在OnHostStopping的回调函数中,调用Cancel方法触发取消事件,然后在任务的运行体中判断是否已经发起了“取消”请求。执行结果如下:

640?wx_fmt=png

Serilog的集成与使用

我们还可以非常方便地在我们的服务宿主程序中使用Serilog,以实现强大的日志功能,代码如下:


class MyService : ServiceHost
{
   private readonly Microsoft.Extensions.Logging.ILogger logger;
  public MyService(ILogger<MyService> logger, IApplicationLifetime applicationLifetime) : base(applicationLifetime)
        => this.logger = logger;
    public override Task StartAsync(CancellationToken cancellationToken)
    {
        this.logger.LogInformation("MyService started.");
        return Task.CompletedTask;
    }
    public override Task StopAsync(CancellationToken cancellationToken)
    {
        this.logger.LogInformation("MyService stopped.");
        return Task.CompletedTask;
    }
}
class SerilogSampleRunner : ServiceRunner<MyService>
{
    protected override void ConfigureLogging(HostBuilderContext context, ILoggingBuilder logging)
    {
        // Leave this method blank to remove any logging configuration from base implementation.
    }
    protected override IHostBuilder ConfigureAdditionalFeatures(IHostBuilder hostBuilder)
    {
        return hostBuilder.UseSerilog((hostBuilderConfig, loggerConfig) =>
        {
            loggerConfig.ReadFrom.Configuration(hostBuilderConfig.Configuration);
        });
    }
}
class Program
{
    static async Task Main(string[] args)
    {
        var serviceRunner = new SerilogSampleRunner();
        await serviceRunner.RunAsync(args);
    }
}

执行上面的代码,可以看到,输出日志的格式发生了变化:

640?wx_fmt=png

Serilog有很多插件,可以很方便地将日志输出到各种不同的载体,比如文件、数据库、Azure托管的消息总线等等,有兴趣的读者可以上Serilog的官方网站了解,这里就不详细介绍了。

总结

本文介绍了基于.NET Core通用主机(Generic Host)的服务宿主框架的设计与实现,并给出了三个应用场景的案例代码,详细代码可以点击文后的下载链接进行下载。有关.NET Core Generic Host以及本文介绍的框架,还有很多高级功能和特殊用法,有需要的读者可以在本文留言,共同探讨。


原文地址:http://sunnycoding.cn/2019/01/29/implementing-generic-purpose-service-hosting-framework-2/

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com
640?wx_fmt=jpeg


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/317430.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CF1131 G. Most Dangerous Shark(DP+单调栈优化)

文章目录problemsolutioncodeproblem solution dpi:dp_i:dpi​: 前iii个多米诺骨牌全都倒下的最小花费 li,ril_i,r_ili​,ri​分别表示第iii个多米诺骨牌倒下时所能波及到的最左/右位置 往左倒&#xff0c;则[li,i)[l_i,i)[li​,i)内的牌都可以选择性地先推倒 dpimin⁡{dpjcos…

Cat Virus

Cat Virus 题意&#xff1a; 让你构造一颗树&#xff0c;要求如果一个点为黑&#xff0c;其子树全为黑&#xff0c;白点任意&#xff0c;现在让你构造一棵树&#xff0c;使其染色方案数为K&#xff0c;节点尽可能少 题解&#xff1a; 首先画出k<9的全部情况&#xff0c;并…

微软发布XAML Studio工具:快速构建UWP XAML原型

IT之家1月30日消息 微软车库的最新项目XAML Studio已经在Windows 10应用商店上架&#xff0c;将帮助开发人员快速构建UWP XAML原型&#xff0c;以后可以轻松地将其复制到Visual Studio中。它将允许开发人员实时预览他们的XAML代码&#xff0c;并与结果进行交互&#xff0c;就像…

.NET Core 3 Preview 2发布,C#8更强大的模式匹配

.NET Core 3 Preview 2 发布了&#xff0c;此版本主要带来了 C# 8 相关的新功能&#xff0c;C# 8 Preview 2 是 .NET Core 3 SDK 的一部分。C# 8 中使用模式进行更多操作&#xff0c;主要特性包括&#xff1a;using 声明改变需要缩进代码的方式&#xff0c;现在可以编写以下代码…

CF407 E. k-d-sequence(线段树+单调栈)

文章目录CF407 E. k-d-sequenceproblemsolutioncodeCF407 E. k-d-sequence problem solution special case&#xff0c;d0d0d0&#xff0c;相当于寻找最长的一段数字相同的区间 other case&#xff0c;如果要满足公差为ddd等差序列 区间内每个数在模ddd意义下同余每个数互不…

D. Binary Literature

D. Binary Literature 题意&#xff1a; 给三个长度为2 * n的01串&#xff0c;让你构造一个长度小于3 * n的字符串&#xff0c;使得这个串至少包含两个01串 题解&#xff1a; 很巧妙的构造题 三个指针分别指向三个串&#xff0c;因为是01串&#xff0c;所以一定存在两个字符…

安逸:鼠绘《诗与远方》

【作品名称】《诗与远方》【作者介绍】徐安&#xff08;笔名安逸&#xff0c;常州&#xff09;&#xff0c;PPT专家&#xff0c;鼠绘专家。平面设计专业&#xff0c;6年PPT设计经验&#xff1b;历届江苏省PPT制作大赛一等奖获得者&#xff0c;PA口袋动画重要合作人。PPT动画制作…

[HNOI2016]网络(树链剖分+线段树+大根堆)

[HNOI2016]网络 problem solution 另辟蹊径&#xff0c;不把交互请求赋在新增路径上&#xff0c;反而把交互请求赋在树上除去该请求路径覆盖点的其它点上 显然&#xff0c;路径问题树剖是非常可以的、 那么一个点上的信息就表示所有不经过该点的交互请求&#xff0c;用堆…

IdentityServer4实战 - JWT Token Issuer 详解

一.前言本文为系列补坑之作&#xff0c;拖了许久决定先把坑填完。下文演示所用代码采用的 IdentityServer4 版本为 2.3.0&#xff0c;由于时间推移可能以后的版本会有一些改动&#xff0c;请参考查看&#xff0c;文末附上Demo代码。本文所诉Token如无特殊说明皆为 JWT Token。众…

P3834 【模板】可持久化线段树 2(整体二分做法)

P3834 【模板】可持久化线段树 2&#xff08;主席树&#xff09; 我们详细讲讲这个整体二分如何求区间第k小 我们都知道二分可以求出区间里某个想要的值&#xff0c;如果有很多询问&#xff0c;我们对每个询问都进行二分&#xff0c;复杂度就是O(QNlog(1e9))铁超&#xff0c;那…

胡浩:人人能学的AI《从零开始机器学习》苏州.NET俱乐部课程分享

【课程名称】《从零开始机器学XI》【老师介绍】胡浩&#xff0c;微软最有价值专家&#xff08;MVP&#xff0c;十余届多方向&#xff09;&#xff0c;微软技术大会讲师。云、数据中心基础架构、全栈虚拟化、企业移动管理等领域的架构师及顾问。AI/ML等新技术的爱好者&#xff0…

IdentityServer4实战 - 与API单项目整合

一.前言我们在实际使用 IdentityServer4 的时候&#xff0c;可能会在使用 IdentityServer4 项目添加一些API&#xff0c;比如 找回密码、用户注册、修改用户资料等&#xff0c;这些API与IdentityServer4怎么共存在一个项目呢&#xff1f;二.整合1.首先在 Startup.cs 中添加 Ide…

[HDU 6157]The Karting(DP)

[HDU 6157]The Karting description solution 先用前缀和求出di:1→id_i:1\rightarrow idi​:1→i 的距离 前缀和满足&#xff1a;若在iii点进行方向改变&#xff0c;则iii产生的贡献是一定的&#xff0c;可以先累计贡献 也就是说真正的路径怎么走&#xff0c;我们是不关心…

.NET 开源简史

现在在微软开发开源软件是很一件正常的事情——但在 2007 年&#xff0c;当时我刚加入微软&#xff0c;那时候可不是这么一回事。微软花了好几年时间才找到正确的方向&#xff0c;让微软这艘大船顺着开源之风向前航行。现在回头远望过去那些曾经面临的挑战&#xff0c;我们一笑…

P2617 Dynamic Rankings(整体二分)

P2617 Dynamic Rankings 题意: 待修改的区间最值问题 题解&#xff1a; 整体二分天然带有修改性 整体二分做不带修改的区间最值—>看这里 现在待修改&#xff0c;我们可以将第l位修改为x&#xff0c;因为我们是用树状数组来维护的&#xff0c;所以把这个过程拆分成将第l个…

[LOJ 6042]「雅礼集训 2017 Day7」跳蚤王国的宰相(树的重心+贪心)

[LOJ 6042]「雅礼集训 2017 Day7」跳蚤王国的宰相 description solution 一个到所有节点距离和最小的节点 ⇔\Leftrightarrow⇔ 树的重心&#xff08;满足最重的儿子最轻&#xff0c;每个儿子siz≤n2\le\frac{n}{2}≤2n​&#xff09; 显然原树的重心答案为0 对于点iii&am…

由优劣语言之争引起的思考

由优劣语言之争引起的思考#欲使其灭亡&#xff0c;必使其疯狂昨天上午由阿里云中间件公众号和架构师小秘圈公众号发布的一篇文章《天天敲代码会使人变聪明么》在.net开发者中掀起了一阵巨浪&#xff0c;文章中提到的语言的先进与落后之争让基于.net开发者们义愤填膺&#xff0c…

[LOJ #521]「LibreOJ β Round #3」绯色 IOI(抵达)(结论)

#521. 「LibreOJ β Round #3」绯色 IOI&#xff08;抵达&#xff09; description solution 因为点的庇护所不能为自身&#xff0c;题目背景在树上&#xff0c;有结论一定是两个相邻点互为庇护所 所以树一定要能两两完美匹配才有解 判断完有解后就是构造解了&#xff0c;…

微软是如何使用 C# 重写 C# 编译器并将其开源的

Roslyn 是 C# 和 Visual Basic.NET 开源编译器的代号。这篇文章将介绍它是如何从微软过去的十年至暗时刻走出来&#xff0c;成为开源跨平台的 C# 和 VB 公共语言引擎。我于 2005 年加入微软&#xff0c;也就是在.NET 2.0 发布之前&#xff0c;当时微软内部已经开始在讨论 Rosly…

P3332 [ZJOI2013]K大数查询(整体二分做法)

P3332 [ZJOI2013]K大数查询 题意: 题解&#xff1a; 利用整体二分来做&#xff0c;这个题和P3834 【模板】可持久化线段树 2的区别在于本题的修改是区间修改&#xff0c;所以将里面的树状数组改成线段树就行&#xff0c;区间修改区间查询 但是不知道为什么我调了一阵子也不对…