教程 基于通用主机实现后台任务的NET核心
前言
很多时候,后台任务是我们的利器,帮助我们处理身后成千上万的事情。在……的时代。NET Framework,我们可能有不止一个项目,有一个或多个对应的Windows服务,可以看作是我们所说的后台任务。
我喜欢把后台任务分为两类,一类是保持运行,像MQ消费者和RPC服务器。另一种是定期运行,如计划任务。
那么在。NET Core时代?答案是肯定的。
通用主机是方案之一,也是本文的主角。
什么是Generic Host
通用主机是ASP.NET核心2.1中的一个新功能,旨在将HTTP管道与Web Host的API分离,从而启用更多的主机方案。现在Asp.Net核心2.1版有两种主机。
WebHost——一个适合托管Web程序的主机,是CreateWebHostBuilder在Core应用的Mai功能中创建的一个熟悉的WebHost。
通用主机(仅在ASP的核心2.1版本中可用。NET)适合托管非web应用程序(例如,运行后台任务的应用程序)。在未来的版本中,通用主机将适用于托管任何类型的应用程序,包括Web应用程序。通用主机最终将取代Web主机,这可能就是这种类型的主机被称为通用主机的原因。
这样,基于通用主机的一些特性可以用一些基本功能进行扩展。例如配置、依赖注入和日志。
泛型宿主往往是通用的,换句话说,我们可以在web项目和非Web项目中使用它!
虽然有时在Web项目中混合后台任务不是一个好的选择,但它仍然是一个解决方案。尤其是资源不充足的时候。
不如让它独立,让它的职责更加单一。
让我们来看看如何创建背景任务。
后台任务示例
,我们先写两个后台任务(一个一直运行,一个定时运行),体验一下如何上手这些后台任务,后面也会用到。这两个任务继承抽象类BackgroundService,而不是接口IHostedService。我们以后再谈区别。
1.一直运行的后台任务
代码优先
公共类printerhostedservice 2 : background service { private readonly ILogger _ logger;私有只读AppSettings _ settingspublic printerhostedservice 2(ILoggerFactory logger factory,IOptionsSnapshotAppSettings选项){ this。_logger=loggerFactory。createlogerprinterhostedservice 2();这个。_设置=选项。价值;}公共覆盖任务停止异步(取消令牌取消令牌){ _记录器。登录信息(“打印机2已停止”);返回任务。完成任务;}受保护的覆盖异步任务execute async(cancelationtoken stopping ken){ while(!停止肯。iscancelingrequested){ _ logger。登录信息($'Printer2正在工作。{ _设置。PrinterDelaySecond } ');等待任务。延迟(时间跨度。from seconds(_设置。PrinterDelaySecond),stoppingken);}}}看一下里面的细节。
我们的服务继承了BackgroundService,所以我们必须实现ExecuteAsync,像StartAsync和StopAsync这样的方法可以选择性地覆盖。
我们只需在内部输出日志,然后休眠配置文件中指定的秒数。
这个任务可以说是最简单的例子,其中也使用了依赖注入。如果你想在任务中注入数据仓库之类的东西,你应该不需要再多说什么了。
用同样的方法写一个定时的。
定期运行的后台任务
这里用Timer完成定时运行的功能,也可以和Quartz结合使用。
公共类TimerHostedService :后台服务{//其他.私有定时器;受保护的覆盖任务执行异步(cancelatilatitoken stopping ken){ _ Timer=新计时器(DoWork,null,TimeSpan .零,时间跨度从秒开始(_设置. TimerPeriod));返回任务。完成任务;} private void DoWork(对象状态){ _logger .登录信息("计时器正在工作");}公共覆盖任务停止异步(取消令牌取消令牌){ _记录器。登录信息("计时器正在停止");_定时器?更改(超时。无限,0);返回基地StopAsync(取消令牌);}公共覆盖void Dispose() { _timer?dispose();基地dispose();}}和第一个后台任务相比,没有太大的差异。
下面我们先来看看如何用控制台的形式来启动这两个任务。
控制台形式
这里会同时引入股市分析来记录任务跑的日志,方便我们观察。主要的函数的代码如下:
类程序{静态异步任务main(string[]args){ var builder=new host builder()//日志记录配置日志记录(工厂={//使用股市分析工厂添加日志(新NLogProviderOptions { CaptureMessageTemplates=true,CaptureMessageProperties=true });NLog .日志管理器。加载配置(' nlog。config’);})//主机配置ConfigureHostConfiguration(config={//命令行if (args!=null) { config .AddCommandLine(args);} })//应用程序配置.ConfigureAppConfiguration((主机上下文,config)={ var env=hostContext .主机环境;配置AddJsonFile('appsettings.json ',optional: true,reloadOnChange: true).AddJsonFile($'appsettings .{env .环境名称}。json ',optional:为真,重新加载更改:为真);配置AddEnvironmentVariables();if (args!=null) { config .AddCommandLine(args);} })//服务配置服务((主机上下文,服务)={服务.AddOptions();服务.配置参数设置(主机上下文配置。GetSection(' AppSettings ');//基本使用服务AddHostedServicePrinterHostedService2();服务AddHostedServiceTimerHostedService();}) ;//控制台等待生成器runconsolesync();////启动并等待关闭//var host=builder .build();//使用(主机)//{//等待主机.StartAsync();//等待宿主waitforshuttowasync();//} }}对于控制台的方式,需要我们对主机构建器有一定的了解,虽说它和WebHostBuild有相似的地方。可能大部分时候,我们是直接使用了网络主持人.CreateDefaultBuilder(参数)来构造的,如果对CreateDefaultBuilder里面的内容没有了解,那么对上面的代码可能就不会太清晰。
上述代码的大致流程如下:
新的一个主机构建器对象配置日志,主要是接入了股市分析主机的配置,这里主要是引入了命令行,因为需要传递参数给程序应用的配置,指定了配置文件,和引入命令行服务的配置,这个就和我们在启动里面写的差不多了,最主要的是我们的后台服务要在这里注入启动其中,
2-5的顺序可以按个人习惯来写,里面的内容也和我们写启动大同小异。
第6步,启动的时候,有多种方式,这里列出了两种行为等价的方式。
a.通过RunConsoleAsync的方式来启动
b.先StartAsync然后再WaitForShutdownAsync
RunConsoleAsync的奥秘,我觉得还是直接看下面的代码比较容易懂。
///摘要///侦听拷贝或终止信号调用,并查看cref='IApplicationLifetime .停止应用程序,以启动关闭过程。///这将取消阻止像RunAsync和WaitForShutdownAsync这样的扩展////summary////param name=' host builder '要配置的请参见cref=' IHostBuilder '/1 ./param/返回链接的请参见cref='IHostBuilder'/的同一个实例/返回公共静态IHostBuilder UseConsoleLifetime(此IHostBuilder主机生成器){返回主机生成器.配置服务((上下文,集合)=集合. AddSingletonIHostLifetime,console lifetime());}///摘要////启用控制台支持,构建并启动主机,等待拷贝或终止信号关闭////summary////param name=' host builder '要配置的请参见cref=' IHostBuilder '/1 ./param///param name=' canceltintoken '/param///返回/returnspublic静态任务RunConsoleAsync(此为IHostBuilder hostBuilder,canceltintoken canceltintoken=default){返回HostBuilder .UseConsoleLifetime().构建()。RunAsync(取消令牌);}这里涉及到了一个比较重要的主机寿命的生命周期,ConsoleLifeTime是默认的一个,可以理解成当接收到拷贝这样的指令时,它就会触发停止。
接下来,写一下股市分析的配置文件
?可扩展标记语言版本='1.0 '编码='utf-8 '?NLog xmlns=' http://www .NLog-项目。org/schemas/NLog。xsd ' xsi :架构位置=' NLog NLog。xsd ' xmlns : xsi=' http://www .w3。org/2001/XMLSchema-instance ' auto load=' true ' internalLogLevel=' Info ' targets xsi 3360 type=' File ' name=' GHost ' fileName=' logs/GHost。日志'布局=' $ '* '最小级别='信息'写入=' ghost '/日志记录程序名称='Microsoft ' .*“最低级别=‘信息’写入=‘幽灵’/规则/nlog这个时候已经可以通过命令启动我们的应用了。
dotnet运行-环境-分段这里指定了运行环境为分期付款,而不是默认的生产。
在构造主机构建器的时候,可以通过使用环境或配置主机配置直接指定运行环境,但是个人更加倾向于在启动命令中去指定,避免一些不可控因素。
这个时候大致效果如下:
虽然效果已经出来了,不过大家可能会觉得这个有点小打小闹,下面来个略微复杂一点的后台任务,用来监听并消费RabbitMQ的消息。
消费MQ消息的后台任务
公共类comsumrabbtmqhostedservice :后台服务{ private readonly ILogger _ logger;私有只读应用设置_设置私有图标连接_连接私有IModel _ channel public comsumerabbtmqhostedservice(ILoggerFactory日志记录程序工厂,IOptionsSnapshotAppSettings选项){这个._logger=loggerFactory .createlogercomsumerabbtmqhostedservice();这个。_设置=选项。价值;InitRabbitMQ(这个。_设置);} private void initrabbtmq(AppSettings settings){ var factory=new connection factory { HostName=settings .HostName,};_连接=工厂创建连接();_通道=_连接创建模型();_频道ExchangeClare(_设置).ExchangeName,ExchangeType .题目);_频道queuedeclarir(_设置).QueueName,false,false,false,null);_频道QueueBind(_设置).QueueName,_设置ExchangeName,_设置.RoutingKey,null);_频道。基本服务质量(0,1,假);_连接connection shuttle=Rabbtmq _ connection shuttle;}受保护的覆盖任务执行异步(取消停止肯){停止肯.throwifcancelationrequested();var consumer=new eventingbasconconsumer(_ channel);消费者。接收=(ch,ea)={ var content=System .文字。编码。UTF8。GetString(ea .身体);HandleMessage(内容);_频道BasicAck(ea .DeliveryTag,false);};消费者关闭=开启关闭关闭消费者registered=OnConsumerRegistered;消费者. unregistered=OnConsumerUnregistered;消费者consumer canceled=onconsumerconconsumercenceled;_频道。基本消费(_设置QueueName,false,consumer);返回任务。完成任务;} private void HandleMessage(字符串内容){ _logger .登录信息($ '使用者收到{ content } ');} private void onconsumerconsumercanceled(对象发送方,consumer eventargs e){ 0.} private void OnConsumerUnregistered(对象发送方,consumer inventargs e){ 0.} private void onconsumerrregistered(对象发送方,consumer inventargs e){ 0.} private void OnConsumerShutdown(对象发送者,shuttonventargs e){ 0.} private void rabbtmq _ connectionshuttonventargs(对象发送方,shuttonventargs e){ 0.}公共覆盖void Dispose() { _channel .close();_连接close();基地dispose();}}代码细节就不需要多说了,下面就启动(法属)马提尼克岛(马提尼克岛的简写)发送程序来模拟消息的发送
同时看我们任务的日志输出
由启动到停止,效果都是符合我们预期的。
下面再来看看网形式的后台任务是怎么处理的。
Web形式
这种模式下的后台任务,其实就是十分简单的了。我们只要在启动的配置服务方法里面注册我们的几个后台任务就可以了。
public void ConfigureServices(IServiceCollection services){ services .AddMvc().SetCompatibilityVersion(兼容性版本.版本_ 2 _ 1);服务AddHostedServicePrinterHostedService2();服务AddHostedServiceTimerHostedService();服务addhostedservice comsumrabbtmqhostedservice();}启动网站点后,我们发了20条(法属)马提尼克岛(马提尼克岛的简写)消息,再访问了一下网站点的首页,最后是停止站点。
下面是日志结果,都是符合我们的预期。
可能大家会比较好奇,这三个后台任务是怎么混合在网项目里面启动的。
答案就在下面的两个链接里。
https://github.com/aspnet/Hosting/blob/2.1.1/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs
https://github.com/aspnet/Hosting/blob/2.1.1/src/Microsoft.AspNetCore.Hosting/Internal/HostedServiceExecutor.cs
上面说了那么多,都是在本地直接运行的,可能大家会比较关注这个要怎样部署,下面我们就不看看怎么部署。
部署
部署的话,针对不同的情形(网络和非网络)都有不同的选择。正常来说,如果本身就是网程序,那么平时我们怎么部署的,就和平时那样部署即可。
花点时间讲讲部署非网的情形。
其实这里的部署等价于让程序在后台运行。
在Linux下后台运行程序的方式有很多,比如Supervisor、Screen、pm2、systemctl等等。
本文主要介绍systemctl,并使用上面的例子进行部署。因为个人服务器没有MQ环境,消费MQ的后台任务没有启用。
首先创建一个服务文件
vim/etc/systemd/system/ghost demo . service内容如下:
[Unit]Description=Generic Host Demo[Service]working directory=/var/www/ghostextecstart=/usr/bin/dotnet/var/www/ghost/consolehost . dll-environment staging kill sign=sigintsyslogidentifier=ghost-示例[install]wanted by=multi-user . target,每个配置的含义都可以自己找到,这里就不解释了。
然后,您可以使用以下命令启动和停止此服务
服务幽灵演示开始服务幽灵演示停止可设置为测试正确后自动启动。
让我们来看看运行的效果
我们先启动服务,然后查看实时日志。我们可以看到应用程序日志一直在输出。
当我们停止服务并查看实时日志时,我们会发现我们的两个后台任务已经停止,并且没有日志再次进入。
再次查看服务系统日志
sudo journal CTL-fu ghost demo . service
我发现它确实停止了。
在这里,我们还可以看到服务的当前环境和根路径。
IHostedService和BackgroundService的区别
在前面的所有例子中,我们使用了BackgroundService而不是IHostedService。
两者有什么区别?
可以简单理解为IHostedService是原料,BackgroundService是用原料部分加工的半成品。
两者都不能直接作为成品使用,需要经过加工才能制成可用的成品。
同时,这也意味着如果使用IHostedService,可能需要做更多的控制。
基于前面的打印后台任务,这里使用了IHostedService。
如果我们简单地将实现代码放入StartAsync方法中,可能会有惊喜。
公共类PrinterHostedService : IHostedService,IDisposable{ //other.公共异步任务开始异步(取消令牌取消令牌){ while(!cancellationToken。iscancelingrequested){控制台。写线('打印机正在工作');等待任务。延迟(时间跨度。from seconds(_设置。PrinterDelaySecond),cancelationtoken);} }公共任务停止异步(取消令牌取消令牌){控制台。写线(“打印机已停止”);返回任务。完成任务;}}运行后,我用ctrl c尝试停止,发现它还在运行。
当ps看它的时候,这个过程还在,它杀了之后就不继续输出了。
问题在哪里?其实原因很明显,因为这个任务还没有启动成功,而且一直处于启动状态!
换句话说,StartAsync方法还没有完成执行。这个问题必须解决。
如何处理这个问题?解决方案也相对简单。您可以通过引用变量来记录要运行的任务,并从StartAsync方法中释放它。
公共类printerhostedservice 3 : IHostedService,IDisposable{ //others.私有bool _ stopping私有Task _ backgroundTask公共任务开始异步(取消令牌取消令牌){控制台。WriteLine('Printer3正在启动');_ background task=background task(cancelationtoken);返回任务。完成任务;}私有异步任务background Task(cancelationtoken cancelationtoken){ while(!_停止){等待任务。延迟(时间跨度。from seconds(_设置。PrinterDelaySecond),cancelationtoken);控制台。WriteLine('Printer3正在做后台工作');} }公共任务停止异步(取消令牌取消令牌){控制台。WriteLine('Printer3正在停止。);_ stopping=true返回任务。完成任务;} public void Dispose() { Console。写线(打印机3正在处理));}}这将使此任务真正成功开始!效果未显示。
相对来说,BackgroundService用起来会比较简单,实现核心的ExecuteAsync这个抽象方法就差不多了,出错的概率也会比较低。
IHostBuilder的扩展写法
在注册服务的时候,我们还可以通过编写IHostBuilder的扩展方法来完成。公共静态类扩展{公共静态IHostBuilder UseHostedServiceT(此主机构建器)其中T :类,IHostedService,IDisposable { return hostBuilder .配置服务(服务=服务. addhostedservice());}公共静态IHostBuilder UseComsumeRabbitMQ(此IHostBuilder为host builder){ 0返回主机构建器.配置服务(服务=服务. addhostedservice comsumrabbtmqhostedservice());}}使用的时候就可以像下面一样。
var builder=new HostBuilder()//others.ConfigureServices((hostContext,services)={ services .AddOptions();服务.配置参数设置(主机上下文配置。GetSection(' AppSettings ');//基本用法//服务AddHostedServicePrinterHostedService2();//服务AddHostedServiceTimerHostedService();//服务addhostedservice comsumrabbtmqhostedservice();})//扩展用法UseComsumeRabbitMQ().UseHostedServiceTimerHostedService().usehostedservicecomsumrabbtmqhostedservice();
总结
通用主机让我们可以用熟悉的方式来处理后台任务,不得不说这是一个很的特性。无论是将后台任务独立一个项目,还是将其混搭在网项目中,都已经符合不少应用的情景了。
最后放上本文用到的示例代码
GenericHostDemo
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。
版权声明:教程 基于通用主机实现后台任务的NET核心是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。