在之前一篇博客《以Windows服务方式运行ASP.NET Core程序》中我讲述了如何把ASP.NET Core程序作为Windows服务运行的方法,而今,我们又遇到了新的问题,那就是:我们的控制台程序,也就是普通的.NET Core程序(而不是ASP.NET Core程序)如何以服务的方式运行呢?

这个问题我们在.NET Core之前早就遇到过,那是是.NET Framework的时代(其实距今也没多远啦),我们是用一个第三方的组件——Topshelf,来解决这个问题的,Topshelf的官网是:http://topshelf-project.com/,它的使用很简单,官网上有具体的描述,对于一个普通的控制台程序而言(通常是一个不需要图形界面的服务),开发和调试的时候,把它当做一个普通的控制台程序来使用,十分方便;而实际部署的时候,通过传入不同的命令行参数,可以使它有了新的行为:安装Windows服务、运行Windows服务、停止/重启Windows服务或者卸载Windows服务。进入跨平台的.NET Core时代之后,Topshelf自然有了支持.NET Core的版本,使用方法与之前的类似,具体在此不表了,因为接下来我们根本不打算使用它!

现在我想要的是:不要引入任何组件,不要对现在控制台程序进行任何修改(ASP.NET Core程序也是控制台程序),开发调试时候不要进行任何复杂的参数配置,一切照旧,仅仅是在部署阶段,把程序当做Windows服务去运行。——你嘚讲吼不吼?

要达到这个目标,就要借助一个神器了,此神器为NSSM,Non-Sucking Service Manager,名字有点拗口,翻译成中文就是:不嗝屁服务管理器。

NSSM的官网是:https://nssm.cc/,十分简陋,但程序功能可是非常强大和全面的,下面我来一步步演示它如何使用。

1,先构建一个简单的服务程序

构建一个简单的服务程序,程序功能描述:程序没有图形界面,仅仅是定时记录一些日志(5秒钟写一下日志),在用户按下<Ctrl>+<C>的时候,程序退出。功能明确,Okay,let's get down to work.

1. 创建一个.NET Core Application,叫MyService

2. Nuget引入Quartz和NLog.Extensions.Logging,一个用来做定时任务,另一个用来log

3. 另外,程序使用了依赖注入,还需要用Nuget引入Microsoft.Extensions.DependencyInjection

4. 给项目增加NLog.Config配置文件,内容是

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. autoReload="true"
  5. throwExceptions="false"
  6. internalLogLevel="Off">
  7. <variable name="theLayout" value="${date:format=HH\:mm\:ss.fff} [${level}][${logger}] ${callsite:className=False:fileName=True:methodName=False} ${message} ${onexception:${newline}}${exception:format=Message,ShortType,StackTrace:innerFormat=Message,ShortType,StackTrace:separator=\r\n:innerExceptionSeparator=\r\n---Inner---\r\n:maxInnerExceptionLevel=5}"/>
  8. <targets>
  9. <target name="asyncFile" xsi:type="AsyncWrapper">
  10. <target name="logfile" xsi:type="File" fileName="${basedir}/log/${shortdate}.log" layout="${theLayout}" encoding="UTF-8" />
  11. </target>
  12. <target name="debugger" xsi:type="Debugger" layout="${theLayout}" />
  13. <target name="console" xsi:type="Console" layout="${theLayout}" />
  14. <target name="void" xsi:type="Null" formatMessage="false" />
  15. </targets>
  16. <rules>
  17. <logger name="Quartz.*" minlevel="Trace" maxlevel="Info" writeTo="void" final="true" />
  18. <logger name="*" minlevel="Debug" writeTo="asyncFile" />
  19. <logger name="*" minlevel="Trace" writeTo="debugger"/>
  20. <logger name="*" minlevel="Trace" writeTo="console"/>
  21. </rules>
  22. </nlog>

还要注意的是这个文件必须复制到生成目录去以便程序运行时候能够加载到。

5. 增加MyServiceJobFactory.cs

  1. using Quartz;
  2. using Quartz.Spi;
  3. using System;
  4. namespace MyService {
  5. class MyServiceJobFactory : IJobFactory {
  6. protected readonly IServiceProvider _container;
  7. public MyServiceJobFactory(IServiceProvider container) {
  8. _container = container;
  9. }
  10. public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) {
  11. return _container.GetService(bundle.JobDetail.JobType) as IJob;
  12. }
  13. public void ReturnJob(IJob job) {
  14. }
  15. }
  16. }

