多线程、方便扩展的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

点击下载DEMO

多线程、方便扩展的Windows服务程序框架的更多相关文章

  1. 使用windows api安装windows服务程序(C#)

    3个步骤: 1.安装器代码编写 2.安装器工具类编写 1)安装.启动服务) 2)卸载服务 3.windows服务程序编写(参考:多线程.方便扩展的Windows服务程序框架) 4.代码下载,在文末(注 ...

  2. .NET 跨平台RPC框架DotNettyRPC Web后台快速开发框架(.NET Core) EasyWcf------无需配置,无需引用,动态绑定,轻松使用 C# .NET 0配置使用Wcf(半成品) C# .NET Socket 简单实用框架 C# .NET 0命令行安装Windows服务程序

    .NET 跨平台RPC框架DotNettyRPC   DotNettyRPC 1.简介 DotNettyRPC是一个基于DotNetty的跨平台RPC框架,支持.NET45以及.NET Standar ...

  3. Java多线程系列--“JUC锁”01之 框架

    本章,我们介绍锁的架构:后面的章节将会对它们逐个进行分析介绍.目录如下:01. Java多线程系列--“JUC锁”01之 框架02. Java多线程系列--“JUC锁”02之 互斥锁Reentrant ...

  4. Silverlight实例教程 - 自定义扩展Validation类,验证框架的总结和建议(转载)

    Silverlight 4 Validation验证实例系列 Silverlight实例教程 - Validation数据验证开篇 Silverlight实例教程 - Validation数据验证基础 ...

  5. Windows Forms框架编程

    <Windows Forms框架编程>节选   第九章 设计模式与原则 软件设计模式(Design pattern)是一套被反复使用的代码设计经验总结.使用设计模式是为了可重用代码.让代码 ...

  6. 用Visual C#创建Windows服务程序

    一.Windows服务介绍: Windows服务以前被称作NT服务,是一些运行在Windows NT.Windows 2000和Windows XP等操作系统下用户环境以外的程序.在以前,编写Wind ...

  7. [翻译].NET Core 3 Preview1和Windows桌面框架开源

    原文来自TechViews 今天,我们宣布推出.NET Core 3 Preview 1.这是.NET Core 3的第一个公开发布.我们有一些令人兴奋的新功能可供分享,并希望得到您的反馈.您可以使用 ...

  8. 开发WINDOWS服务程序

    开发WINDOWS服务程序 开发步骤: 1.New->Other->Service Application 2.现在一个服务程序的框架已经搭起来了,打开Service1窗口,有几个属性说明 ...

  9. 用C/C++创建windows服务程序

    转载:https://blog.csdn.net/chenyujing1234/article/details/8023816 一.演示过程下方代码演示了如何使用vs(C/C++)创建windows服 ...

随机推荐

  1. 使用 cacti 批量监控服务器以及其 PHP 运作环境配置

    http://www.ibm.com/developerworks/cn/linux/l-cn-cacti/ http://www.360doc.com/content/12/0711/22/1465 ...

  2. Python读取word文档(python-docx包)

    最近想统计word文档中的一些信息,人工统计的话...三天三夜吧 python 不愧是万能语言,发现有一个包叫做 docx,非常好用,具体查看官方文档:https://python-docx.read ...

  3. 一次 read by other session 的处理过程

     一个哥们给我打电话.他说系统中一直出现等待事件 read by other session .而且该等待都是同一个sql引起的.比較紧急,请我帮忙远程看看. 远程过去之后,用脚本把 等待事件给抓 ...

  4. 解决from lxml import etree 导入的时候,显示etree不存在

    问题: 当安装完lxml之后,发现使用 from lxml import etree  时,etree不可用 原因 :是lxml中没有etree包 解决: 去官网下载对应包:官网地址:http://l ...

  5. centos7.0 crontab 的yii计划任务没有执行

    */1 * * * * /www/yii solr/update-article 创建了每分钟执行一次的计划而计划任务没有执行 原因是自己少加了执行用户 */1 * * * * php /www/yi ...

  6. poj1066(叉乘的简单应用)

    做完了才发现,好像没有人和我的做法一样的,不过我怎么都觉得我的做法还是挺容易想的. 我的做法是: 把周围的方框按顺时针编号,然后对于每一条边,如果点出现在边的一侧,则把另一侧所有的点加1,这样最后统计 ...

  7. 【BZOJ4820】[Sdoi2017]硬币游戏 AC自动机+概率DP+高斯消元

    [BZOJ4820][Sdoi2017]硬币游戏 Description 周末同学们非常无聊,有人提议,咱们扔硬币玩吧,谁扔的硬币正面次数多谁胜利.大家纷纷觉得这个游戏非常符合同学们的特色,但只是扔硬 ...

  8. 自然常数e的神奇之美

  9. 九度OJ 1184:二叉树遍历 (二叉树)

    时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:3515 解决:1400 题目描述: 编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储). 例如如下的 ...

  10. Difference Between ZIP and GZIP

    From: http://www.differencebetween.net/technology/difference-between-zip-and-gzip/ Summary: 1. GZIP ...