開發語言:C#
開發環境: Visual Studio 2022
微軟官方文件:https://learn.microsoft.com/zh-cn/dotnet/framework/windows-services/
最近在公司要求使用Windows服務作為訊息佇列的消費者,所以自行研究了一下C#中Windows服務如何建立以及如何使用,以及部署的方式。我是西瓜程式猿,此篇記錄一下供大家參考學習。
一、Windows服務概述
1.1-Windows服務是什麼?
Windows服務是在Windows作業系統上,以後臺形式執行的應用程式。它們可以在系統啟動時自動啟動,並且獨立於使用者登入。Windows服務通常用於執行那些長時間執行、無需使用者互動或需要在後臺持續執行的任務。
1.2-Windows能用來做什麼?
- 後臺任務和自動化:可以使用Windows服務來執行重複性的計劃任務、資料同步、定期備份、報告生成等。
- 網路服務:Windows服務可以作為網路伺服器提供網路服務,如Web伺服器、FTP伺服器、郵件伺服器等。
- 定時任務:Windows服務可以建立定時任務並在指定時間間隔或特定事件發生時觸發執行操作。
- 資料處理:可以使用Windows服務進行資料處理、資料轉換、資料清洗等批次處理任務。
- 訊息佇列:可以用於訊息佇列的消費者,後臺任務一直和訊息佇列保持長連線,需要消費時會自動接收到進行業務處理。
1.3-Windows服務有什麼優勢?
- 後臺執行:Windows服務在後臺執行,不會干擾使用者的工作,也無需使用者登入即可持續執行任務。
- 系統級別許可權:Windows服務可以在系統級別執行,具有更高的許可權,可以訪問系統資源和執行敏感操作。
- 自動啟動:Windows服務可以在系統啟動時自動啟動,確保任務始終處於執行狀態。
- 可靠性和穩定性:Windows服務被設計為長時間執行的應用程式,具有較高的可靠性和穩定性。
二、建立Windows服務
2.1-建立Windows服務專案
(1)開啟【Visual Studio】開發工具,然後選擇【 Windows 服務(.NET Framework) 】,點選下一步。
注意:Windows服務只有在.NET Framework版本中才有了,在跨平臺中使用Worker Service。
(2)修改專案名稱和專案儲存目錄,專案名稱我寫的是【MyDemoService】,然後框架我選擇的是【.NET Farmework 4.8】,這個可以根據自己的需要填寫和選擇,然後點選【建立】。
建立好的目錄如下:【Program.cs】是主程式的入口,【Service1.cs】是服務的入口,可以建立多個,然後在Prodrams.cs中配置就好了。
(3)【Service1】服務名稱可以重新命名修改,此處我重新命名為【MyDemoService】, Program.cs檔案中也相對應的也要進行修改。
(4)然後我們就可以在【MyDemoService】中寫業務邏輯程式碼了,有很多種方式可以定位到要寫的具體檔案,先列舉兩種常用的。
方法一:在【program.cs】檔案中,找到這個類,按鍵盤上的F12可以直接進入檢視檔案。
方法二:直接右擊,然後點選【檢視程式碼】。
業務程式碼寫到這裡面:
到這一步服務就建立好了,然後就寫具體的業務程式碼就行了。注意:服務必須至少重寫 OnStart 和 OnStop 才有用。
2.2-服務可以重寫的方法
/// <summary>
/// 服務啟動:指示服務開始執行時應採取的操作。 必須在此過程中為服務編寫程式碼才能執行有用的操作。
/// </summary>
/// <param name="args"></param>
protected override void OnStart(string[] args)
{
}
/// <summary>
/// 服務停止:指示在服務停止執行時應發生什麼情況。
/// </summary>
protected override void OnStop()
{
}
/// <summary>
/// 暫停:指示在服務暫停時應發生什麼情況。
/// </summary>
protected override void OnPause()
{
}
/// <summary>
/// 繼續:指示服務在暫停後恢復正常執行時應發生什麼情況。
/// </summary>
protected override void OnContinue()
{
}
/// <summary>
/// 停止前:指示在系統關閉之前應發生什麼情況(如果此時服務正在執行)。
/// </summary>
protected override void OnShutdown()
{
}
2.3-配置日誌(log4net)
為了方便測試,先介紹一下如何使用log4net做日誌記錄,當日志啟動時和停止時我們記錄一下。
(1)我們在專案目錄下新建一個資料夾【LogConfig】,然後再建立一個檔案為【log4net.config】。
(2)【log4net.config】內容如下。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
</configSections>
<system.web>
<compilation debug="true" targetFramework="4.5.2" />
<httpRuntime targetFramework="4.5.2" />
</system.web>
<log4net>
<!--錯誤日誌:::記錄錯誤日誌-->
<!--按日期分割日誌檔案 一天一個-->
<!-- appender 定義日誌輸出方式 將日誌以回滾檔案的形式寫到檔案中。-->
<appender name="ErrorAppender" type="log4net.Appender.RollingFileAppender">
<!--儲存路徑:下面路徑專案啟動的時候自動在C盤中建立log、logError檔案-->
<file value="log/error/error_" />
<!-- 如果想在本專案中新增路徑,那就直接去掉C:\\ 只設定log\\LogError 專案啟動中預設建立檔案 -->
<appendToFile value="true"/>
<!--按照何種方式產生多個日誌檔案(日期[Date],檔案大小[Size],混合[Composite])-->
<rollingStyle value="Date"/>
<!--這是按日期產生資料夾-->
<datePattern value="yyyy-MM-dd'.log'"/>
<!--是否只寫到一個檔案中-->
<staticLogFileName value="false"/>
<!--保留的log檔案數量 超過此數量後 自動刪除之前的 好像只有在 按Size分割時有效 設定值value="-1"為不限檔案數-->
<param name="MaxSizeRollBackups" value="100"/>
<!--每個檔案的大小。只在混合方式與檔案大小方式下使用。超出大小後在所有檔名後自動增加正整數重新命名,數字最大的最早寫入。可用的單位:KB|MB|GB。不要使用小數,否則會一直寫入當前日誌-->
<maximumFileSize value="50MB" />
<!-- layout 控制Appender的輸出格式,也可以是xml 一個Appender只能是一個layout-->
<layout type="log4net.Layout.PatternLayout">
<!--每條日誌末尾的文字說明-->
<!--輸出格式 模板-->
<!-- <param name="ConversionPattern" value="記錄時間:%date 執行緒ID:[%thread] 日誌級別:%-5level 記錄類:%logger
操作者ID:%property{Operator} 操作型別:%property{Action}%n 當前機器名:%property%n當前機器名及登入使用者:%username %n
記錄位置:%location%n 訊息描述:%property{Message}%n 異常:%exception%n 訊息:%message%newline%n%n" />-->
<!--樣例:2008-03-26 13:42:32,111 [10] INFO Log4NetDemo.MainClass [(null)] - info-->
<!--<conversionPattern value="%newline %n記錄時間:%date %n執行緒ID:[%thread] %n日誌級別: %-5level %n錯誤描述:%message%newline %n"/>-->
<conversionPattern value="%n==========
%n【日誌級別】%-5level
%n【記錄時間】%date
%n【執行時間】[%r]毫秒
%n【出錯檔案】%F
%n【出錯行號】%L
%n【出錯的類】%logger 屬性[%property{NDC}]
%n【錯誤描述】%message
%n【錯誤詳情】%newline"/>
</layout>
<filter type="log4net.Filter.LevelRangeFilter,log4net">
<levelMin value="ERROR" />
<levelMax value="FATAL" />
</filter>
</appender>
<!--DEBUG:::記錄DEBUG日誌-->
<!--按日期分割日誌檔案 一天一個-->
<!-- appender 定義日誌輸出方式 將日誌以回滾檔案的形式寫到檔案中。-->
<appender name="DebugAppender" type="log4net.Appender.RollingFileAppender">
<!--儲存路徑:下面路徑專案啟動的時候自動在C盤中建立log、logError檔案-->
<file value="log/debug/debug_" />
<!-- 如果想在本專案中新增路徑,那就直接去掉C:\\ 只設定log\\LogError 專案啟動中預設建立檔案 -->
<appendToFile value="true"/>
<!--按照何種方式產生多個日誌檔案(日期[Date],檔案大小[Size],混合[Composite])-->
<rollingStyle value="Date"/>
<!--這是按日期產生資料夾-->
<datePattern value="yyyy-MM-dd'.log'"/>
<!--是否只寫到一個檔案中-->
<staticLogFileName value="false"/>
<!--保留的log檔案數量 超過此數量後 自動刪除之前的 好像只有在 按Size分割時有效 設定值value="-1"為不限檔案數-->
<param name="MaxSizeRollBackups" value="100"/>
<!--每個檔案的大小。只在混合方式與檔案大小方式下使用。超出大小後在所有檔名後自動增加正整數重新命名,數字最大的最早寫入。可用的單位:KB|MB|GB。不要使用小數,否則會一直寫入當前日誌-->
<maximumFileSize value="50MB" />
<!-- layout 控制Appender的輸出格式,也可以是xml 一個Appender只能是一個layout-->
<layout type="log4net.Layout.PatternLayout">
<!--每條日誌末尾的文字說明-->
<!--輸出格式 模板-->
<!-- <param name="ConversionPattern" value="記錄時間:%date 執行緒ID:[%thread] 日誌級別:%-5level 記錄類:%logger
操作者ID:%property{Operator} 操作型別:%property{Action}%n 當前機器名:%property%n當前機器名及登入使用者:%username %n
記錄位置:%location%n 訊息描述:%property{Message}%n 異常:%exception%n 訊息:%message%newline%n%n" />-->
<!--樣例:2008-03-26 13:42:32,111 [10] INFO Log4NetDemo.MainClass [(null)] - info-->
<!--<conversionPattern value="%newline %n記錄時間:%date %n執行緒ID:[%thread] %n日誌級別: %-5level %n錯誤描述:%message%newline %n"/>-->
<conversionPattern value="%n==========
%n【日誌級別】%-2level
%n【記錄時間】%date
%n【執行時間】[%r]毫秒
%n【debug檔案】%F
%n【debug行號】%L
%n【debug類】%logger 屬性[%property{NDC}]
%n【debug描述】%message"/>
</layout>
<filter type="log4net.Filter.LevelRangeFilter,log4net">
<levelMin value="DEBUG" />
<levelMax value="WARN" />
</filter>
</appender>
<!--INFO:::記錄INFO日誌-->
<!--按日期分割日誌檔案 一天一個-->
<!-- appender 定義日誌輸出方式 將日誌以回滾檔案的形式寫到檔案中。-->
<appender name="INFOAppender" type="log4net.Appender.RollingFileAppender">
<!--儲存路徑:下面路徑專案啟動的時候自動在C盤中建立log、logError檔案-->
<file value="log/info/info_" />
<!-- 如果想在本專案中新增路徑,那就直接去掉C:\\ 只設定log\\LogError 專案啟動中預設建立檔案 -->
<appendToFile value="true"/>
<!--按照何種方式產生多個日誌檔案(日期[Date],檔案大小[Size],混合[Composite])-->
<rollingStyle value="Date"/>
<!--這是按日期產生資料夾-->
<datePattern value="yyyy-MM-dd'.log'"/>
<!--是否只寫到一個檔案中-->
<staticLogFileName value="false"/>
<!--保留的log檔案數量 超過此數量後 自動刪除之前的 好像只有在 按Size分割時有效 設定值value="-1"為不限檔案數-->
<param name="MaxSizeRollBackups" value="100"/>
<!--每個檔案的大小。只在混合方式與檔案大小方式下使用。超出大小後在所有檔名後自動增加正整數重新命名,數字最大的最早寫入。可用的單位:KB|MB|GB。不要使用小數,否則會一直寫入當前日誌-->
<maximumFileSize value="50MB" />
<!-- layout 控制Appender的輸出格式,也可以是xml 一個Appender只能是一個layout-->
<layout type="log4net.Layout.PatternLayout">
<!--每條日誌末尾的文字說明-->
<!--輸出格式 模板-->
<!-- <param name="ConversionPattern" value="記錄時間:%date 執行緒ID:[%thread] 日誌級別:%-5level 記錄類:%logger
操作者ID:%property{Operator} 操作型別:%property{Action}%n 當前機器名:%property%n當前機器名及登入使用者:%username %n
記錄位置:%location%n 訊息描述:%property{Message}%n 異常:%exception%n 訊息:%message%newline%n%n" />-->
<!--樣例:2008-03-26 13:42:32,111 [10] INFO Log4NetDemo.MainClass [(null)] - info-->
<!--<conversionPattern value="%newline %n記錄時間:%date %n執行緒ID:[%thread] %n日誌級別: %-5level %n錯誤描述:%message%newline %n"/>-->
<conversionPattern value="%n==========
%n【日誌級別】%-2level
%n【記錄時間】%date
%n【執行時間】[%r]毫秒
%n【info檔案】%F
%n【info行號】%L
%n【info類】%logger 屬性[%property{NDC}]
%n【info描述】%message"/>
</layout>
<filter type="log4net.Filter.LevelRangeFilter,log4net">
<levelMin value="INFO" />
<levelMax value="WARN" />
</filter>
</appender>
<!--Set root logger level to DEBUG and its only appender to A1-->
<root>
<!--控制級別,由低到高: ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF-->
<level value="ALL" />
<appender-ref ref="DebugAppender" />
<appender-ref ref="ErrorAppender" />
<appender-ref ref="INFOAppender" />
</root>
</log4net>
</configuration>
(3)並且右擊【【log4net.config】】檔案,點選【屬性】,然後將[複製到輸出目錄]設定為【始終複製】。
(4)然後安裝log4net。在專案目錄中右擊【引用】,然後點選【管理NuGet程式包】
(5)然後點選瀏覽,搜尋【log4net】,右側點選安裝。
(6)重要:然後配置【AssemblyInfo.cs 】檔案,如果不配置,是輸出不了日誌的。
新增到底部即可:(如果你的【log4net.config】檔案路徑和我的不一樣,記得修改成跟自己配置路徑一樣的)。
程式碼:
[assembly: log4net.Config.XmlConfigurator(ConfigFileExtension = "config", ConfigFile = "LogConfig/log4net.config", Watch = true)]
(7)在服務啟動方法【OnStart】中,配置啟動log4net。
程式碼:
XmlConfigurator.Configure(new System.IO.FileInfo("LogConfig/log4net.config"));
(8)然後就可以使用log4net了,首先在Windows服務中獲得log4net的例項。
程式碼:
private static readonly log4net.ILog logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
三、Windows服務的執行和部署
3.1-服務基本配置
(1)點選我們的服務【MyDemoService.cs】,然後右擊點選【新增安裝程式】。
(2)然後可以看到下面多出來了一個檔案,就是安裝程式。
(3)然後可以修改基本資訊,服務元件中的【服務名稱】【服務描述】等等。我們右擊【serviceInstall1】點選屬性,然後進行修改。
(4)然後點選【serviceProcessInstall1】右擊屬性,進行修改。
3.2-服務執行與釋出
當我們直接按F5或者其他方式直接執行專案時,會提示:"無法從命令列或除錯程式啟動服務。必須首先安裝 Windows服務(使用installutil.exe),然後用ServerExplorer、Windows服務管理工具或 NET START命令啟動它。"。不是這樣執行的,跟著下面步驟來操作執行與釋出Windows服務吧。
前提注意:如果你設定的目標平臺是x64,開啟的目錄會不一樣,不然導致服務執行不起來。可以右擊專案名,點選【屬性】——>【生成】——>【目標平臺】檢視。
如果不是x64版本,複製這個地址:
C:\Windows\Microsoft.NET\Framework\v4.0.30319
如果是x64版本,複製這個地址:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319
不然會報類似這種錯誤:在初始化安裝時發生異常: System.BadImageFormatException: 未能載入檔案或程式集...
(1)然後我們把上面的地址(根據自己的環境選擇)新增到環境變數中。點選【控制皮膚】——>【系統和安全】
(2)然後點選【系統】
(3)點選【高階系統設定】
(4)點選【環境變數】
(5)在【系統變數】中找到Path,然後點選【編輯】。
(6)然後點選【新建】,然後把我們複製的目錄複製到這裡。然後點選確認即可。
(7)測試是否配置成功,輸入這個命令檢視一下【InstallUtil】,如果是下面這樣的內容說明成功了。
(8)然後編輯解決方案和專案。
(9)以管理員身份執行cmd命令,然後安裝服務。
InstallUtil 專案啟動執行檔案全路徑
西瓜程式猿的例子:
InstallUtil D:\專案演示臨時儲存\MyDemoService\MyDemoService\bin\Debug\MyDemoService.exe
(10)出現這個說明安裝成功了。
(11)開啟服務管理器,找到要啟動的服務,然後右擊啟動服務。
啟動後可以看到日誌也有了:
(12)如果要解除安裝服務,可以執行這個命令:
InstallUtil /u 專案啟動執行檔案全路徑
西瓜程式猿的例子:
InstallUtil /u D:\專案演示臨時儲存\MyDemoService\MyDemoService\bin\Debug\MyDemoService.exe
3.3-常見命令
1、安裝服務:InstallUtil 專案啟動執行檔案全路徑
3、啟動服務:net start 服務名
4、停止服務:net stop 服務名
4、解除安裝服務:InstallUtil /u 專案啟動執行檔案全路徑
3.4-檢視計算機事件
如果在啟動Windows服務時報錯,我們可以透過檢視計算機事件查詢到具體的報錯資訊。
(1)按鍵盤上的【Win+R】,然後輸入【eventvwr.msc】
(2) 然後點選確定,就可以開啟事件檢視器的視窗。在視窗左側找到【Windows日誌】——>【系統】,就可以看到電腦的開機關機及日誌記錄了。
3.5-在伺服器上安裝相同Windows服務的多個例項
這個方案比較全可以參考:https://www.u72.net/b/show-317328.html
我是西瓜程式猿,用的是這種方法:
(1)透過此命令可以部署多個,[servicename]唯一就行了,自己配置。
sc create [servicename] binpath= [path to your exe]
注意: [servicename]填寫的是服務名稱,[path to your exe] 是執行檔案的路徑,必須是完整路徑,不要忘記binpath= 後面的空格。這種方法確實允許多次安裝服務。但是服務安裝程式提供的所有資訊。 F.e.描述、登入型別等被忽略。
3.6-常見的錯誤
1、無法開啟計算機“.”上的服務控制管理器
在“安裝”階段發生異常。
System.InvalidOperationException: 無法開啟計算機“.”上的服務控制管理器。此操作可能需要其他特權。
引發了內部異常 System.ComponentModel.Win32Exception,錯誤訊息如下: 拒絕訪問。。
正在開始安裝的“回退”階段。
檢視日誌檔案的內容以獲得 D:\專案演示臨時儲存\MyDemoService\MyDemoService\bin\Debug\MyDemoService.exe 程式集的進度。
該檔案位於 D:\專案演示臨時儲存\MyDemoService\MyDemoService\bin\Debug\MyDemoService.InstallLog。
正在回滾程式集“D:\專案演示臨時儲存\MyDemoService\MyDemoService\bin\Debug\MyDemoService.exe”。
受影響的引數是:
logtoconsole =
logfile = D:\專案演示臨時儲存\MyDemoService\MyDemoService\bin\Debug\MyDemoService.InstallLog
assemblypath = D:\專案演示臨時儲存\MyDemoService\MyDemoService\bin\Debug\MyDemoService.exe
正在將事件日誌還原到源 TestDemoServices 的前一狀態。
“回退”階段已成功完成。
已完成事務處理安裝。
安裝失敗,已執行回退。
解決:許可權不夠,開啟cmd時記得【以管理員身份】開啟。
2、未能載入檔案或程式集xxx的基個依賴項
在初始化安裝時發生異常: System.BadImageFormatException: 未能載入檔案或程式集“file://E:\DebuginServers.sB惑的基個依賴項。試圖載入格式不正確的程式。
解決:請看本文第【3.2】點
3、在執行【.bat】檔案時,報路徑不存在相關的錯
** 執行命令**:InstallUtil /u D:\專案演示臨時儲存\MyDemoService\MyDemoService\bin\Debug\MyDemoService.exe
錯誤資訊:C:\Windows\system32>InstallUtil /u D:\欏圭洰婕旂ず涓存椂淇濆瓨\MyDemoService\MyDemoService\bin\Debug\MyDemoService.exe Microsoft (R) .NET Framework 安裝實用工具版本 4.8.9037.0 版權所有 (C) Microsoft Corporation。保留所有權利。 在初始化安裝時發生異常: System.IO.FileNotFoundException: 未能載入檔案或程式集“file:///D:\欏圭洰婕旂ず涓存椂淇濆瓨\MyDemoService\MyDemoService\bin\Debug\MyDemoService.exe”或它的某一個依賴項。系統找不到指定的檔案。。
分析:這個錯是亂碼導致的,執行的命令是這個【InstallUtil /u D:\專案演示臨時儲存\MyDemoService\MyDemoService\bin\Debug\MyDemoService.exe】,但是在控制檯裡面顯示的是這個【file:///D:\欏圭洰婕旂ず涓存椂淇濆瓨\MyDemoService\MyDemoService\bin\Debug\MyDemoService.exe】。 出現了亂碼,這可能是由於控制檯編碼設定不正確導致的 。
解決:使用正確的編碼,嘗試將控制檯編碼設定為與檔案路徑所使用的編碼一致。例如,如果檔案路徑是UTF-8編碼,您可以在控制檯中執行以下命令進行設定:
chcp 65001
截圖:
四、高效工具:編寫bat啟動解除安裝服務
4.1-安裝服務
我們可以建立一個檔案【安裝服務.bat】輸入以下內容:
chcp 65001
REM Install
InstallUtil 專案啟動執行檔案全路徑
pause
注意:執行這個命令需要【以管理員身份】執行,不然會報這個錯誤:
System.InvalidOperationException: 無法開啟計算機“.”上的服務控制管理器。此操作可能需要其他特權。
引發了內部異常 System.ComponentModel.Win32Exception,錯誤訊息如下: 拒絕訪問。。
正確執行方式:
4.2-啟動服務
我們可以建立一個檔案【解除安裝服務.bat】輸入以下內容:
REM Install
net start 服務名
pause
注意:執行這個命令需要【以管理員身份】執行,不然會報這個錯誤:
發生系統錯誤。
拒絕訪問。
4.3-停止服務
我們可以建立一個檔案【解除安裝服務.bat】輸入以下內容:
REM Install
net stop 服務名
pause
注意:執行這個命令需要【以管理員身份】執行,不然會報這個錯誤:
發生系統錯誤。
拒絕訪問。
4.4-解除安裝服務
我們可以建立一個檔案【解除安裝服務.bat】輸入以下內容:
chcp 65001
REM Install
InstallUtil /u 專案啟動執行檔案全路徑
pause