使用Windows服务实现投票结果的自动发送功能

摘要: 在使用 Microsoft Visual Studio .NET 2003 设计投票系统时,我们希望投票系统能够提供一些扩展功能,比如除了正确地完成投票的各个事务外,还能够将投票结果自动发送给投票发起人或管理员。本文讨论了投票结果自动发送功能的设计、实现,以及在此过程中需要注意的问题

关键字: Microsoft Visual Studio .NET 2003 、 Borland C++ Builder 、 Windows 服务、投票系统、自动发送

一、 总体分析

首先来看看投票系统的工作场景,以便对设计与实现中的问题进行解答。假设现有一个使用 Microsoft Visual Studio .NET 2003 开发的基于 Web 页面的投票子系统,某公司某部门要对年终评优进行一次投票,每个员工在登录该投票子系统以后就可以进行投票。管理员在创建该投票项目时指定投票的标题和内容、可选项目、截止时间以及管理员的邮箱地址。在服务器时间到达管理员指定的投票项目截止时间后,员工将无法通过 Web 页面进行投票,同时服务器将投票结果以电子邮件的形式发送给管理员。

基于上述的工作场景,我们可以了解到,使用 Web 应用程序实现投票系统 [1] 的基本处理逻辑并不困难,但是要实现结果的自动发送功能,就不能只依靠 Web 应用程序。从投票系统的工作情况来看,处理新投票项目的添加、删除、修改,以及投票、通过页面查看投票结果等功能都可以直接交给 B/S 模式的 Web 应用程序来完成,客户端只要向 Web 服务器提出请求就可以完成相应的处理,在完成客户端的请求后,服务器会将处理结果反馈给客户端;然而投票结果的自动发送功能则不一样,它需要这样一种工作方式:长时间驻留内存,时刻监视着系统时间是否已经到达管理员所指定的投票项目截止时间,如果符合条件,就会自动地把该投票的结果发送到指定的邮箱地址。

显然, Web 应用程序无法胜任这样的工作方式,我们选择 Windows 服务来实现这样的功能。 Windows 服务可以在计算机启动的时候自动启动,一直驻留后台执行,管理员还可以在必要的时候暂停或者停止 Windows 服务。 MMC 的服务管理单元为管理 Windows 服务提供了一个中心位置。

二、 详细分析与设计

为了描述的方便,在这里我们把设计分为两个部分:内部设计和外部设计。

如果把实现投票结果自动发送功能的 Windows 服务称为投票结果处理服务器,把用于实现投票系统基本逻辑的 Web 应用程序称为投票子系统,那么内部设计就是指投票结果处理服务器内部处理逻辑的设计,而外部设计的主要任务是完成对投票结果处理服务器和投票子系统之间的通讯设计。

1、 内部设计

我们应该很清楚地看到投票结果处理服务器内部的工作方式:在服务器启动的时候,会自动地将投票项目的信息读入内存,然后进入循环,每隔一定的时间(比如 5 秒)对读入的投票项目进行判断,如果投票项目过期,则统计投票结果,并将结果发送出去。

首先要解决的是数据存储问题,也就是数据结构问题。投票项目是以数据库记录的形式保存的,那么在读入内存以后,应该以一种什么样的数据结构来存储呢?解决这一问题需要从服务器的工作方式入手。显然,数据库中的投票项目不止一条,由于管理员在创建投票项目时对投票结果的处理方式有不同的选择(可以选择自动发送结果功能,也可以不选择此功能),这就决定了在多条投票项目中可能只有一部分需要对结果进行发送。此外,每一个投票项目记录有多个字段,比如投票项目的 ID 号、投票内容、截止时间等,这些字段的值构成了投票结果的实体,是所需发送信息的主要组成部分。我们可以考虑使用结构体数组来存储这些投票项目记录,比如:

** typedef struct ** t_VoteItemInfo {

int id; // 投票项目的 ID 域

String Title; // 投票项目的标题域

String Name; // 投票项目的名称域

// . . .

TdateTime VoteDeadline; // 投票项目的截止时间域

} VoteItemInfo;

** typedef ** VoteItemInfo VoteItemInfoArray[MAX_SIZE];

然而,我们不能确定某一时刻数据库中的投票项目记录有多少,也不能确定在所有的投票项目记录中,需要对结果进行发送的记录有多少,这就使得我们很难确定上面代码中的 MAX_SIZE 的值,如果 MAX_SIZE 的值定义得太小,可能会造成投票项目无法一次性读入内存,必要的时候还需要有专门的换入 - 换出算法来替换数组中的数据,这样实现起来非常麻烦;如果 MAX_SIZE 定义得太大,又可能会造成存储空间的浪费,投票结果处理服务器是一个常驻内存的 Windows 服务,大量空间的浪费会对服务器的性能造成影响。

从上面的分析可以看出,数组并不是存储投票项目记录的最好方法,在此我们引入队列,使用单向队列(也就是单向链表)来存储投票项目记录。 C++ 的 STL 库为我们定义这样的数据结构提供了很好的机遇,下面的代码很容易地实现了这样的定义:

** #include **

  1<queue>
  2
  3** using  ** **namespace** std; 
  4
  5** typedef  ** **struct** t_vi { 
  6
  7**int** id;  //  投票项目的  ID  域 
  8
  9String Title;  //  投票项目的标题域 
 10
 11String Name;  //  投票项目的名称域 
 12
 13// . . . 
 14
 15TdateTime VoteDeadline;  //  投票项目的截止时间域 
 16
 17} VoteInfoNode; 
 18
 19** typedef  ** queue<voteinfonode> VoteInfoQueue; 
 20
 21其次,让我们来具体分析一下服务器内部对投票项目过期的判断方式。由于管理员在创建投票时所提供的信息和所作的选择需要保存,因此投票项目的截止时间(也可以称为过期时间)是以记录字段的形式保存在数据库中的。在服务器读入投票项目队列时,投票项目的截止时间就会成为队列节点的一个域而装入内存,这样,如果某一时刻服务器发现队列中存在一个已过期的时间,那么就可以很容易地确定与该过期时间相关联的其它投票项目信息,组织、生成并发送投票项目的结果也就变得非常容易。 
 22
 23假设某一时刻投票项目队列中有  N  个节点(  N  的值足够大),那么要判断具体是哪个节点已经过期就是一个费时的操作,比如我们可以遍历队列中的每个节点来进行判断,但这样做会占用一定的处理时间,因为需要循环(这种情况下算法复杂度为  O(N)  )。另一方面,服务器程序需要时刻观察队列中的数据,以便及时发现过期的节点,这就决定了服务器程序对投票项目队列的遍历周期不能太长。现假设服务器程序每隔  0.5  秒对队列遍历一次,如果遍历队列的耗时超过  0.5  秒,那么就有可能无法找到过期的项目,因为在一次遍历还没有完成的情况下,服务器程序已经进入了下一次遍历周期。显然这里是存在矛盾的,一方面服务器需要时刻扫描投票项目队列,这决定了扫描过程不能太费时;另一方面如果节点数  N  足够大(比如大于  1000000  ),这又决定了扫描过程会非常耗时。由于投票结果处理服务器是一个常驻内存的  Windows  服务,这就决定了服务器不能够占用太多的处理时间,否则会影响整个服务器甚至整个计算机系统的性能。 
 24
 25解决这样的矛盾需要从队列本身的属性出发。队列是  FIFO  的数据结构,如果节点  D  在  T  时刻过期,那么就可以对  D  进行信息发送处理,然后让  D  出队,这样做既可以节省空间,也可以节约处理的时间。更进一步,我们可以在构建投票项目队列之前先对所有的记录进行排序,把投票截止时间较小的项目排在前面,通过这种方式创建出来的队列具有这样的特性:最早过期的项目排在最前面。这一点是非常重要的,因为如果队列中的前导节点没有过期,那么后继节点是肯定没有过期的。 
 26
 27![](http://dev.csdn.net/article/40/D:/My Documents\\Word Documents\\使用Windows服务实现投票结果的自动发送功能_1.bmp)
 28
 29图一  前导节点没有过期,其后继节点肯定没有过期 
 30
 31采用了这样的处理方式以后,服务器程序只要定期地检查队列的首节点是否过期就可以完成处理:如果首节点过期,那么处理首节点,然后首节点出队,其后继节点成为首节点,并进入下一轮的判断(这种情况下算法复杂度为  O(1)  )。 
 32
 33下面的伪代码详细地描述了这一过程: 
 34
 35//  本函数每隔一定的时间间隔(  timer interval  )执行一次 
 36
 37** void __fastcall  ** ProcessTimer_OnTimer (TObject *sender) 
 38
 39{ 
 40
 41** if  ** (  队列不为空  ) 
 42
 43{ 
 44
 45tmpNode =  队列  .front();  //  获得队列头节点 
 46
 47**if** (tmpNode.  过期时间  &lt; 当前时间  )  //  该节点已经过期(被处理过) 
 48
 49{ 
 50
 51队列  .pop();  //  节点出队 
 52
 53**return** ; 
 54
 55} 
 56
 57**else** **if** (tmpNode.  过期时间  =  当前时间  ) //  恰好过期 
 58
 59{ 
 60
 61将  tmpNode  的信息以邮件形式发送出去  ; 
 62
 63队列  .pop();  //  节点出队 
 64
 65} 
 66
 67} 
 68
 69} 
 70
 71此外,由于投票项目信息都保存在数据库中,那么在构造投票项目队列之前对数据的排序可以使用  SQL  查询语句来实现,例如: 
 72
 73** SELECT  ** * **FROM** t_VoteItems **ORDER** **BY** VoteDeadline **ASC**
 74
 75上面讲述了投票结果处理服务器两个核心部分的设计:数据结构和过期判断处理。现在来讨论一下服务器程序本身的特性与其相关的设计。 
 76
 77a、  日志 
 78
 79服务器程序是后台运行的,它无法将即时的信息显示在屏幕上,因此需要使用日志。日志是这样一个文件,它记录了服务器运行的整个过程和输出信息,以及信息的写入时间与状态,以便在服务器程序工作不正常的时候跟踪错误出现的时间和原因。当多个服务器程序共用同一个日志文件时,日志文件中必须记录某条信息具体是由哪个服务器写入的;如果服务器程序有各自独立的日志文件时,这样的信息可以省略。 
 80
 81a.1  日志文件的写入 
 82
 83通常使用形如“  ** int  ** WriteLog ( **int** level, **char** *fmt, . . .)  ”的可变参函数来实现日志文件的写入。可变参函数主要通过  va_list  变量以及与其相关的  va_start  、  vfprintf  和  va_end  函数实现,具体内容可以参考  MSDN  相关文档。需要注意的是:  ①  UNIX  系统中可变参函数的实现与  Windows  系统略有不同;  ②  日志文件需要用“  a+  ”(添加)的方式打开,否则有可能只写入一条最近产生的日志信息。 
 84
 85在编写服务器程序的时候,应该在合理的地方调用  WriteLog  函数以完成日志的写入,例如可以在  try. . .catch  块中使用  WriteLog  函数: 
 86
 87try 
 88
 89{ 
 90
 91// . . . 
 92
 93} 
 94
 95catch (Exception &amp;e) 
 96
 97{ 
 98
 99WriteLog (LOGLEV_ERR, “  错误信息=  %s\n”, e.Message.c_str()); 
100
101} 
102
103a.2  日志文件的管理 
104
105日志文件记录了服务器运行的整个细节,每运行一次服务器程序,就会有大量的信息写入日志文件,一段时间以后日志文件的大小有可能达到几十兆,甚至占用整个硬盘空间,造成服务器系统的崩溃。因此,在设计服务器程序的时候,应该根据服务器的使命等级来对日志文件进行相应的处理。例如,在日志文件中快速准确地定位错误信息以找到错误源,这对于使命关键的服务器系统是非常重要的,一般需要对日志文件进行备份,而这又可以通过设计额外的日志管理系统来实现;对于一般的服务器程序,在稳定性要求不是很高的情况下,我们可以选择直接重写日志文件的方式。在设计投票结果处理服务器时,我们选择后面这种日志处理方式。 
106
107日志文件的备份可以用单独的模块实现,也可以直接在  WriteLog  函数中实现。在写入日志之前,先判断日志文件的大小,如果大于规定的大小,则复制日志文件,并且采用“  w  ”(覆盖写入)的方式打开日志文件并写入日志;否则直接使用“  a+  ”(添加)的方式打开日志文件。 
108
109b、  配置文件 
110
111对于服务器程序而言,配置文件是非常重要的。配置文件中保存了很多服务器程序运行的参数,服务器程序在指定的时刻(一般是启动的时候)读取配置文件并应用相应的参数。在服务器系统中采用配置文件将使得服务器程序更具有灵活性,使得服务器程序能够更好地适应不同的需求环境。例如,如果将邮件服务器地址写入配置文件,那么在邮件服务器地址发生变更时,我们只需要修改配置文件并让服务器程序应用最新的设置,而不需要修改服务器程序代码。 
112
113Windows  服务的配置文件一般使用  INI  的文件格式,这样使得读写配置文件变得很方便。在投票结果处理服务器的设计中,我们将读取配置文件参数的代码写在了服务启动(  ServiceStart  )的处理函数中,这样,每次服务器启动的时候都会应用最新的配置参数。同样,如果某时刻对配置文件进行了更改,那么要使服务器应用最新的配置参数,就必须重新启动服务。 
114
115c、  通讯 
116
117一般来讲,服务器程序需要和外部的机能进行通讯,以向该外部机能提供服务,这种外部机能通常称为客户端。进程间的通讯有多种方式,例如共享内存、管道、信号以及  socket  等。在投票系统中,我们使用  socket  通讯,以实现  Web  应用程序对服务器程序的信息通知。  .NET Framework  的  System.Net.Sockets  命名空间提供了用于  socket  通讯的类,这使得投票子系统和投票结果处理服务器之间的通讯成为可能。下面的  C#  代码实现了对投票结果处理服务器的信息通知: 
118
119using  System.Net.Sockets; 
120
121using  System.Text; 
122
123public  class  VotingNotifier 
124
125{ 
126
127private  int  VotingNotifierServerPort = 9023; 
128
129private  string  VotingNotifierServerHostName =  "127.0.0.1"  ; 
130
131&lt;SPAN lang=EN-US style="FONT-SIZE: 9pt; FONT-FAMILY: 'Courier New'; mso-font-ke</voteinfonode></queue>
Published At
Categories with Web编程
Tagged with
comments powered by Disqus