Windows Service,也称Windows服务,是32位Windows操作系统中一种长期运行的后台程序。它们长期后台运行,没有用户界面,默默无闻,但它们却是支持Windows正常运行的幕后英雄,却永无出头之日。
Windows服务程序为其他系统模块提供了非常重要的服务,而且各个Windows服务分工明确,比如IISAdmin服务提供WEB内容的发布功能,若IISAdmin服务不启动,则静态HTML页面、ASP、ASP.NET或者WebService等等统统不行;有个名为“Print Spooler”的服务用于提供打印支持,若该服务不启动,则任何软件都不能进行打印,比如Word,记事本或者报表软件等等。
Windows启动后在没有用户登录时就会启动Windows服务。Windows NT和Windows2000,以及更新的版本操作系统能运行Windows服务,但Windows98及其前期版本是不能运行服务的。
我们打开Windows资源管理器,在左边的树状列表中选中“桌面-控制面板-管理工具”。
在右边的列表中打开“服务”项目即可打开Windows服务管理器。
在这些服务中,有我们最熟悉的IIS Admin和World Wide Web Publishing服务了。我们双击一个服务项目即可打开服务属性对话框。
Windows服务有一个服务名称属性,该属性是服务的惟一的不可重复的名称,我们可以在命令行中使用命令“net start 服务名称”来启动服务,使用“net stop 服务名称”来停止服务。
Windows服务的启动类型有自动,手动和已禁用。当启动类型为自动时,Windows启动后不等用户登录就自动启动服务,当启动类型为手动时,需要某个操作员登录后点击这里的“启动”按钮来启动服务,而当启动类型为已禁用时,Windows服务不能启动。
该页面中的“启动”按钮用于启动尚未启动的Windows服务,运行提供服务的进程;“停止”按钮用于停止已经启动的服务,杀死服务进程;而“暂停”按钮用于通知服务进程暂时停止提供服务,但服务进程依然存在;而“恢复”按钮用于通知处于暂停模式的服务进程重新提供服务。
我们可以查看服务属性对话框的“登录”页面。
可以指定服务使用本地系统帐户登录,也可另外指定其他的用户,这里有一个允许服务和桌面交互的选项,若选中此选项,则Windows服务可以显示图形化用户界面,比如显示自己的窗体,显示消息框等等。不过不建议使用该选项,而且Windows服务运行时不要显示图形化用户界面。
我们切换到“依存关系”页面,可以看到本服务和其他服务的依存关系。
各个Windows服务之间可能存在依赖关系,比如IISADMIN服务就依赖另外一个名为RPC的Windows服务,当启动一个Windows服务时,系统会启动该服务所依赖的其他Windows服务。例如我们设置IISADMIN服务为自动启动,而RPC服务为手动启动,则Windows启动后会试图自动启动IISADMIN服务,结果会首先启动RPC服务,即使RPC服务不是自动启动。若RPC服务为禁止,无论如何也不能启动,则IISADMIN服务就无法自动启动了。
编写Windows服务是一种比较高级的编程技术,内部使用了很多Windows操作系统的核心功能,但微软.NET框架已经很好的封装了这些技术细节,使得我们可以很方便的使用C#编写自己的Windows服务,其基本过程一般为
1. 创建C#工程。创建一个EXE工程,可以是WinForm或者命令行格式的。添加对System.ServiceProcess.dll和System.Configuration.Install.dll的引用。
2. 创建服务类。新增一个类,该类型继承System.ServiceProcess.ServiceBase类型,在构造函数中设置ServiceName属性来指明该服务的名称。然后重载它的OnStart方法来响应启动服务的事件,重载OnStop方法来响应停止服务的事件,重载OnPause方法来响应暂停服务的事件,重载OnContinue方法来响应恢复提供服务的事件。在重载这些方法时必须要立即返回,其中不能出现长时间的操作,若处理时间过长则Windows服务管理器会觉得该Windows服务停止响应而报错。为此我们可以使用一个线程来进行实际的工作,而OnStart方法创建线程,OnStop方法关闭线程,OnPause方法挂起线程,而OnContinue方法来恢复运行线程。
3. 启动服务。在main函数中调用“System.ServiceProcess.ServiceBase.Run( 自定义服务类的实例 )”来运行服务。比如“System.ServiceProcess.ServiceBase.Run( new MyService() )”,这里的MyService就是继承自ServiceBase。
4. 安装服务。新增一个类,该类型继承自System.Configuration.Install.Installer类型,该类型用于配合微软.NET框架自带的安装命令行工具InstallUtil.exe的。我们为该类型附加System.ComponentModel.RunInstallerAttribute特性,并在它的构造函数中使用System.ServiceProcess.ServiceInstaller对象和System.ServiceProcess.ServiceProcessInstaller对象向系统提供该服务的安装信息。程序编译后我们可以使用命令行“InstallUtil.exe EXE文件名”向Windows服务管理器注册服务,可以使用命令行“InstallUtil.exe/uEXE文件名”从Windows服务管理器中注销服务。
5. 编写服务客户端。这是一个根据实际情况而可选的过程,由于Windows服务是没有用户界面的,因此我们可以编写一个具有用户界面的程序来显示和控制Windows服务提供的数据,并进行一些系统设置等操作。比如对于MS SQL Server,数据库引擎是以服务的形式存在,而SQL Server企业管理器就是一个客户端软件。
现在我们要求使用C#和VS.NET2005开发一个软件,该软件功能为
1. 该软件能监视指定目录下的文件和子目录的新增,修改,删除和重命名操作,并将操作日志记录到一个数据库中。
2. 该软件以Windows服务的形式运行,能监视不同的用户帐户的操作记录。
3. 有一个客户端软件能控制服务,并能查看服务的保存的监视记录。其用户界面为
客户端软件还能设置服务监视的目录,系统设置对话框为
一般而言,我们将服务和客户端分成两个C#工程开发,但这里为了方便我们只在一个工程中实现服务器和客户端软件的开发。Windows服务是不能显示图形化用户界面的,但并不是说Windows服务的软件中不能包含显示图形化用户界面的软件模块。我们完全可以编写一个EXE,其中包含服务器和客户端两个相互独立的软件模块。直接执行EXE将以服务模式运行,若带有命令行参数将以客户端模式运行。为此我们设计了如下的命令行参数
命令行参数 |
功能 |
无任何参数 |
以服务模式运行,调用ServiceBase.Run函数来运行服务。 |
/install |
调用InstallUtil.exe安装服务,将EXE自己注册到Windows服务管理器中。 |
/uninstall |
调用InstallUtil.exe卸载服务。 |
/client |
以客户端模式运行,显示图形用户界面。 |
/debug |
以调试模式下运行,方便VS.NET对服务的操作过程提供调试。 |
使用VS.NET调试服务是一个比较麻烦的事,首先我们得安装并运行服务,然后使用VS.NET的菜单项目“工具-附加到进程”的操作来附加到服务程序,然后设置断点进行调试,其中OnStart函数是没有办法设置断点调试的。为此我们专门添加一个“/debug”命令行参数使得程序不进入服务模式,而是直接运行提供服务内容的功能性代码,然后主线程休眠,但功能性代码还在运行,可以调试。这样我们在VS.NET中设置断点后可以直接运行进行调试了。
这里我们设计的C#工程名称为MyWindowsService,编译生成的文件为MyWindowsService.exe。
在本软件中,数据将保存到应用程序目录下的一个名为FileSystemWatcher.mdb的Access2000格式的数据库。数据库中的表结构为
文件系统操作日志表 FileSystemLog,字段有
字段名 |
类型 |
说明 |
RecordID |
文本(50) |
记录编号 |
WatchTime |
文本(20) |
记录时间,为yyyy-MM-dd HH:mm:ss格式 |
ObjectName |
文本(250) |
对象名称 |
EventStyle |
文本(50) |
事件类型 |
该数据表中保存的数据范例为
RecordID |
WatchTime |
ObjectName |
EventStyle |
0d4e0d9a-6826-415b-bd47-c86fbb1449b0 |
2008-10-02 15:31:27 |
c:"aaaaaa.txt |
Renamed |
22c1df6d-4f94-488c-a705-e8024d875213 |
2008-10-02 20:37:03 |
d:"aa.png |
Renamed |
27632fe8-6cbf-4a41-95ad-6ab2e8222192 |
2008-10-02 20:40:56 |
c:"a.pdf |
Created |
48403266-0150-44c8-8efa-169f7a68bcb4 |
2008-10-03 11:02:04 |
c:"zzzzzz.bmp |
Renamed |
6c3b603a-f43b-415c-8122-4aa23376d575 |
2008-10-02 11:26:57 |
c:"SDC_2008_10_2.log |
Changed |
6fb9fad1-51f5-40b2-b05b-d0628f775a3c |
2008-10-02 15:31:52 |
c:"aaaaaa.txt |
Deleted |
735d74e6-1548-4d7d-9048-ab75dd1c5874 |
2008-10-02 20:31:27 |
c:"aa.bmp |
Renamed |
7b36a079-c56c-48f7-9c6e-cf0d77b9c6c1 |
2008-10-02 11:27:12 |
c:"SDC_2008_10_2.log |
Changed |
7c2672ac-b210-4eca-9277-2505030e72e5 |
2008-10-02 20:39:12 |
d:"aa.png |
Deleted |
9ab95c19-ccd0-43eb-89ec-3930ebec9a8d |
2008-10-02 21:55:57 |
c:"b.bmp |
Renamed |
9adb5696-fb6a-497e-b4ff-06f5da896434 |
2008-10-02 20:39:12 |
d:"1.png |
Deleted |
9f4d702f-57c1-46ec-a827-701c2a15ee81 |
2008-10-02 23:59:04 |
c:"新建文件夹 |
Created |
c163fa48-f5ea-49b1-95c9-b89f9ee622e5 |
2008-10-02 11:26:42 |
c:"新建 文本文档.txt |
Created |
对于新增文件或目录操作其EventStyle值为Created,对于修改为Changed,对于删除为Deleted,对于重命名为Renamed。
系统设置信息表 SystemConfig,字段有
字段名 |
类型 |
说明 |
ConfigName |
文本(50) |
系统配置名称 |
ConfigValue |
文本(250) |
配置数据 |
该数据表中保存的数据的范例为
ConfigName |
ConfigValue |
LogChanged |
True |
LogCreated |
True |
LogDeleted |
True |
LogRenamed |
False |
path0 |
c:"*.txt |
path1 |
d:" |
在这里配置项LogChanged表示是否监视文件内容是否被改变事件,配置项LogCreated表示是否监视新建文件或目录事件,配置项LogDeleted表示是否监视文件或目录删除事件,配置项LogRenamed表示是否监视文件或目录重命名事件。而path0,path1,path2等表示监视的路径,支持通配符。系统配置中可以有若干个path配置项。
我们可以使用System.IO.FileSystemWatcher来监视文件系统的对象的修改,我们可以使用它的Path属性来设置要监视的文件夹,使用Filter属性来设置文件名过滤器,然后响应它的Changed事件来处理文件内容修改操作,响应Created事件来处理新增文件或目录操作,响应Deleted事件来处理删除文件或目录操作,响应Renamed事件来处理文件和目录重命名操作。这这里我们简单是将这些事件信息保存到数据表FileSystemLog中。程序在监视文件系统前会读取系统配置信息表SystemConfig中读取配置信息,根据其中的path配置项目创建若干个FileSystemWatcher对象展开监视。
我们选定服务的名称为“MyFileSystemWatcher”。
本软件的客户端具有一个图形化用户界面,其界面设计如下
客户端主窗体 |
工具条:刷新 删除记录 系统配置 启动服务 停止服务 |
数据列表 显示从 FileSystemLog表查询所得的数据 |
状态栏 |
此外还有一个系统配置对话框,用于查看和修改数据表SystemConfig中保存的系统配置信息。
根据上述的软件设计,我已经把软件编写完毕,现对该软件结构进行说明
本软件为一个C#编写的EXE,主要包含服务端软件模块和客户端软件模块。首先对比较好理解的具有图形化用户界面的客户端模块进行说明,客户端的主界面为
这个界面主要功能是数据库信息管理,最上面为一个工具条,中间大部分的一个ListView控件,最下面为状态栏。
对于ListView控件其内容是分组的,因此需要设计其分组信息,在VS.NET的窗体设计器中我们点中ListView控件,在旁边的属性列表中选择Groups属性,点击旁边的小按钮可以弹出分组设计器。
使用这个分组编辑器我们可以很容易的设计该ListView控件的分组信息。
这个窗体的加载事件处理为
/// <summary> /// 服务已经安装标记 /// </summary> private bool bolServiceInstalled = false ; private void frmViewLog_Load(object sender, EventArgs e) { try { System.ServiceProcess.ServiceController control = new System.ServiceProcess.ServiceController("MyFileSystemWatcher"); System.ServiceProcess.ServiceControllerStatus status = control.Status; control.Dispose(); bolServiceInstalled = true; } catch( Exception ext ) { lblServiceInstall.Text = "服务尚未安装" ; myTimer.Enabled = false ; btnStartService.Enabled = false ; btnStopService.Enabled = false ; bolServiceInstalled = false ; MessageBox.Show( this,"服务尚未安装:" + ext.Message , "系统错误"); } this.btnRefresh_Click(null, null); } |
在这里我们首先创建一个联系到文件监视服务的ServiceController,调用它的Status属性,若一切正常则表示服务已经安装,我们设置bolServiceInstalled的标志变量,若发生错误则服务尚未安装,则显示“服务尚未安装”的提示信息。
对于工具条的“刷新列表”按钮,其点击事件处理为
private void btnRefresh_Click(object sender, EventArgs e) { this.Cursor = System.Windows.Forms.Cursors.WaitCursor; lvwRecord.BeginUpdate(); try { lvwRecord.Items.Clear(); using (System.Data.IDbCommand cmd = Util.DBConnection.CreateCommand()) { cmd.CommandText = "Select RecordID , ObjectName , WatchTime , EventStyle From FileSystemLog order by WatchTime"; System.Data.IDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { ListViewItem NewItem = new ListViewItem(); NewItem.Tag = Convert.ToString(reader.GetValue(0)); NewItem.Text = Convert.ToString(reader.GetValue(1)); NewItem.SubItems.Add(Convert.ToString(reader.GetValue(2))); string Style = Convert.ToString(reader.GetValue(3)); NewItem.SubItems.Add(Style); Style = Style.Trim().ToLower(); if (Style == "created") { NewItem.Group = lvwRecord.Groups[0]; NewItem.ImageIndex = 0; } else if (Style == "changed") { NewItem.Group = lvwRecord.Groups[1]; NewItem.ImageIndex = 1; } else if (Style == "deleted") { NewItem.Group = lvwRecord.Groups[2]; NewItem.ImageIndex = 2; } else if (Style == "renamed") { NewItem.Group = lvwRecord.Groups[3]; NewItem.ImageIndex = 3; } NewItem.StateImageIndex = NewItem.ImageIndex; lvwRecord.Items.Add(NewItem); } reader.Close(); } myStatus.Text = "共列出 " + lvwRecord.Items.Count + " 个记录"; } catch (Exception ext) { MessageBox.Show(ext.ToString(), "系统错误"); } this.Cursor = System.Windows.Forms.Cursors.Default; lvwRecord.EndUpdate(); } |
在该按钮事件处理中,我们查询数据表FileSystemLog,对每一条查询所得的数据创建一个ListViewItem项目,并根据记录的EventStyle值设置该列表项目的图标序号和分组状态。
工具条的“删除记录”按钮用于删除列表中选择的项目,其点击事件处理为
private void btnDelete_Click(object sender, EventArgs e) { if (lvwRecord.SelectedItems.Count > 0) { using (System.Data.IDbCommand cmd = Util.DBConnection.CreateCommand()) { for (int iCount = lvwRecord.Items.Count - 1; iCount >= 0; iCount--) { ListViewItem item = lvwRecord.Items[iCount]; if (item.Selected) { cmd.CommandText = "Delete From FileSystemLog Where RecordID = '" + item.Tag + "'"; cmd.ExecuteNonQuery(); lvwRecord.Items.Remove(item); } }//for }//using } } |
在刷新列表中,我们将列表项目的Tag属性值设置为数据库记录的编号,在这里我们利用这个事先保存的数据库记录的编号拼凑出SQL语句然后删除指定的记录。
工具条的“启动服务”按钮用于启动后台的文件监视服务。其点击事件处理为
private void btnStartService_Click(object sender, EventArgs e) { if (bolServiceInstalled == false) return; using (System.ServiceProcess.ServiceController control = new System.ServiceProcess.ServiceController("MyFileSystemWatcher")) { if (control.Status == System.ServiceProcess.ServiceControllerStatus.Stopped) { control.Start(); } } } |
在这里我们创建一个ServiceController对象,若判断出服务的状态为停止,则调用控制器的Start方法来启动服务,在这里Start方法内部只是通知操作系统启动指定名称的服务,它发送通知后立即返回,并不会等待服务启动后返回。
类似的对于“停止服务”,其点击事件处理为
private void btnStopService_Click(object sender, EventArgs e) { if (bolServiceInstalled == false) return; using (System.ServiceProcess.ServiceController control = new System.ServiceProcess.ServiceController("MyFileSystemWatcher")) { if (control.Status == System.ServiceProcess.ServiceControllerStatus.Running) { control.Stop(); } } } |
在这个处理过程中,若判断出服务状态为运行中,则调用控制器的Stop方法来停止服务。在这里Stop方法内部只是通知操作系统停止指定的服务,它发送通知后立即返回,不会等待服务停止后返回。
我们还在窗体上放置一个定时器控件,定时间隔为2秒,用于根据服务的状态刷新工具条按钮状态,其定时事件处理为
private void myTimer_Tick(object sender, EventArgs e) { if (bolServiceInstalled == false) return; using (System.ServiceProcess.ServiceController control = new System.ServiceProcess.ServiceController("MyFileSystemWatcher")) { btnStartService.Enabled = (control.Status == System.ServiceProcess.ServiceControllerStatus.Stopped); btnStopService.Enabled = (control.Status == System.ServiceProcess.ServiceControllerStatus.Running); } } |
在这里我们创建了一个绑定到文件系统监控服务的ServiceController对象,然后根据它的Status状态来设置“启动服务”和“停止服务”按钮的可用状态。
在客户端主窗体中点击工具条的“系统配置”按钮就会弹出系统设置对话框,该对话框的用户界面为
该对话框比较简单,就是用于显示和修改系统配置信息对象MyConfig中的内容。由于文件系统监视服务只有在启动的时候读取系统配置信息,因此对系统配置的任何修改都需要重新启动服务才能生效。
系统配置信息对象MyConfig用于读取和修改保存在数据表SystemConfig中的系统配置信息。其包含的配置信息的代码如下
private bool bolLogRenamed = true; /// <summary> /// 是否记录重命名事件 /// </summary> public bool LogRenamed { get { return bolLogRenamed; } set { bolLogRenamed = value; } } private bool bolLogChanged = true; /// <summary> /// 是否记录文件修改事件 /// </summary> public bool LogChanged { get { return bolLogChanged; } set { bolLogChanged = value; } } private bool bolLogCreated = true; /// <summary> /// 是否记录对象创建事件 /// </summary> public bool LogCreated { get { return bolLogCreated; } set { bolLogCreated = value; } } private bool bolLogDeleted = true; /// <summary> /// 是否记录对象删除事件 /// </summary> public bool LogDeleted { get { return bolLogDeleted; } set { bolLogDeleted = value; } } private string[] myWatchedPaths = null; /// <summary> /// 监视的目录 /// </summary> public string[] WatchedPaths { get { return myWatchedPaths; } set { myWatchedPaths = value; } } |
它的Load方法用于从数据库中加载配置信息,其处理过程为
public void Load() { myWatchedPaths = null; System.Collections.ArrayList paths = new System.Collections.ArrayList(); using (System.Data.IDbCommand cmd = Util.DBConnection.CreateCommand()) { cmd.CommandText = "Select ConfigName , ConfigValue From SystemConfig"; System.Data.IDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { string Name = Convert.ToString(reader.GetValue(0)); if (Name == null) { continue; } Name = Name.Trim().ToLower(); string Value = Convert.ToString(reader.GetValue(1)); if (Name.StartsWith("path")) { paths.Add(Value.Trim()); } else if (Name == "logrenamed") { bolLogRenamed = Convert.ToBoolean(Value); } else if (Name == "logchanged") { bolLogChanged = Convert.ToBoolean(Value); } else if (Name == "logdeleted") { bolLogDeleted = Convert.ToBoolean(Value); } else if (Name == "logcreated") { bolLogCreated = Convert.ToBoolean(Value); } } } myWatchedPaths = (string[])paths.ToArray(typeof(string)); } |
在该方法中程序查询数据表SystemConfig中的配置项目名称和数据,若项目名称以“path”开头则为要监视的路径,而配置项logrenamed,logchanged,logdeleted,logcreated分别表示是否监视文件目录重命名,修改,删除和新建等操作。
MyConfig对象还有一个Save方法用于将系统配置信息保存到数据库中,其处理过程为
public void Save() { using (System.Data.IDbCommand cmd = Util.DBConnection.CreateCommand()) { cmd.CommandText = "Delete From SystemConfig"; cmd.ExecuteNonQuery(); cmd.CommandText = "Insert Into SystemConfig ( ConfigName , ConfigValue ) Values( ? , ? )" ; System.Data.IDbDataParameter pName = cmd.CreateParameter(); cmd.Parameters.Add( pName ); System.Data.IDbDataParameter pValue = cmd.CreateParameter(); cmd.Parameters.Add( pValue );
pName.Value = "LogRenamed"; pValue.Value = bolLogRenamed.ToString(); cmd.ExecuteNonQuery(); pName.Value = "LogChanged"; pValue.Value = bolLogChanged.ToString(); cmd.ExecuteNonQuery(); pName.Value = "LogDeleted"; pValue.Value = bolLogDeleted.ToString(); cmd.ExecuteNonQuery(); pName.Value = "LogCreated"; pValue.Value = bolLogCreated.ToString(); cmd.ExecuteNonQuery(); for (int iCount = 0; iCount < myWatchedPaths.Length; iCount++) { string path = myWatchedPaths[ iCount ] ; if( path == null || path.Trim().Length == 0 ) { continue ; } pName.Value = "path"+ iCount ; pValue.Value = path ; cmd.ExecuteNonQuery(); } } } |
在这个方法中,首先删除数据表SystemConfig中所有的记录,然后将所有的配置信息保存到数据表SystemConfig中。
类MyFileSystemWatcherService就是文件系统监视服务,它是从ServiceBase派生的,首先说明一下执行文件系统监视的功能性的过程,其代码如下
/// <summary> /// 文件系统监视器列表 /// </summary> private System.Collections.ArrayList myWatchers = null; /// <summary> /// 开始启动文件系统监视 /// </summary> /// <returns>操作是否成功</returns> internal bool StartFileSystemWatching() { myWatchers = new System.Collections.ArrayList(); MyConfig.Instance.Load(); string[] paths = MyConfig.Instance.WatchedPaths; System.Text.StringBuilder myPathList = new StringBuilder(); if (paths != null) { foreach (string path in paths) { if (System.IO.Path.IsPathRooted(path) == false) { continue; } string BasePath = null; string Filter = null; if (System.IO.Directory.Exists(path)) { BasePath = path; Filter = "*.*"; } else { BasePath = System.IO.Path.GetDirectoryName(path); Filter = System.IO.Path.GetFileName(path); } if (BasePath == null) { continue; } BasePath = BasePath.Trim(); if (BasePath.ToUpper().StartsWith(System.Windows.Forms.Application.StartupPath)) { // 不能监视程序本身所在的目录的文件系统更改 continue; } if (System.IO.Directory.Exists(BasePath) == false) { // 不能监视不存在的目录 continue; } if (myPathList.Length > 0) { myPathList.Append(";"); } myPathList.Append(path); System.IO.FileSystemWatcher watcher = new System.IO.FileSystemWatcher(); watcher.Path = BasePath; watcher.Filter = Filter; watcher.EnableRaisingEvents = true; watcher.IncludeSubdirectories = false; if (MyConfig.Instance.LogChanged) { watcher.Changed += delegate(object sender, System.IO.FileSystemEventArgs args) { WriteFileSystemLog(args.FullPath, args.ChangeType.ToString()); }; } if (MyConfig.Instance.LogCreated) { watcher.Created += delegate(object sender, System.IO.FileSystemEventArgs args) { WriteFileSystemLog(args.FullPath, args.ChangeType.ToString()); }; } if (MyConfig.Instance.LogDeleted) { watcher.Deleted += delegate(object sender, System.IO.FileSystemEventArgs args) { WriteFileSystemLog(args.FullPath, args.ChangeType.ToString()); }; } if (MyConfig.Instance.LogRenamed) { watcher.Renamed += delegate(object sender, System.IO.RenamedEventArgs args) { WriteFileSystemLog(args.FullPath, args.ChangeType.ToString()); }; } myWatchers.Add(watcher); }//foreach this.EventLog.WriteEntry( "开始监视文件系统 " + myPathList.ToString(), EventLogEntryType.Information); }//if return true; } |
在这个过程中,首先使用MyConfig.Load从数据库中加载系统配置,然后遍历所有需要监视的路径,对其中的每个路径解析出目录名和文件名,然后创建一个FileSystemWatcher对象,设置其Path和Filter属性,还根据MyConfig中的系统配置来绑定监视对象的Changed事件,Created事件,Deleted事件和Renamed事件,以实现对文件系统的监视。这里绑定事件的代码使用了C#2.0的匿名委托的语法功能。设置FileSystemWatcher对象后将该对象添加到文件系统监视器列表myWatchers中。
启动服务后使用EventLog.WriteEntry向Windows系统事件日志添加一些日志信息。
这里使用了一个WriteFileSystemLog方法,该方法代码为
private void WriteFileSystemLog(string ObjectName, string EventStyle ) { System.Data.IDbConnection conn = Util.DBConnection; if (conn == null) return; // 将监视结果添加到数据库中 using (System.Data.IDbCommand cmd = conn.CreateCommand()) { cmd.CommandText = "Insert Into FileSystemLog ( RecordID , WatchTime , ObjectName , EventStyle ) Values ( '" + System.Guid.NewGuid().ToString() + "' , '" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "' , ? , '" + EventStyle + "')" ; System.Data.IDbDataParameter p = cmd.CreateParameter(); p.Value = ObjectName; cmd.Parameters.Add(p); cmd.ExecuteNonQuery(); } } |
该方法参数是记录的文件或目录名,以及事件类型,程序首先拼凑出一个Insert的SQL语句,然后向数据表FileSystemLog添加一条数据。
类型MyFileSystemWatcherService还重载了ServiceBase的OnStart,OnStop,OnPause,OnContinue等方法来响应外界对服务过程的控制。
OnStart方法的代码如下,该方法调用StartFileSystemWatching函数就算完成了启动服务的操作。
protected override void OnStart(string[] args) { this.StartFileSystemWatching(); } |
OnStop方法的代码如下,该方法首先销毁掉所有正在运行的文件系统监视器,然后关闭数据库连接。
protected override void OnStop() { if (myWatchers != null) { foreach (System.IO.FileSystemWatcher w in myWatchers) { w.EnableRaisingEvents = false; w.Dispose(); } myWatchers = null; } Util.CloseDBConnection(); base.OnStop(); } |
OnPause方法代码如下,该方法设置所有的文件系统监视器不触发事件,这样软件不能感知文件系统的修改,因此也就暂停了对文件系统的监视。
protected override void OnPause() { if (myWatchers != null) { foreach (System.IO.FileSystemWatcher w in myWatchers) { w.EnableRaisingEvents = false; } } base.OnPause(); } |
OnContinue方法的代码如下,该方法重新设置所有的文件系统监视器能触发事件,因此软件又能监视文件系统的修改了。
protected override void OnContinue() { if (myWatchers != null) { foreach (System.IO.FileSystemWatcher w in myWatchers) { w.EnableRaisingEvents = true ; } } base.OnContinue(); } |
类型Util用于管理数据库连接,其代码为
private static System.Data.IDbConnection myDBConnection = null; /// <summary> /// 获得数据库连接对象 /// </summary> public static System.Data.IDbConnection DBConnection { get { if (myDBConnection == null) { myDBConnection = new System.Data.OleDb.OleDbConnection( "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=""" + System.IO.Path.Combine( System.Windows.Forms.Application.StartupPath, "FileSystemWatcher.mdb") + """"); myDBConnection.Open(); } return myDBConnection; } } /// <summary> /// 关闭数据库连接 /// </summary> public static void CloseDBConnection() { if (myDBConnection != null) { myDBConnection.Close(); myDBConnection = null; } } |
从这个代码可以看出软件使用的数据库是应用程序目录下的FileSystemWatcher.mdb数据库。为了提高效率,减少数据库的连接次数,服务在运行其间只连接一次数据库,使用完毕后不断开,只有退出软件时才断开数据库连接。
在类型Program中定义了Main函数,该函数就是本软件的启动入口方法。其代码为
[System.STAThread()] static void Main() { try { System.Uri uri = new Uri(typeof(string).Assembly.CodeBase); string RuntimePath = System.IO.Path.GetDirectoryName( uri.LocalPath ) ; string strInstallUtilPath = System.IO.Path.Combine(RuntimePath, "InstallUtil.exe"); foreach (string arg in System.Environment.GetCommandLineArgs()) { Console.WriteLine(arg); if (arg == "/install") { System.Diagnostics.Process.Start(strInstallUtilPath, """" + System.Windows.Forms.Application.ExecutablePath + """"); return; } else if (arg == "/uninstall") { System.Diagnostics.Process.Start(strInstallUtilPath, "/u """ + System.Windows.Forms.Application.ExecutablePath + """"); return; } else if (arg == "/client") { // 启动客户端 Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); using (frmClient frm = new frmClient()) { Application.Run(frm); //frm.ShowDialog(); Util.CloseDBConnection(); } return; } else if (arg == "/debug") { MyFileSystemWatcherService service = new MyFileSystemWatcherService(); service.StartFileSystemWatching(); System.Threading.Thread.Sleep(1000 * 600); return; } } } catch (Exception ext) { Console.WriteLine(ext.ToString()); return; } // 运行服务对象 ServiceBase.Run( new MyFileSystemWatcherService()); } |
Main函数决定调用本软件的那个功能模块,由于Main函数本身具有安装和卸载服务的功能,首先得找到微软.NET框架所带的InstallUtil.exe的完整的路径。微软.NET编程中,基础类型string属于mscorlib.dll,因此可以使用typeof(string).Assembly.CodeBase获得文件mscorlib.dll的绝对路径名,而InstallUtil.exe和mscorlib.dll是同一个目录的,因此也就能获得InstallUtil.exe的绝对路径名了。
我们使用System.Environment.GetCommandLineArgs()获得所有的命令行参数。遍历所有的参数,若存在“/install”则表示要安装服务,于是调用InstallUtil.exe来将软件本身注册为服务,若遇到“/uninstall”则调用InstallUtil.exe卸载服务,若遇到“/client”则调用客户端模块,若遇到“/debug”则创建服务对象,调用它的StartFileSystemWatching模拟启动服务,然后主线程阻塞掉,但此时文件系统监视的功能性模块还在运行,可以设置断点进行调试。
若没有遇到任何可识别的命令行参数,则调用ServiceBase.Run函数来执行服务。
由于向Windows系统注册自己为服务时没有指明任何命令行参数,因此服务管理器启动进程时不会添加任何命令行参数,因此本程序也就是以服务模式运行。若在Windows资源管理器中双击执行程序时也是以服务模式运行,此时没有相关的运行环境,程序启动后会报错。此时必须添加程序代码可识别的命令行参数。
程序编写完毕,编译通过,生成一个MyWindowsService.exe文件,我们就可以开始运行这个软件了。
首先我们得向系统注册服务,我们可以使用命令行“程序路径/MyWindowsService.exe /install”来注册服务,也可以直接运行“微软.NET框架路径/installutil.exe程序路径/MyWindowsService.exe”;相反的,我们可以使用命令行“程序路径/MyWindowsService.exe /uninstall”或者“微软.NET框架路径/installutil.exe/u程序路径/MyWindowsService.exe”来卸载服务。
安装服务后,我们可以使用命令行“程序路径/MyWindowsService.exe /client”来运行该服务的客户端软件了。
在本课程中,我们使用C#编写了一个简单的用于监视文件系统的Windows服务,包括服务器软件和客户端软件,若使用传统的C++开发服务这种底层程序需要熟悉大量的API函数,而微软.NET框架很好的封装了这些技术细节,简化了编程过程,使得我们可以把主要警力放在提供服务内容的功能性模块的开发上来,从这里可以看出基于微软.NET框架是可以低成本的开发出一些功能强大的软件。