在之前一篇博客《以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配置文件,内容是

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
throwExceptions="false"
internalLogLevel="Off">
<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}"/>
<targets>
<target name="asyncFile" xsi:type="AsyncWrapper">
<target name="logfile" xsi:type="File" fileName="${basedir}/log/${shortdate}.log" layout="${theLayout}" encoding="UTF-8" />
</target>
<target name="debugger" xsi:type="Debugger" layout="${theLayout}" />
<target name="console" xsi:type="Console" layout="${theLayout}" />
<target name="void" xsi:type="Null" formatMessage="false" />
</targets>
<rules>
<logger name="Quartz.*" minlevel="Trace" maxlevel="Info" writeTo="void" final="true" />
<logger name="*" minlevel="Debug" writeTo="asyncFile" />
<logger name="*" minlevel="Trace" writeTo="debugger"/>
<logger name="*" minlevel="Trace" writeTo="console"/>
</rules>
</nlog>

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

5. 增加MyServiceJobFactory.cs

using Quartz;
using Quartz.Spi;
using System;
namespace MyService {
class MyServiceJobFactory : IJobFactory {
protected readonly IServiceProvider _container;
public MyServiceJobFactory(IServiceProvider container) {
_container = container;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) {
return _container.GetService(bundle.JobDetail.JobType) as IJob;
}
public void ReturnJob(IJob job) {
}
}
}

6. 增加PeriodLoggingJob.cs

using Microsoft.Extensions.Logging;
using Quartz;
using System;
using System.Threading.Tasks;
namespace MyService {
class PeriodLoggingJob : IJob {
private readonly ILogger<PeriodLoggingJob> _logger;
public PeriodLoggingJob(ILogger<PeriodLoggingJob> logger, IServiceProvider serviceProvider) {
_logger = logger;
}
private void DoLoggingJob() {
_logger.LogInformation("logging...");
}
public Task Execute(IJobExecutionContext context) {
try {
DoLoggingJob();
}
catch (Exception ex) { //必须妥善处理好定时任务中发生的异常
_logger.LogError(ex, "执行定时任务发生意外错误");
}
returnTask.CompletedTask;
}
}
}

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

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using Quartz;
using Quartz.Impl;
using Quartz.Spi;
using System;
using System.Collections.Specialized;
using System.IO;
using System.Threading;
namespace MyService {
class Program {
//注册各种服务
static void RegisterServices(IServiceCollection services) {
//日志相关
services.AddSingleton<ILoggerFactory, LoggerFactory>();
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace));
//定时任务相关
services.AddSingleton<IJobFactory, MyServiceJobFactory>();
services.AddSingleton<PeriodLoggingJob>();
}
static void Main(string[] args) {
//注册退出事件处理(响应<Ctrl>+<C>)
ManualResetEvent exitEvent = new ManualResetEvent(false);
Console.CancelKeyPress += delegate (object sender, ConsoleCancelEventArgs e) {
e.Cancel = true;
exitEvent.Set();
};
//处理其它程序关闭事件(如kill),使得程序可以优雅地关闭
AppDomain.CurrentDomain.ProcessExit += (sender, e) => { exitEvent.Set(); };
//容器生成
ServiceCollection services = new ServiceCollection();
RegisterServices(services);
using (ServiceProvider container = services.BuildServiceProvider()) {
//日志初始化
var loggerFactory = container.GetRequiredService<ILoggerFactory>();
loggerFactory.AddNLog(new NLogProviderOptions {
CaptureMessageTemplates = true,
CaptureMessageProperties = true
});
string nlogConfigFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "NLog.config");
NLog.LogManager.LoadConfiguration(nlogConfigFile);
//记录启动日志
ILogger<Program> logger = container.GetService<ILogger<Program>>();
logger.LogInformation("MyService启动.");
//定时任务配置
NameValueCollection props = new NameValueCollection { { "quartz.serializer.type", "binary" } };
StdSchedulerFactory schedulerFactory = new StdSchedulerFactory(props);
IScheduler scheduler = schedulerFactory.GetScheduler().Result;
scheduler.JobFactory = container.GetService<IJobFactory>(); //每天1:00执行APP状态更新任务
ITrigger periodLoggingJobTrigger = TriggerBuilder.Create().WithIdentity("PeriodLoggingJobTrigger")
.StartNow().WithSimpleSchedule(x=>x.WithIntervalInSeconds().RepeatForever()).Build();
IJobDetail checkPasswordOutOfDateJob = JobBuilder.Create<PeriodLoggingJob>().WithIdentity("PeriodLoggingJob").Build();
scheduler.ScheduleJob(checkPasswordOutOfDateJob, periodLoggingJobTrigger); //开启定时服务
scheduler.Start();
//----------------------------------------↑↑↑ 程序开始 ↑↑↑----------------------------------------
exitEvent.WaitOne();
//----------------------------------------↓↓↓ 程序结束 ↓↓↓----------------------------------------
//定时任务结束
scheduler.Shutdown();
//记录结束日志
logger.LogInformation("MyService停止.");
}
}
}
}

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

