程序員的自我救贖---11.3:WinService服務


《前言》

(一) Winner2.0 框架基礎分析

(二)PLSQL報表系統

(三)SSO單點登錄

(四) 短信中心與消息中心

(五)錢包系統

(六)GPU支付中心

(七)權限系統

(八)監控系統

(九)會員中心

(十) APP版本控制系統

(十一)Winner前端框架與RPC接口規范講解

(十二)上層應用案例

(十三)總結

 

《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。或者掃描左側二維碼加群。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM