32.4.3 Windows服务项目
使用C# Windows服务的新项目创建向导可以创建Windows服务,该项目命名为QuoteService,其窗口如图32-7所示。注意,在选择项目时不要误选为Web服务项目。
图 32-7
在单击 OK 按钮开始创建 Windows 服务应用程序之后,就会出现一个外表与 Windows Forms 应用程序相似的设计器,但是不能在其中插入 Windows Forms 组件,因为应用程序不能直接在屏幕上显示任何信息,本章的后面将使用设计器添加性能计数器和事件日志等其他组件。
选择这个服务的属性,可以打开如图 32-8 所示的属性编辑窗口。
图 32-8
使用服务属性可以配置如下值:
AutoLog指定启动和停止服务的事件自动写到日志文件中。
● CanPauseAndContinue 、 CanShutdown 和 CanStop 指定服务可以处理具体的暂停、继续、关闭和停止服务的请求。
● ServiceName 是写到注册表中的服务名称,使用这个名称可以控制服务。
● CanHandlePowerEvent 选项对运行在膝上计算机的服务有效。如果启用这个选项,服务就可以响应低电源事件,改变服务的操作方式。
提示:
不管项目的名称是什么,默认的服务名称都是WinService1。可以只安装一个WinService1服务。如果在测试过程中出现了安装错误,有可能已经安装了WinService1服务。因此,在服务开发的初始阶段,一定要用属性编辑器把服务的名称改为比较适当的名称。
使用属性编辑器改变上述属性,在InitalizeComponent()方法中设置ServiceBase派生类的值。Windows Forms应用程序中也使用InitalizeComponent()方法,对于服务而言,这个方法的使用方式与Windows Forms应用程序相似。
向导将生成代码,但是我们将把文件名改为QuoteService.cs,把命名空间的名称改为Wrox.ProCSharp.WinServices,并把类名改为QuoteService。后面将详细讨论这些代码。
1. ServiceBase类
ServiceBase类是所有.NET服务的基类。QuoteService类就是从ServiceBase类派生出来的;QuoteService类使用一个未标注的帮助类System.ServiceProcess.NativeMethods与SCM进行通信,System.ServiceProcess.NativeMethods是Win32 API调用的包装类。ServiceBase类是私有的,因此,不能在这里的代码中使用它。
图 32-9 显示了 SCM 、 QuoteService 类和 System.ServiceProcess 命名空间中的类是怎样相互作用的。在这个图中,垂直方向为对象的生命线,水平方向为通信情况,通信是按照时间的先后顺序而进行的。
SCM启动应该启动的服务进程。首先调用Main()方法。在示例服务的Main()方法中,调用ServiceBase基类的Run()方法。Run()使用SCM中的NativeMethods.StartServiceCtrl Dispatcher()注册ServiceMainCallback()方法,并把记录写到事件日志中。
接下来,SCM在服务程序中调用已注册的ServiceMainCallback()方法。ServiceMainCallback()本身使用NativeMethods.RegisterServiceCtrlHandler[Ex]()在SCM中注册处理程序,并在SCM中设置服务的状态。之后调用OnStart()方法。在OnStart()中,必须执行启动代码。如果OnStart()执行成功,就把字符串Service Started Sucessful写到事件日志中。
处理程序是在ServiceCommandCallback()方法中执行的。当改变了对服务的请求时,SCM就调用ServiceCommandCallback()方法。ServiceCommandCallback()方法再把请求发送给OnPause()、OnContinue()、OnStop()、OnCustomCommand()和OnPowerEvent()。
图 32-9
2. 主函数
现在讨论服务进程中由应用程序向导生成的主函数。在主函数中,声明了一个元素为ServiceBase类的数组ServicesToRun。创建QuoteService类的一个实例,并作为ServicesToRun数组的第一个元素。如果在这个服务进程中要运行多个服务,就需要把具体服务类的多个实例添加到数组中。然后把ServicesToRun数组传递给ServiceBase类的静态方法Run()。使用ServiceBase的Run()方法,可以把SCM引用供给服务的入口点。服务进程的主线程现在处于停滞状态,等待服务的结束。
下面是自动生成的代码:
static void Main()
{
System.ServiceProcess.ServiceBase[] ServicesToRun;
// More than one user Service may run within the same process. To
// add another service to this process, change the following line
// to create a second service object. For example,
//
// ServicesToRun = New System.ServiceProcess.ServiceBase[]
// {
// new WinService1(), new MySecondUserService()
// };
//
ServicesToRun = new System.ServiceProcess.ServiceBase[]
{
new QuoteService()
};
System.ServiceProcess.ServiceBase.Run(ServicesToRun);
}
如果进程中只有一个服务,就可以删除数组。Run()方法接收从ServiceBase派生出来的单个对象,因此Main()方法可以简化为:
System.ServiceProcess.ServiceBase.Run(new QuoteService());
如果有多个服务,例如Windows程序Services.exe就包含多个服务,并且需要那些服务有共享的初始化,则共享的初始化必须在Run()方法运行之前完成。因为主线程处于停滞状态,直到服务进程停止为止,以后的指令在服务结束之前就不能执行。
初始化花费的时间不应该太长,通常不应该超过 30 秒。如果执行初始化代码所花费的时间过多,则服务控制管理器就认为服务启动失败了。初始化时间不应该超过 30 秒,必须是针对速度最慢的机器而言。如果初始化的时间过长,就应该在另一线程中进行初始化,以便主线程及时地调用 Run() 。然后,事件对象可以用信号通知线程已经完成了它的工作。
3. 服务的启动
在服务启动时,调用OnStart()方法。这时,可以启动套接字服务器。为了使用QuoteServer,必须引用QuoteServer.dll程序集。调用OnStart()的线程不能停滞下来,OnStart()方法必须返回给调用者(即ServiceBase类的ServiceMainCallback()方法)。ServiceBase类注册处理程序,并在调用OnStart()之后把服务成功启动的消息通知给SCM:
protected override void OnStart(string[] args)
{
quoteServer = new QuoteServer(@"c:/ProCSharp/Services/quotes.txt",
5678);
quoteServer.Start();
}
quoteServer变量声明为类中的私有成员:
namespace Wrox.ProCSharp.WinServices
{
public class QuoteService : System.ServiceProcess.ServiceBase
{
private System.ComponentModel.Container components = null;
private QuoteServer quoteServer;
4. 处理程序方法
当停止服务时,就调用OnStop方法。应该在OnStop方法中停止服务的功能:
protected override void OnStop()
{
quoteServer.Stop();
}
除了 OnStart() 和 OnStop() 之外,还可以重写服务类中的下列处理程序:
OnPause():在暂停服务时,调用这个方法。
OnContinue():当服务从暂停状态返回到正常操作时,调用这个方法。为了调用已重写的OnPause()方法和OnContinue()方法,CanPauseAndContinue属性必须设置为true。
OnShutdown() :当 Windows 操作系统关闭时,调用这个方法。通常情况下, OnShutdown()方法的行为应该与OnStop()方法相似。如果需要更多的时间关闭服务,则可以请求更多的时间。与OnPause()和OnContinue()相似,必须设置一个属性使行为有效,即CanShutdown属性必须设置为true。
OnCustomCommand() :这个处理程序可以为服务控制程序发送过来的定制命令提供服务。OnCustomCommand()方法有一个用于获取定制命令号码的int参数,号码的取值范围是128至256,小于128的值是为系统预留的。在我们的服务中,使用定制命令号码为128的命令重新读取引用文件:
protected override void OnPause()
{
quoteServer.Suspend();
}
protected override void OnContinue()
{
quoteServer.Resume();
}
protected override void OnShutdown()
{
OnStop();
}
public const int commandRefresh = 128;
protected override void OnCustomCommand(int command)
{
switch (command)
{
case commandRefresh:
quoteServer.RefreshQuotes();
break;
default:
break;
}
}