一、概述
從.net framework遷移到.net core,除了要遷移基於asp.net的web程序,還有一個項目也是比較重要的,即服務程序或叫守護進程。在.net core中創建workerservice程序已經不同於.net framework,首先workerservice是按照DI方式管理,第二workerservice實現了真正的跨平臺,第三部署安裝需要結合操作系統工具,windows下使用sc.exe,linux下使用systemd。
特別說明,本文不會記錄操作系統的安裝和配置,也不會記錄dotnet運行環境的安裝和使用,所以如果你有這方面的需求,我表示很抱歉,請自行搜索,或去微軟官網查看dotnet運行環境的安裝和使用教程,本文更不會記錄Docker環境的部署。
操作環境說明:
1.設計編程在Windows11 + VS2022 + dotnet6
2.測試環境在Windows Server 2022和Debian11
好了,繼續下邊的工作。
二、設計
此程序主要目的是將workerservice程序從.net framework遷移到.net core,設計的功能是服務啓動和結束時輸出日誌,啓動後服務持續輸出日誌(條/秒)。
此程序主要包含以下部分功能:
第一,日誌log4net的配置:在asp.netcore中未曾見到的log4net抽瘋病,在workerservice中見到了,主要是log4net.config配置文件引起的。
第二,appsettings.json配置文件的操作:同在asp.netcore中操作一樣
第三,worker類的定義和功能實現:遇到了不少問題,不過都一一解決了。
三、編碼
1.日誌相關編碼
日誌編碼主要設計三個文件:LogHelper、Log4Writer和log4net.config
1.1.LogHelper文件主要負責啓動log4net。需要在Program文件中調用啓動。
public class LogHelper { private static object _objLocker = new object(); private static bool _isConfigure = false; public static void Configure() { if (!_isConfigure) { lock (_objLocker) { if (!_isConfigure) { _isConfigure = true; var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log4net.config"); XmlConfigurator.Configure(new FileInfo(logPath)); } } } } }
1.2.Log4Writer文件主要提供程序輸出日誌到文件
public class Log4Writer { private static readonly log4net.ILog loginfo = log4net.LogManager.GetLogger("loginfo"); private static readonly log4net.ILog logerror = log4net.LogManager.GetLogger("logerror"); public static void WriteLog(string info) { if (loginfo.IsInfoEnabled) { loginfo.Info(info); } } public static void WriteLog(string info, Exception ex) { if (logerror.IsErrorEnabled) { logerror.Error(info, ex); } } }
1.3.log4net.config文件主要定義日誌的輸出信息。如果不使用DI模式那麼DiocAppender相關的配置刪除即可。
<?xml version="1.0" encoding="utf-8" ?> <log4net> <root> <!--日誌級別: NONE > FATAL > ERROR > WARN > INFO > DEBUG > ALL --> <priority value="ALL"/> <level value="ALL" /> <appender-ref ref="DiocAppender" /> </root> <!-- name屬性指定其名稱,type則是log4net.Appender命名空間的一個類的名稱,意思是,指定使用哪種介質--> <appender name="DiocAppender" type="log4net.Appender.RollingFileAppender" > <!--日誌輸出到exe程序這個相對目錄下--> <param name="File" value="logs/diocs/" /> <!--輸出的日誌不會覆蓋以前的信息--> <param name="AppendToFile" value="true" /> <!--備份文件的個數--> <param name="MaxSizeRollBackups" value="50" /> <!--最小鎖定模型以允許多個進程可以寫入同一個文件--> <param name="MaxFileSize" value="10240" /> <!--當前日誌文件的最大大小--> <param name="lockingModel" type="log4net.Appender.FileAppender+MinimalLock" /> <!--是否使用靜態文件名--> <param name="StaticLogFileName" value="false" /> <!--日誌文件名--> <DatePattern value="yyyyMMdd".txt"" /> <!--文件創建的方式,這裏是以Date方式創建--> <param name="RollingStyle" value="Date" /> <!--輸出級別在INFO和ERROR之間的日誌--> <filter type="log4net.Filter.LevelRangeFilter"> <param name="LevelMin" value="ALL" /> <param name="LevelMax" value="FATAL" /> </filter> <!--信息日誌佈局--> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="date [%thread] %-5level %logger - %message%newline" /> </layout> </appender> <!--錯誤日誌類--> <logger name="logerror"> <!--日誌類的名字--> <level value="ALL" /> <!--定義記錄的日誌級別--> <appender-ref ref="ErrorAppender" /> </logger> <!--信息日誌類--> <logger name="loginfo"> <!--日誌類的名字--> <level value="ALL" /> <!--定義記錄的日誌級別--> <appender-ref ref="InfoAppender" /> </logger> <!--錯誤日誌附加介質--> <!-- name屬性指定其名稱,type則是log4net.Appender命名空間的一個類的名稱,意思是,指定使用哪種介質--> <appender name="ErrorAppender" type="log4net.Appender.RollingFileAppender"> <!--日誌輸出到exe程序這個相對目錄下--> <param name="File" value="logs/errors/" /> <!--輸出的日誌不會覆蓋以前的信息--> <param name="AppendToFile" value="true" /> <!--備份文件的個數--> <param name="MaxSizeRollBackups" value="50"/> <!--最小鎖定模型以允許多個進程可以寫入同一個文件--> <param name="MaxFileSize" value="10240" /> <!--當前日誌文件的最大大小--> <param name="lockingModel" type="log4net.Appender.FileAppender+MinimalLock" /> <!--是否使用靜態文件名--> <param name="StaticLogFileName" value="false" /> <!--日誌文件名--> <param name="DatePattern" value="yyyyMMdd".txt"" /> <!--文件創建的方式,這裏是以Date方式創建--> <param name="RollingStyle" value="Date" /> <!--錯誤日誌佈局--> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="[%t]異常類:%c [%x] 異常信息:%m%n" /> </layout> </appender> <!--信息日誌附加介質--> <!-- name屬性指定其名稱,type則是log4net.Appender命名空間的一個類的名稱,意思是,指定使用哪種介質--> <appender name="InfoAppender" type="log4net.Appender.RollingFileAppender"> <!--日誌輸出到exe程序這個相對目錄下--> <param name="File" value="logs/infos/" /> <!--輸出的日誌不會覆蓋以前的信息--> <param name="AppendToFile" value="true" /> <!--備份文件的個數--> <param name="MaxSizeRollBackups" value="50" /> <!--最小鎖定模型以允許多個進程可以寫入同一個文件--> <param name="MaxFileSize" value="10240" /> <!--當前日誌文件的最大大小--> <param name="lockingModel" type="log4net.Appender.FileAppender+MinimalLock" /> <!--是否使用靜態文件名--> <param name="StaticLogFileName" value="false" /> <!--日誌文件名--> <param name="DatePattern" value="yyyyMMdd".txt"" /> <!--文件創建的方式,這裏是以Date方式創建--> <param name="RollingStyle" value="Date" /> <!--信息日誌佈局--> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%m%n" /> </layout> </appender> </log4net>
2.appsettings.json相關編碼
它的相關編碼主要涉及三個文件:ConfigFiles/AppSetting、ConfigFiles/ConfigFileManager和Consts/ConstFiles
2.1.ConfigFiles/AppSetting文件主要管理與appsettings.json相關的配置信息。
public class AppSetting { private static object _objLocker = new object(); private static AppSetting _instance; private AppSetting() { } public static AppSetting Instance { get { if (_instance == null) { lock (_objLocker) { if (_instance == null) { _instance = new AppSetting(); AppSettingConfig.Load(_instance); } } } return _instance; } } private class AppSettingConfig { private static AppSetting _appSetting; public static void Load(AppSetting appSetting) { _appSetting = appSetting; Compute(); ChangeToken.OnChange(() => ConfigFileManager.Instance.AppSetting.GetReloadToken(), () => { Change(); }); } private static void Compute() { _appSetting.ServiceName = ConfigFileManager.Instance.AppSetting.GetValue<string>(ConstFiles.AppSettings_ServiceName); } private static void Change() { } } #region Object Propertries public string ServiceName { get; private set; } #endregion }
2.2.ConfigFiles/ConfigFileManager文件主要負責管理配置文件信息。
public class ConfigFileManager { private static object _objLocker = new object(); private static ConfigFileManager _instance; private ConfigFileManager() { } public static ConfigFileManager Instance { get { if (_instance == null) { lock (_objLocker) { if (_instance == null) { _instance = new ConfigFileManager(); Create(); } } } return _instance; } } private static void Create() { //appsettings.json _instance.AppSetting = new ConfigurationBuilder() .AddJsonFile(ConstFiles.AppSettings, true, true) .Build(); } public IConfigurationRoot AppSetting { get; private set; } }
2.3.Consts/ConstFiles文件主要定義一些與配置文件相關的屬性和常量。
public class ConstFiles { public static string AppPathBase { get { return AppDomain.CurrentDomain.BaseDirectory; } } public static string AppDataPathBase { get { return Path.Combine(AppPathBase, "Data"); } } public static string AppFilePathBase { get { return Path.Combine(AppPathBase, "Files"); } } public static string AppServicePathBase { get { if (SystemUtils.IsDebug) { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), Sys_ServicePath); } else { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), Sys_ServicePath); } } } public const string Sys_ServicePath = "XXXXX\\Configuration\\Paas"; public const string AppSettings = "appsettings.json"; public const string AppSettings_ServiceName = "ServiceName"; }
3.worker相關編碼
public class Worker : BackgroundService { private readonly IHostApplicationLifetime _hostApplicationLifetime; public Worker(IHostApplicationLifetime hostApplicationLifetime) { _hostApplicationLifetime = hostApplicationLifetime; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { while (!stoppingToken.IsCancellationRequested) { Log4Writer.WriteLog($"Worker running at: {DateTimeOffset.Now}"); await Task.Delay(1000, stoppingToken); } } catch (Exception ex) { Log4Writer.WriteLog(ex.Message); } } public override Task StartAsync(CancellationToken cancellationToken) { try { _hostApplicationLifetime.ApplicationStopping.Register(() => { StopWork(); }); Log4Writer.WriteLog($"Worker Start at: {DateTimeOffset.Now}"); } catch (Exception ex) { Log4Writer.WriteLog(ex.Message); } return base.StartAsync(cancellationToken); } private void StopWork() { Log4Writer.WriteLog($"程序將要退出,請不要再接受請求,以及馬上處理完待處理請求。"); } public override Task StopAsync(CancellationToken cancellationToken) { try { Log4Writer.WriteLog($"Worker Stop at: {DateTimeOffset.Now}"); } catch (Exception ex) { Log4Writer.WriteLog(ex.Message); } return base.StopAsync(cancellationToken); } }
4.測試運行
完成上述設計編碼,此時回到Program入口文件,進行此文件的編碼,內容如下:
public static void Main(string[] args) { LogHelper.Configure(); IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { hostBuilder = hostBuilder.UseWindowsService(o => { o.ServiceName = AppSetting.Instance.ServiceName; }); } else { hostBuilder = hostBuilder.UseSystemd(); } hostBuilder = hostBuilder.ConfigureServices(services => { services.AddHostedService<Worker>(); }); IHost host = hostBuilder.Build(); host.Run(); }
編碼中已區分Windows和Linux環境,很顯然上述編碼依賴兩個軟件包Microsoft.Extensions.Hosting.WindowsServices和Microsoft.Extensions.Hosting.Systemd。完成上述編碼後,就可以使用VS進行調試運行了。
四、部署
通過VS編碼測試與實際將程序部署到生產環境還有很多事情要做,比如在Windows環境裏,你需要會使用sc.exe工具或編寫bat腳本,在Linux環境裏,你需要會編碼XXXX.service文件。
1.Windows部署
在windows下服務管理主要是sc.exe工具。主要使用以下幾條命令:
1.1.註冊服務
set cmdpath=%~dp0jks.core.test.workerService
set svcName=TestWorkerService
sc create %svcName% binpath= "%cmdpath%" type= share start= auto displayname= "%svcName%"
sc description %svcName% "xxxxxxxxxxxxxxxxxxx"
說明:
a)~dp0表示批處理腳本所在目錄
b)svcName表示服務名稱
c)binpath表示服務程序的位置目錄
d}start=auto表示該服務在計算機每次重新啓動時自動啓動並運行(即使沒有人登錄到計算機)
e)displayname表示指定一個友好名稱,用於標識用戶界面程序中的服務
f)sc description %svcName% "xxxxxxxxxxxxxxxxxxx"表示給指定的服務添加描述信息
1.2.啓動服務
sc start TestWorkerService或net start TestWorkerService
1.3.停止服務
sc stop TestWorkerService或net stop TestWorkerService
1.4.卸載服務
sc delete TestWorkerService
1.5.注意事項
1.5.1.在windows系統下運行服務,Program中一定要設置服務名稱,且與sc.exe create 服務名稱 保持一致。
2.Debian部署
在Linux下服務管理主要Systemd程序負責。通常需要爲服務程序創建.service配置文件,如MyService.service,這個文件通常放置於etc/systemd/system/目錄或usr/lib/systemd/system/目錄,這倆目錄也沒啥卻別,主要前者的優先級高一點而已。
2.1.配置文件
[Unit] Description=Long running service/daemon created from .NET worker template [Service] # The systemd service file must be configured with Type=notify to enable notifications. Type=notify # will set the Current Working Directory (CWD). Worker service will have issues without this setting WorkingDirectory=/srv/Worker # systemd will run this executable to start the service ExecStart=/srv/Worker/MyService # to query logs using journalctl, set a logical name here SyslogIdentifier=MyService # Use your username to keep things simple. # If you pick a different user, make sure dotnet and all permissions are set correctly to run the app # To update permissions, use 'chown yourusername -R /srv/Worker' to take ownership of the folder and files, # Use 'chmod +x /srv/Worker/MyService' to allow execution of the executable file User=yourusername # This environment variable is necessary when dotnet isn't loaded for the specified user. # To figure out this value, run 'env | grep DOTNET_ROOT' when dotnet has been loaded into your shell. Environment=DOTNET_ROOT=/usr/share/dotnet/dotnet # This gives time to MyService to shutdown gracefully. TimeoutStopSec=300 [Install] WantedBy=multi-user.target
2.2.配置文件修改說明
2.2.1.Description:服務程序的描述信息。
2.2.2.WorkingDirectory:服務程序所在目錄。
2.2.3.ExecStart:服務程序的啓動位置。
2.2.4.User:使用時,應將 User=yourusername 項中的 yourusername 改爲具體的 linux 系統的登錄名。
2.3.查看服務狀態
systemctl status MyService
2.4.啓動服務
systemctl start MyService
2.5.停止服務
systemctl stop MyService
2.6.開機自動啓動服務
systemctl enable MyService
2.7.禁止開機自啓服務
systemctl disable MyService
2.8.查看服務是否存在服務列表中
systemctl list-unit-files --type=service
五、總結
經上述設計、編碼、部署、測試等過程,發現.net6的worker service程序在windows和linux系統裏均運行良好,.net跨平臺已不是夢,切實可行。
源碼地址:https://gitee.com/kinbor/jks.core.test.workerService