6. 增加PeriodLoggingJob.cs

  1. using Microsoft.Extensions.Logging;
  2. using Quartz;
  3. using System;
  4. using System.Threading.Tasks;
  5. namespace MyService {
  6. class PeriodLoggingJob : IJob {
  7. private readonly ILogger<PeriodLoggingJob> _logger;
  8. public PeriodLoggingJob(ILogger<PeriodLoggingJob> logger, IServiceProvider serviceProvider) {
  9. _logger = logger;
  10. }
  11. private void DoLoggingJob() {
  12. _logger.LogInformation("logging...");
  13. }
  14. public Task Execute(IJobExecutionContext context) {
  15. try {
  16. DoLoggingJob();
  17. }
  18. catch (Exception ex) { //必须妥善处理好定时任务中发生的异常
  19. _logger.LogError(ex, "执行定时任务发生意外错误");
  20. }
  21. returnTask.CompletedTask;
  22. }
  23. }
  24. }

7. Program.cs的完整内容如下

  1. using Microsoft.Extensions.DependencyInjection;
  2. using Microsoft.Extensions.Logging;
  3. using NLog.Extensions.Logging;
  4. using Quartz;
  5. using Quartz.Impl;
  6. using Quartz.Spi;
  7. using System;
  8. using System.Collections.Specialized;
  9. using System.IO;
  10. using System.Threading;
  11. namespace MyService {
  12. class Program {
  13. //注册各种服务
  14. static void RegisterServices(IServiceCollection services) {
  15. //日志相关
  16. services.AddSingleton<ILoggerFactory, LoggerFactory>();
  17. services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
  18. services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace));
  19. //定时任务相关
  20. services.AddSingleton<IJobFactory, MyServiceJobFactory>();
  21. services.AddSingleton<PeriodLoggingJob>();
  22. }
  23. static void Main(string[] args) {
  24. //注册退出事件处理(响应<Ctrl>+<C>)
  25. ManualResetEvent exitEvent = new ManualResetEvent(false);
  26. Console.CancelKeyPress += delegate (object sender, ConsoleCancelEventArgs e) {
  27. e.Cancel = true;
  28. exitEvent.Set();
  29. };
  30. //处理其它程序关闭事件(如kill),使得程序可以优雅地关闭
  31. AppDomain.CurrentDomain.ProcessExit += (sender, e) => { exitEvent.Set(); };
  32. //容器生成
  33. ServiceCollection services = new ServiceCollection();
  34. RegisterServices(services);
  35. using (ServiceProvider container = services.BuildServiceProvider()) {
  36. //日志初始化
  37. var loggerFactory = container.GetRequiredService<ILoggerFactory>();
  38. loggerFactory.AddNLog(new NLogProviderOptions {
  39. CaptureMessageTemplates = true,
  40. CaptureMessageProperties = true
  41. });
  42. string nlogConfigFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "NLog.config");
  43. NLog.LogManager.LoadConfiguration(nlogConfigFile);
  44. //记录启动日志
  45. ILogger<Program> logger = container.GetService<ILogger<Program>>();
  46. logger.LogInformation("MyService启动.");
  47. //定时任务配置
  48. NameValueCollection props = new NameValueCollection { { "quartz.serializer.type", "binary" } };
  49. StdSchedulerFactory schedulerFactory = new StdSchedulerFactory(props);
  50. IScheduler scheduler = schedulerFactory.GetScheduler().Result;
  51. scheduler.JobFactory = container.GetService<IJobFactory>();
  52.  
  53. //每天1:00执行APP状态更新任务
  54. ITrigger periodLoggingJobTrigger = TriggerBuilder.Create().WithIdentity("PeriodLoggingJobTrigger")
  55. .StartNow().WithSimpleSchedule(x=>x.WithIntervalInSeconds().RepeatForever()).Build();
  56. IJobDetail checkPasswordOutOfDateJob = JobBuilder.Create<PeriodLoggingJob>().WithIdentity("PeriodLoggingJob").Build();
  57. scheduler.ScheduleJob(checkPasswordOutOfDateJob, periodLoggingJobTrigger);
  58.  
  59. //开启定时服务
  60. scheduler.Start();
  61. //----------------------------------------↑↑↑ 程序开始 ↑↑↑----------------------------------------
  62. exitEvent.WaitOne();
  63. //----------------------------------------↓↓↓ 程序结束 ↓↓↓----------------------------------------
  64. //定时任务结束
  65. scheduler.Shutdown();
  66. //记录结束日志
  67. logger.LogInformation("MyService停止.");
  68. }
  69. }
  70. }
  71. }

