以Windows服務方式運行.NET Core程序

在以前一篇博客《以Windows服務方式運行ASP.NET Core程序》中我講述瞭如何把ASP.NET Core程序做爲Windows服務運行的方法,而今,咱們又遇到了新的問題,那就是:咱們的控制檯程序,也就是普通的.NET Core程序(而不是ASP.NET Core程序)如何以服務的方式運行呢?html

這個問題咱們在.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,名字有點拗口,翻譯成中文就是:不嗝屁服務管理器。app

NSSM的官網是:https://nssm.cc/,十分簡陋,但程序功能但是很是強大和全面的,下面我來一步步演示它如何使用。async

1,先構建一個簡單的服務程序

構建一個簡單的服務程序,程序功能描述:程序沒有圖形界面,僅僅是定時記錄一些日誌(5秒鐘寫一下日誌),在用戶按下<Ctrl>+<C>的時候,程序退出。功能明確,Okay,let's get down to work.ide

1. 建立一個.NET Core Application,叫MyService測試

2. Nuget引入Quartz和NLog.Extensions.Logging,一個用來作定時任務,另外一個用來log網站

3. 另外,程序使用了依賴注入,還須要用Nuget引入Microsoft.Extensions.DependencyInjectionui

4. 給項目增長NLog.Config配置文件,內容是spa

<?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. 發佈程序

選擇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年的最後一篇博客,若是是,上面這段文字就算是我對今年本身的主要工做總結吧。

相關文章
相關標籤/搜索