在之前一篇博客《以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(5).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. 發布程序
2,NSSM配置
1. 安裝服務


其它一些操作
其實不用我說大家也應該知道了:
- 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年的最后一篇博客,如果是,上面這段文字就算是我對今年自己的主要工作總結吧。