《WinService服務》
說道Windows服務基本每個以.net為主要開發語言的技術團隊都會用到這個,Winner2.0中對於WinServices也有一些與眾不同的地方。
正常來說,每次開發一個項目如果我們要用到Windows服務就要單獨在項目下建立一個WinService。其實WinService 就是一個殼子。
但是每次為了這個殼子都要投產到服務器還要通過cmd命令去部署。
因為一直習慣這么做,可能不會覺得麻煩,但是項目一多就會發現要部署幾十個服務那不那么順心了。所以Jason開發了一套“WinServiceJob”工具。
先說說有點再說是怎么實現的:
1,無需新建WinService服務項目。
2,無需cmd命令部署。
3,更新無需停止服務。
從思路上來說,Jason開發的WinServiceJob思路和《短信中心》是一樣的(本身兩個項目也都是Jason開發的)。
簡單講就是 WinServiceJob 是一個類似Docker的容器,它本身不做任何業務。具體業務是讀取數據庫的配置然后反射程序集來執行的。
WinServiceJob ,就是個空殼子。所有項目只需要編譯一個service程序集然扔到 WinServiceJob 項目下,然后數據庫一配置WinServiceJob 就會
幫我們去執行工作,這里可以通過Cycle字段來配置執行周期,是一天一次還是60分鍾一次。每次執行完一個服務之后更新NextRunTime保存下一次
執行時間。
這樣省去了程序員們每次開發Windows 服務時的瑣碎事情,只需在當前開發項目編譯一個要執行的service.dll 然后交給WinServiceJob 管理人員
就行了。看了前面《短信中心》講解的就應該很清楚,要支持這種寫法必須要求所有的執行項目要繼承接口約束。
using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Winner.Job.Master.Interface { /// <summary> /// WinService工作單元接口 /// </summary> public interface IJob { /// <summary> /// 執行工作單元 /// </summary> /// <param name="runTime"></param> /// <returns>返回執行結果JobResult</returns> JobResult Run(DateTime runTime); } }
兩個值得一說的地方:
1,WinServiceJob更新時服務無需重啟;
2,WinServiceJob可以在服務器上部署多個;
這兩個地方實現的方式也很特殊,先來看一段代碼:
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Remoting.Lifetime; using System.Text; using System.Threading.Tasks; using Winner.Job.Master.Interface; namespace Winner.Job.Master.Remoting { /// <summary> /// 遠程處理的應用程序中跨應用程序域邊界訪問對象。 /// </summary> public class RemoteLoader : MarshalByRefObject { private Assembly _assembly; public void LoadAssembly(string assemblyFile) { try { _assembly = Assembly.LoadFrom(assemblyFile); } catch (Exception ex) { throw ex; } } public T GetInstance<T>(string typeName) where T : class { if (_assembly == null) return null; var type = _assembly.GetType(typeName); if (type == null) return null; return Activator.CreateInstance(type) as T; } public JobResult ExecuteMothod(string typeName, DateTime? runtime) { if (_assembly == null) { return JobResult.FailResult("加載程序集失敗"); } var type = _assembly.GetType(typeName); var obj = Activator.CreateInstance(type); IJob job = obj as IJob; return job.Run(runtime.HasValue ? runtime.Value : DateTime.Now); } public override object InitializeLifetimeService() { ILease aLease = (ILease)base.InitializeLifetimeService(); if (aLease.CurrentState == LeaseState.Initial) { // 不過期:TimeSpan.Zero aLease.InitialLeaseTime = TimeSpan.FromMinutes(1000); } return aLease; } } }
這是一個遠程調用的工具類,每個子服務中必須把這個程序集放到目錄下,主服務拿子服務的目錄下RemoteLoader來獲取子服務的信息,
准確來說是服務名:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using Winner.Framework.Utils; using Winner.Job.Master.DataAccess; using Winner.Job.Master.Entites; using Winner.Job.Master.Entites.Map; using Winner.Job.Master.Interface; using Winner.Job.Master.Remoting; namespace Winner.Job.Master.Facade { /// <summary> /// 工作計划服務對象 /// </summary> [Serializable] public class JobService { public void Execute(JobMap job) { try { //遠程執行服務 var result = RemoteExecute(job); if (!result.Success) { job.Status = (int)JobStatus.失敗; job.ErrorInfo = result.Message; } else { job.Status = (int)JobStatus.成功; job.ErrorInfo = string.Empty; } //計算狀態和下次運行情況 ModifyModel(job); //修改數據庫時間 Modify(job); //GC回收 GC.Collect(); } catch (Exception ex) { Log.Error(ex); } } /// <summary> /// 遠程執行 /// </summary> /// <param name="job"></param> /// <returns></returns> private JobResult RemoteExecute(JobMap job) { AppDomain appDomain = null; try { //服務反射信息 string[] array = job.TypeConfig.Split(','); //服務的程序集名稱 string assemblyFile = array[0]; //服務的類名稱 string className = array[1]; //設置AppDomain安裝程序信息 AppDomainSetup setup = new AppDomainSetup(); //服務名稱 setup.ApplicationName = job.ServiceName; //安裝(運行)目錄(提示:在當前運行目錄的子目錄,而子目錄則是”服務名稱“) setup.ApplicationBase = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, job.ServiceName); setup.ShadowCopyDirectories = setup.ApplicationBase; setup.ShadowCopyFiles = "true"; //Config配置文件路徑 setup.ConfigurationFile = Path.Combine(setup.ApplicationBase, assemblyFile + ".dll.config"); //構造一個新的AppDomain appDomain = AppDomain.CreateDomain(job.ServiceName, null, setup); //獲取遠程調用程序 對象名稱 string name = Assembly.LoadFile(Path.Combine(setup.ApplicationBase, "Winner.Job.Master.Remoting.dll")).GetName().FullName; //創建遠程調用程序實例 var remoteLoader = (RemoteLoader)appDomain.CreateInstanceAndUnwrap(name, typeof(RemoteLoader).FullName); //加載服務程序集 remoteLoader.LoadAssembly(Path.Combine(setup.ApplicationBase, assemblyFile + ".dll")); //執行服務 var result = remoteLoader.ExecuteMothod(className, job.NextRunTime); Log.Info(string.Format("執行結果:Service={0} Success={1} Message={2}", job.ServiceName, result.Success, result.Message)); return result; } catch (Exception ex) { Log.Error("執行服務異常", ex); return JobResult.FailResult("遠程執行服務出現異常:" + ex.Message); ; } finally { if (appDomain != null) { Log.Info("卸載AppDomain:" + appDomain.FriendlyName); AppDomain.Unload(appDomain); appDomain = null; } } } private void ModifyModel(JobMap job) { if (job == null) return; if (job.Status != (int)JobStatus.暫停 && job.Status != (int)JobStatus.成功 && job.IsContinue == 2) { job.RetryTime = DateTime.Now.AddMinutes(job.RetryInterval); return; } if (job.IsContinue == 1 || job.Status == (int)JobStatus.成功) { job.NextRunTime = job.NextRunTime.HasValue ? job.NextRunTime.Value : DateTime.Now; if (job.Cycle != 0) { if (job.Cycle < 0) job.NextRunTime = job.NextRunTime.HasValue ? job.NextRunTime.Value.AddMinutes(0 - job.Cycle) : DateTime.Now.AddMinutes(0 - job.Cycle); else { switch ((Cycle)job.Cycle) { case Cycle.Daily: job.NextRunTime = job.NextRunTime.Value.AddDays(1); break; case Cycle.Fortnightly: job.NextRunTime = job.NextRunTime.Value.AddDays(14); break; case Cycle.Monthly: job.NextRunTime = job.NextRunTime.Value.AddMonths(1); break; case Cycle.Weekly: job.NextRunTime = job.NextRunTime.Value.AddDays(7); break; case Cycle.Yearly: job.NextRunTime = job.NextRunTime.Value.AddYears(1); break; default: break; } } } } } private bool Modify(JobMap job) { Tsys_Winservice daWinService = new Tsys_Winservice(); daWinService.WinServiceId = job.WinServiceId; daWinService.NextRunTime = job.NextRunTime; daWinService.Status = job.Status; daWinService.RetryTime = job.RetryTime; daWinService.RetryInterval = job.RetryInterval; if (!daWinService.Update()) { return false; } return true; } } }
這里采用的方式是,每次新的服務去創建一個新的APPDomain 和 線程去執行,執行完了之后線程同步,APPDomain 也卸載掉。
包括使用影加載,這里的效果就是不會對dll進行文件占用,所以隨意更新dll是不不需要去重啟服務的。
另外前面有說到 新增一個子服務 也可以不需要重啟服務,這種方式是可以實現的,但是因為要不停的讀取數據庫,所以后期修改了一下。
為了避免每次時時刻刻都要去掃數據庫,所以采用了一次性加載到隊列當中,然后如果有更新服務的話,還是要重啟WinServiceJob。
最后,要說的是如果一個WinServiceJob可能負載的子服務太多造成臃腫執行性能低的話,我們可以部署多個。由於Winservice重名的話是
部署不了的。所以這里WinServiceJob在部署時具體的名稱我們是從xml配置文件中讀取的。
<?xml version="1.0" encoding="utf-8"?> <Root> <InstallInfo> <ServiceName value="Winner.Job.Master.WinService" /> <DisplayName value="Winner.Job.Master.DisplayName" /> <Description value="Winner.Job.Master.Description 列隊0號" /> </InstallInfo> </Root>
C# 這邊就讀取配置文件就行了:
這里當時特地還做了測試,直接從APP.Config文件中讀取是讀不到的,所以要單獨寫xml文件去配置。
我們來看看最終的部署結構目錄:
我們在實際的使用過程中差不多一個WinServiceJob負載了二三十個服務在跑。 總共開發的四五年里我們寫的WinService不下上百個。
WinServiceJob 幫程序員省去了開發服務,部署服務的一系列瑣碎事宜。
好了,就寫到這里。最后一句話:代碼不重要,還是思想。WinServiceJob主要還是模仿了類似Docker容器這種思想來做的。
這里我把整個WinServiceJob 的代碼全部開源到GitHub方便大家參考:https://github.com/demon28/WinServiceJob
有興趣一起探討Winner框架的可以加我們QQ群:261083244。或者掃描左側二維碼加群。