这就是整个服务程序的完整内容,本来我可以提供一个更简单的程序,这里啰里啰嗦写了这么一大堆,目的还是让初学者更加清楚.NET Core的程序结构和运行方式。其中内容包括:NLog的使用、Quartz的使用、容器及依赖注入的入门例子、如何处理程序关闭事件等,也许你想问“为什么要引入Quartz,搞这么复杂,弄个Timer不行吗?”当然行,但Quartz更强大,而且更适合给大家演示容器与依赖注入的使用。

8. 试运行程序

运行这个程序,输出几条日志信息后,以<Ctrl>+<C>来结束程序的运行,这样会在程序目录下产生log目录及日志文件,文件的内容大致如下:

  1. 19:03:37.117 [Info][MyService.Program] (d:\work\MyService\MyService\Program.cs:55) MyService启动.
  2. 19:03:37.637 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging...
  3. 19:03:42.536 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging...
  4. 19:03:47.535 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging...
  5. 19:03:49.293 [Info][MyService.Program] (d:\work\MyService\MyService\Program.cs:80) MyService停止.

9. 发布程序

选择publish,在publish的目标目录下产生一堆文件,将这些文件复制到D:\Service\MyService目录下,一会儿我们要用到这个目录。

2,NSSM配置

首先要获取NSSM程序,当然是要到官网下载,版本选择最新版,尽管它声称是pre-release版,但功能杠杠的,没有任何影响,而正式版(非pre-release)则是2014年的了,太旧了。下载下来后找到对应的exe文件,叫nssm.exe。(注意有32位版和64位版的分别)
 
它是个绿色软件,不需要安装,仅此一个exe文件,把这个文件复制到C:\Windows\System32目录下,之后经常要用。
 
在Windows命令行中直接敲nssm,会出现它的帮助提示。

1. 安装服务

>nssm install MyService
出现配置界面(注意,需要管理员权限)
配置选项比较多,这是我的配置,供参考:
 
点“Install service”即将服务安装好了。我们打开Windows服务来查看所安装的服务:
 
 
服务已经安装完毕,一切准备就绪。
 
2. 启动服务
>nssm start MyService

其它一些操作
其实不用我说大家也应该知道了:

  • nssm status MyService 查看服务状态
  • nssm stop MyService 停止服务
  • nssm restart MyService 重启服务
  • nssm edit MyService 重新配置服务的参数
  • nssm remove MyService 删除服务

其余的请自行参考nssm的使用手册。

注意事项:需要用管理员身份来执行上面这些命令,否则会出现访问拒绝的错误。

3,分享一些想法

2018年快过去了,回顾这一年来,我觉得我在公司所做的最大且重要的一件事情就是推动了.NET Core的应用,将能迁移的.NET Framework的程序都迁移至.NET Core了,为什么要这么干?最最主要的原因当然是要跨平台,原先ASP.NET开发的网站,只能运行于Windows平台,它们得依赖于IIS!Windows(作为服务器)本身就是一个非常复杂的系统,有着各种令人眼花缭乱的配置,加上IIS,就更加令人感到困惑,我同意IIS是功能强大的服务器程序,但它真的过于复杂,设计不合理,很难用,让我等菜鸟频频掉到它的坑里爬不出来。IIS并不是一个能够自由选择版本的软件,它的版本通常认为与Windows操作系统绑定,微软官方并不建议安装与Windows操作系统原生版本不一致的IIS,所以现在甚至还有公司继续在用IIS6,而各个版本的IIS的行为却不尽相同,默认IIS并不带安装ASP.NET组件,所以在Windows系统和IIS刚部署好的时候,想直接运行ASP.NET网站居然还不行,要自己去安装ASP.NET的支持,完成后还需要使用一条额外的命令来注册ASP.NET组件,另外还可能遇到稀奇古怪的问题,大多数问题可以通过安装若干个补丁解决(如ASP.NET MVC的路由不起作用导致网站无法访问的问题),而有时则不会那么顺利,你得仔细看看这些补丁是否符合当前操作系统及IIS版本,甚至操作系统的语言版本也会影响你所要安装的补丁。IIS与ASP.NET程序之间的关系也是令人很懵逼,我想让我的ASP.NET程序自始至终运行着就是做不到,尽管应用程序池里似乎有这个选项,我在StackOverflow上针对相关问题进行过讨论,有不少人顶我,但也有人说不行(我猜跟IIS版本还有关系),ASP.NET程序空闲一段时间后便被IIS踢掉——即便你的主机不差内存,你无法肯定IIS一运行你的程序就跟着跑起来,也无法肯定你的程序什么时候在运行,什么时候被踢掉,这是个类似薛定谔的猫的问题,你的ASP.NET程序就通常处于这么一种“叠加态”,你得看一看才知道确切它是否在运行,这一看,才使得程序从“叠加态”坍缩为“生态”或“死态”,且从“死态”转入“生态”还需要耗费好些时间,表现为第一次打开页面时候的长时间卡顿,跟客户演示系统,有时候会很尴尬。我曾经为了让程序不被IIS踢掉,还手工写了一个KeepAlive的小程序,定时去get我的网站的首页,实在奇葩。微软对此的解释是:IIS并不是为long-term程序设计的,你想在IIS里做一个准时的定时服务,那是相当不妥,根本不是为这种事情设计的,所以不好用不能怪我。我承认这当然是一种设计,但ASP.NET网站除了提供网页之外,跑一些后台服务也应该是很正常的吧?没办法,于是我将服务和网站分开,中间用总线沟通,听起来很cool?——其实这是一段悲伤的往事,不过说来话长,以后有机会再提了。.NET Core出现了,ASP.NET Core也和它一起到来,2.0版开始就是一个很完善的版本,我想是时候上了,这是工作量很大的差事,但为了将来更好的发展,我们必须经历这个艰难的爬坡,所幸的是现在一切都已转入正轨,我预想的目的达到了。

.NET Core的一大特点就是程序都可以独立运行,包括ASP.NET Core程序,不再依赖于IIS,我可以根据业务的需要,将系统划分为多个模块,方便开发分工和测试,这些模块甚至不需要部署在同一台主机上,极大提高了灵活性。一般来说,我还是推荐将程序部署至Linux环境,理由依旧是Linux作为服务器操作系统的使用体验远远好于Windows,Windows实在太过复杂了!但也有例外,如果遇到缺乏Linux支持技术的客户的情况,那就把程序部署到他们的Windows主机上吧,无所谓,反正.NET Core是跨平台的。

不知这是不是我2018年的最后一篇博客,如果是,上面这段文字就算是我对今年自己的主要工作总结吧。

