多線程、方便擴展的Windows服務程序
吳劍 2012-06-02
原創文章,轉載必需注明出處:http://www.cnblogs.com/wu-jian/
前言
在項目應用中經常會碰到定時調度的工作,比如我曾經開發一個日訪問量超過1000W的網站,如果這1000W訪問都從數據庫讀取數據顯示給用戶,我的服務器肯定承受不了,於是我需要每10分鍾把首頁生成一次.html的靜態文件;我的數據庫里還有一張表,用來收集系統的各種異常、出錯和危險信息,我需要把這張表里的記錄每半個小時向運維人員發送一封郵件,這樣他們就可以及時了解到系統運行情況;我需要每天凌晨統計數據生成報表以方便各個部門的頭頭們清早就可以查看;需要每月統計數據生成報表給BOSS以提醒他是不是該加薪了......諸如此類的需求越來越多,於是我考慮做一個公用的Windows服務程序框架,利用多線程,可同時執行多項任務,同時任務的擴展要簡單快速,當然還需要可配置,每項任務的開關、執行時間等通過隨時修改配置文件即可調控。
OK,本文主要針對Windows服務程序的實現以及多線程多任務的設計,代碼中涉及到面向對象、反射、XML、Hashtable等基礎技術,讀者可自行查閱相關資料,不作深入探討。個人能力有限,不足之處還請指正。程序於生產環境穩定運行,監控內存、IO等未發現異常。
設計
不論是生成靜態頁、發送郵件、還是統計數據生成報表,所有任務都可抽像出兩個公共部分:配置與邏輯。配置包括任務項的名稱描述,開啟或關閉,程序集以及執行時間;邏輯包括任務執行與停止。所有任務需要繼承和實現上述抽像類,另一個工具類實現IConfigurationSectionHandler接口,以添加自定義配置節點和對配置文件進行操作,工具類還包含一些公共靜態方法,如下圖所示:
編碼
創建Windows服務項目
Visual Studio已為我們提供了Windows Service的模板,如下圖所示:
配置類ServiceConfig
每項任務需要包含一個配置類並繼承至Base.ServiceConfig。抽象屬性必須在子類中實現,如果任務中包含更多的自定義配置,也可在此擴展。
之所以使用配置類,是希望把配置數據加載進內存,以避免Windows服務程序頻繁對配置文件進行讀取,減少IO性能消耗。
using System; namespace WuJian.WindowsServiceDemo.Base { /// <summary> /// 服務配置類 /// </summary> public abstract class ServiceConfig { #region 子類必需實現的抽象屬性 /// <summary> /// 工作項說明 /// </summary> public abstract string Description { get; } /// <summary> /// 工作項是否開啟 /// </summary> public abstract string Enabled { get; } /// <summary> /// 工作項程序集 /// </summary> public abstract string Assembly { get; } /// <summary> /// 工作項執行間隔時間 /// </summary> public abstract int Interval { get; } #endregion #region 擴展屬性 //可擴展 #endregion } }
工作項ServiceJob
每項任務必須包含一個工作類並繼承至Base.ServiceJob。抽像方法Start()與Stop()必須在子類中實現。
using System; namespace WuJian.WindowsServiceDemo.Base { /// <summary> /// 工作項 /// </summary> public abstract class ServiceJob { //配置對象 private ServiceConfig mConfigObject; //下次運行時間 private DateTime mNextTime; //任務是否在運行中 protected bool mIsRunning; /// <summary> /// 構造函數 /// </summary> public ServiceJob() { //變量初始化 this.mNextTime = DateTime.Now; this.mIsRunning = false; } /// <summary> /// 配置對象 /// </summary> public ServiceConfig ConfigObject { get { return this.mConfigObject; } set { this.mConfigObject = value; } } /// <summary> /// 開始工作 /// </summary> public void StartJob() { if (this.mConfigObject != null && this.mNextTime != null) { if (this.mConfigObject.Enabled.ToLower() == "true") { if (DateTime.Now >= this.mNextTime) { if (!this.mIsRunning) { this.mNextTime = DateTime.Now.AddSeconds((double)this.mConfigObject.Interval); this.Start(); } } } } } /// <summary> /// 停止工作 /// </summary> public void StopJob() { this.mConfigObject = null; this.mNextTime = DateTime.Now; this.mIsRunning = false; this.Stop(); } #region 子類必需實現的抽象成員 /// <summary> /// 開始工作 /// </summary> protected abstract void Start(); /// <summary> /// 停止工作 /// </summary> protected abstract void Stop(); #endregion } }
工具類ServiceTools
工具類實現了IConfigurationSectionHandler接口,封裝了對app.config的讀取、日志生成等靜態方法。
using System; using System.Collections.Specialized; using System.Configuration; using System.Xml; using System.IO; namespace WuJian.WindowsServiceDemo.Base { /// <summary> /// 工具類 /// </summary> public class ServiceTools : System.Configuration.IConfigurationSectionHandler { /// <summary> /// 獲取AppSettings節點值 /// </summary> /// <param name="key"></param> /// <returns></returns> public static string GetAppSetting(string key) { return ConfigurationManager.AppSettings[key].ToString(); } /// <summary> /// 獲取configSections節點 /// </summary> /// <returns></returns> public static XmlNode GetConfigSections() { XmlDocument doc = new XmlDocument(); doc.Load(ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).FilePath); return doc.DocumentElement.FirstChild; } /// <summary> /// 獲取section節點 /// </summary> /// <param name="nodeName"></param> /// <returns></returns> public static NameValueCollection GetSection(string nodeName) { return (NameValueCollection)ConfigurationManager.GetSection(nodeName); } /// <summary> /// 停止Windows服務 /// </summary> /// <param name="serviceName">服務名稱</param> public static void WindowsServiceStop(string serviceName) { System.ServiceProcess.ServiceController control = new System.ServiceProcess.ServiceController(serviceName); control.Stop(); control.Dispose(); } /// <summary> /// 寫日志 /// </summary> /// <param name="path">日志文件</param> /// <param name="cont">日志內容</param> /// <param name="isAppend">是否追加方式</param> public static void WriteLog(string path, string cont, bool isAppend) { using (StreamWriter sw = new StreamWriter(path, isAppend, System.Text.Encoding.UTF8)) { sw.WriteLine(DateTime.Now); sw.WriteLine(cont); sw.WriteLine(""); sw.Close(); } } /// <summary> /// 實現接口以讀寫app.config /// </summary> /// <param name="parent"></param> /// <param name="configContext"></param> /// <param name="section"></param> /// <returns></returns> public object Create(object parent, object configContext, System.Xml.XmlNode section) { System.Configuration.NameValueSectionHandler handler = new System.Configuration.NameValueSectionHandler(); return handler.Create(parent, configContext, section); } }//end class }
框架代碼
首先把所有任務都放進內存(Hashtable),這個過程使用了反射。然后使用托管的線程池執行多任務,如下代碼所示:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.ServiceProcess; using System.Text; using System.IO; using System.Collections; using System.Collections.Specialized; using System.Xml; using System.Reflection; using System.Threading; namespace WuJian.WindowsServiceDemo { public partial class Service1 : ServiceBase { //用哈希表存放任務項 private Hashtable hashJobs; public Service1() { InitializeComponent(); } protected override void OnStart(string[] args) { //啟動服務 this.runJobs(); } protected override void OnStop() { //停止服務 this.stopJobs(); } #region 自定義方法 private void runJobs() { try { //加載工作項 if (this.hashJobs == null) { hashJobs = new Hashtable(); //獲取configSections節點 XmlNode configSections = Base.ServiceTools.GetConfigSections(); foreach (XmlNode section in configSections) { //過濾注釋節點(如section中還包含其它節點需過濾) if (section.Name.ToLower() == "section") { //創建每個節點的配置對象 string sectionName = section.Attributes["name"].Value.Trim(); string sectionType = section.Attributes["type"].Value.Trim(); //程序集名稱 string assemblyName = sectionType.Split(',')[1]; //完整類名 string classFullName = assemblyName + ".Jobs." + sectionName + ".Config"; //創建配置對象 Base.ServiceConfig config = (Base.ServiceConfig)Assembly.Load(assemblyName).CreateInstance(classFullName); //創建工作對象 Base.ServiceJob job = (Base.ServiceJob)Assembly.Load(config.Assembly.Split(',')[1]).CreateInstance(config.Assembly.Split(',')[0]); job.ConfigObject = config; //將工作對象加載進HashTable this.hashJobs.Add(sectionName, job); } } } //執行工作項 if (this.hashJobs.Keys.Count > 0) { foreach (Base.ServiceJob job in hashJobs.Values) { //插入一個新的請求到線程池 if (System.Threading.ThreadPool.QueueUserWorkItem(threadCallBack, job)) { //方法成功排入隊列 } else { //失敗 } } } } catch (Exception error) { Base.ServiceTools.WriteLog(Base.ServiceTools.GetAppSetting("LOG_PATH") + "Error.txt", error.ToString(), true); } } private void stopJobs() { //停止 if (this.hashJobs != null) { this.hashJobs.Clear(); } } /// <summary> /// 線程池回調方法 /// </summary> /// <param name="state"></param> private void threadCallBack(Object state) { while (true) { ((Base.ServiceJob)state).StartJob(); //休眠1秒 Thread.Sleep(1000); } } #endregion }//end class }
配置文件
示例中定義了兩項任務Job1與Job2,為簡單演示,這兩項任務分別每隔5秒與10秒寫一次文本文件。
首先在configSections中添加自定義節點,然后在自定義節點中配置任務的基本屬性,為方便擴展,利用反射獲取assembly屬性來創建任務對象。
<?xml version="1.0"?> <configuration> <configSections> <!--自定義工作項,name屬性請與Jobs下的任務目錄同名,會據此加載該任務的config對象--> <section name="Job1" type="WuJian.WindowsServiceDemo.Base.ServiceTools,WuJian.WindowsServiceDemo"/> <section name="Job2" type="WuJian.WindowsServiceDemo.Base.ServiceTools,WuJian.WindowsServiceDemo"/> </configSections> <Job1> <add key="description" value="任務一" /> <add key="enabled" value="true" /> <add key="assembly" value="WuJian.WindowsServiceDemo.Jobs.Job1.Job,WuJian.WindowsServiceDemo" /> <add key="interval" value="5" /> </Job1> <Job2> <add key="description" value="任務二" /> <add key="enabled" value="true" /> <add key="assembly" value="WuJian.WindowsServiceDemo.Jobs.Job2.Job,WuJian.WindowsServiceDemo" /> <add key="interval" value="10" /> </Job2> <appSettings> <!--日志路徑--> <add key="LOG_PATH" value="E:\Study\WindowsServiceDemo\Logs\" /> </appSettings> </configuration>
安裝
在Service1設計模式點擊鼠標右鍵,選擇“添加安裝程序”
設置服務的基本屬性,包括執行權限,在WMI中的名稱、備注、啟動方式等。
最后執行installUtil命令在Windows中安裝部署服務程序。為使用方便,編寫了兩個Bat文件以快速安裝和卸載服務。
Install.bat用於安裝服務
%systemroot%\microsoft.net\framework\v4.0.30319\installUtil.exe WuJian.WindowsServiceDemo.exe
pause
UnInstall.bat用於卸載服務
%systemroot%\microsoft.net\framework\v4.0.30319\installUtil.exe WuJian.WindowsServiceDemo.exe /u pause
執行Install.bat后就可以在Windows服務WMI中看到了,所下圖所示:
DEMO
開發運行環境:.Net Framework 4.0、Visual Studio 2010
如下圖所示,任務一生成job1.txt,每5秒記錄一次時間;任務二生成job2.txt,每10秒記錄一次時間。
<全文完>
作者:吳劍
出處:http://www.cnblogs.com/wu-jian/
本文版權歸作者和博客園共有,歡迎轉載,但必需注明出處,並且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。