8. 试运行程序

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

19:03:37.117 [Info][MyService.Program] (d:\work\MyService\MyService\Program.cs:55) MyService启动.
19:03:37.637 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging...
19:03:42.536 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging...
19:03:47.535 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging...
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. 解决Ubuntu系统下的VMware Workstation无法打开虚拟网络编辑器界面的问题

    本文由荒原之梦原创,原文链接:http://zhaokaifeng.com/?p=630 操作环境: Ubuntu 17 VMware 14 pro for Linux 问题描述: 我在Ubuntu ...

  2. SQL—对数据表内容的基本操作

    数据表  students      id name sex age address 101 张汉 男 14 杭州 102 欧阳钦 男 13 杭州 103 吴昊 男 14 北京 104 钱进进 男 1 ...

  3. JS(总结)

    基础 Javascript是一种弱类型语言,它分别有什么优点和缺点 弱类型语言:简单好用,更灵活多变.但是会牺牲性能,比如一些隐含的类型转换 强类型语言:类型转换的时候非常严格,,强类型语言是直接操纵 ...

  4. Centos7 编译安装Nginx 教程

    相信经过上篇博文的学习,聪明的你已经学会了如何在Centos7 上通过yum 方式安装Nginx ,但是有时候有些场景或者特俗情况下,我们往往需要通过编译源码方式安装,以便于更灵活地定制我们的Ngin ...

  5. Java 读书笔记 (六) 引用类型

    Java里使用long类型的数据要在数值后面加上L,否则会作为整型解析. 引用类型 引用类型是一个对象类型,它的值是指向内存空间的引用,就是地址, 所指向的内存中保存着变量所表示的一个值或一组值. i ...

  6. composer的安装方法

    网上说的方法几乎都不正确,经作者总结,终于知道怎么使用composer的方法.第一,从http://docs.phpcomposer.com/下载安装包:composer.phar 第二,把安装包放在 ...

  7. go语言nsq源码解读八 http.go、http_server.go

    这篇讲另两个文件http.go.http_server.go,这两个文件和第六讲go语言nsq源码解读六 tcp.go.tcp_server.go里的两个文件是相对应的.那两个文件用于处理tcp请求, ...

  8. BZOJ_1342_[Baltic2007]Sound静音问题_单调队列

    BZOJ_1342_[Baltic2007]Sound静音问题_单调队列 题意: 给出n个数,求∑[ max{a[i]~a[i+m-1]} - min{a[i]~a[i+m-1]} <= c ] ...

  9. python 安装cv2

    问题描述:import cv2 报错提示未安装此包. 解决措施: 1.cmd框中输入pip install cv2,若安装成功,则恭喜你一次性成功,如提示"无法找到与你当前版本的匹配&quo ...

  10. 基于promtheus的监控解决方案

    一.前言 鄙人就职于某安全公司,团队的定位是研发安全产品云汇聚平台,为用户提供弹性伸缩的云安全能力.前段时间产品组提出了一个监控需求,大致要求:平台对vm实行动态实时监控,输出相应图表界面,并提供警报 ...