多线程、方便扩展的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中看到了,所下图所示:
执行结果
如下图所示,任务一生成job1.txt,每5秒记录一次时间;任务二生成job2.txt,每10秒记录一次时间。
DEMO下载
开发运行环境:.Net Framework 4.0、Visual Studio 2010