以Windows服务方式运行.NET Core程序的更多相关文章

  1. [转帖]以Windows服务方式运行.NET Core程序

    以Windows服务方式运行.NET Core程序 原作者blog:https://www.cnblogs.com/guogangj/p/10093102.html 里面使用了NSSM 工具 但是自己 ...

  2. 连表查询都用Left Join吧 以Windows服务方式运行.NET Core程序 HTTP和HTTPS的区别 ASP.NET SignalR介绍 asp.net—WebApi跨域 asp.net—自定义轻量级ORM C#之23中设计模式

    连表查询都用Left Join吧   最近看同事的代码,SQL连表查询的时候很多时候用的是Inner Join,而我觉得对我们的业务而言,99.9%都应该使用Left Join(还有0.1%我不知道在 ...

  3. [转帖]以Windows服务方式运行ASP.NET Core程序

    以Windows服务方式运行ASP.NET Core程序 原作者blog: https://www.cnblogs.com/guogangj/p/9198031.htmlaspnet的blog 需要持 ...

  4. 以Windows服务方式运行ASP.NET Core程序

    我们对ASP.NET Core的使用已经进行了相当一段时间了,大多数时候,我们的Web程序都是发布到Linux主机上的,当然了,偶尔也有需求要发布到Windows主机上,这样问题就来了,难道直接以控制 ...

  5. 以Windows服务方式运行ASP.NET Core程序【转载】

    我们对ASP.NET Core的使用已经进行了相当一段时间了,大多数时候,我们的Web程序都是发布到Linux主机上的,当然了,偶尔也有需求要发布到Windows主机上,这样问题就来了,难道直接以控制 ...

  6. MongoDB 3.4 安装以 Windows 服务方式运行

    1.首先从https://www.mongodb.com/download-center#community 下载社区版,企业版也是类似. 2.双击运行安装,可自定义安装路径,这里采用默认路径(C:\ ...

  7. 【数据库开发】在Windows上以服务方式运行 MSOPenTech/Redis

    在Windows上以服务方式运行 MSOPenTech/Redis ServiceStack.Redis 使用教程里提到Redis最好还是部署到Linux下去,Windows只是用来做开发环境,现在这 ...

  8. (转)在Windows上以服务方式运行 MSOPenTech/Redis

    ServiceStack.Redis 使用教程里提到Redis最好还是部署到Linux下去,Windows只是用来做开发环境,现在这个命题发生改变了,在Windows上也可以部署生产环境的Redis, ...

  9. 在Windows上以服务方式运行 MSOPenTech/Redis

    ServiceStack.Redis 使用教程里 提到Redis最好还是部署到Linux下去,Windows只是用来做开发环境,现在这个命题发生改变了,在Windows上也可以部署生产环境的 Redi ...

随机推荐

  1. 安装Twisted

    Python看到网络编程,讲到Twisted这个强大的网络框架,很有兴趣,配合 官方文档,打算研究一哈,但是一开始就碰壁了. 安装的时候 pip install Twisted报错了: 提示没有装什么 ...

  2. Jodd

    Jodd = tools + ioc + mvc + db + aop + tx + json + html < 1.7 Mb Jodd is set of Java microframewor ...

  3. Zepto源码(2016)——Zepto模块(核心模块)

    // Zepto.js // (c) 2010-2016 Thomas Fuchs // Zepto.js may be freely distributed under the MIT licens ...

  4. Extjs 上传文件 IE不兼容的问题[提示下载保存]

    我最不喜欢的浏览器的是IE,但无奈很多项目的客户使用的是IE. 在使用Extjs做文件上传时,其他浏览器没有问题,但IE却一个劲提示保存文件,看服务端运行,它其实是运行成功了已经,但客户端的进度条却一 ...

  5. Android监测手指上下左右滑动屏幕

    在开发android程序时,有时会需要监测手指滑动屏幕,当手指朝上下左右不同方向滑动时做出不同的响应,那怎么去实现呢? 利用Android提供的手势监测器就可以很方便的实现,直接上代码(已测试通过) ...

  6. SimpleXML系列函数操作XML

    创建SimpleXML对象 种方法来创建对象,分别是: l  Simplexml_load_file()函数,将指定的文件解析到内存中. l  Simplexml_load_string()函数,将创 ...

  7. 修改LINUX的时区。

    新装的机器(redhat7)有几台时区不对: 百度了之后找到了以下解决方法输入 tz    依次选择Asia China  east China  Yes 1  然后 export TZ 新开对话发现 ...

  8. 【SAP HANA】新建账户和数据库(2)

    开启HANA Studio,进入到User和Role的目录,这两个地方是创建账号和权限的. 新建用户 输入用户名和密码即可. 注意,如果系统里有同名的Catalog(数据库)存在的话,会报错,因为默认 ...

  9. java泛型中使用的排序算法——归并排序及分析

    一.引言 我们知道,java中泛型排序使用归并排序或TimSort.归并排序以O(NlogN)最坏时间运行,下面我们分析归并排序过程及分析证明时间复杂度:也会简述为什么java选择归并排序作为泛型的排 ...

  10. 【SQL】面面俱到 | 在SQL中使用CUBE和ROLLUP实现数据多维汇总

    偶然在网上看到一篇文章,讲到数据汇总,提到了CUBE,感觉有些晦涩,想试着自己表述一下.同时,个人也认为CUBE还是很有用的,对SQL或数据分析感兴趣的小伙伴不妨了解一下,或许有用呢! 先设定个需求, ...