在.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;并…

NOIP2021:游记

前言 似乎前一秒还在为接下来的考试紧张&#xff0c;下一秒就已经走出了考场 恍惚之间突然意识到&#xff0c;有些日子&#xff0c;可能真的变成了过往 停止emo 感谢FFC&#xff0c;感谢大连&#xff0c;使这次考试顺利进行 主要的问题是T4的暴力 写的时候只有不到一个点&…

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

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

P5113-Sabbat of the witch【分块,基数排序】

正题 题目链接:https://www.luogu.com.cn/problem/P5113 题目大意 一个长度为nnn的序列aaa&#xff0c;mmm次要求支持以下操作 将区间[l,r][l,r][l,r]都变为xxx。询问区间[l,r][l,r][l,r]的和。将第xxx次操作111撤销。 强制在线 1≤n,m≤105,1≤ai,x≤1091\leq n,m\leq 10^…

C. The Sports Festival

C. The Sports Festival 题意&#xff1a; n个数&#xff0c;依次将所有数加入到区间内&#xff0c;每次得到一个k&#xff0c;k等于当前区间的最大值减最小值&#xff0c; 求所有k的和的最小值 题解&#xff1a; 一开始就没往dp那方面想&#xff0c;自己在dp这方面的理解还…

模板:点分治点分树

文章目录前言点分治背景解析代码点分树情境代码thanks for reading!所谓点分治&#xff0c;就是把所有的点分开来治 &#xff08;逃&#xff09; &#xff08;应广大观众要求&#xff0c;开篇废话改回原风格qwq&#xff09; 前言 很神奇的算法。 没有引入任何新的知识&#x…

.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;现在可以编写以下代码…

Loj#2474-「2018 集训队互测 Day 3」北校门外的未来【LCT】

正题 题目链接:https://loj.ac/p/2474 题目大意 开始有一个只有点111的图&#xff0c;一个点xxx能走到点yyy当且仅当路径(x,y)(x,y)(x,y)之间&#xff08;不包括x,yx,yx,y&#xff09;不存在编号比xxx或yyy要大的节点。有mmm次操作&#xff1a; 新建一个编号为yyy的节点和xx…

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动画制作…

洛谷P4292:重建计划(点分治、单调队列)

解析 第一眼&#xff1a;Wow这么水的黑&#xff1f;&#xff1f; 然后写了一发二分套线段树的3log代码上去 T到飞起&#xff0c;只有40… 无奈瞅了一眼标签&#xff1a;单调队列 对啊 于是又写了一个上去 20 … 好啊 然后就摆烂了 qwq 果然黑题没有一个好东西 一个关键的思…

E. Colorings and Dominoes(未解决)

E. Colorings and Dominoes 题意&#xff1a; n * m的格子&#xff0c;分为黑白格子&#xff0c;白格子可以染成蓝色或者红色&#xff0c;一个1 * 2的多米诺骨牌&#xff0c;可以覆盖两个连续的水平的红色格子或者两个连续的竖着的蓝色格子&#xff0c;对于一个染色方案&…

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

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

P4557-[JSOI2018]战争【凸包,闵可夫斯基和】

正题 题目连接:https://www.luogu.com.cn/problem/P4557 题目大意 给出两个点集A,BA,BA,B&#xff0c;qqq次询问给出一个向量vvv&#xff0c;询问将BBB中所有点加上向量vvv后两个集合的凸包是否有交。 1≤n,m,q≤1051\leq n,m,q\leq 10^51≤n,m,q≤105 解题思路 闵可夫斯基和…

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;那…

YBTOJ洛谷P3292:幸运数字(线性基、点分治/倍增)

解析 虽然使用三个log的倍增算法艹过去了 但是我们还是来聊聊正解吧 考虑点分治 对于当前的根&#xff0c;dfs求出联通块内每个点到当前根的线性基 一条路径的答案应该在路径出现上第一个成为根的点时统计到 具体来说&#xff0c;就是路径的两端点在同一个solve函数的不同子树…

AGC053E-More Peaks More Fun【计数】

正题 题目链接:https://atcoder.jp/contests/agc053/tasks/agc053_e 题目大意 给出nnn个二元组(ai,bi)(a_i,b_i)(ai​,bi​)&#xff0c;求有多少个1∼n1\sim n1∼n的排列ppp满足&#xff1a; 你可以将一些(ai,bi)(a_i,b_i)(ai​,bi​)变为(bi,ai)(b_i,a_i)(bi​,ai​)定义序…