分步创建C#服务:第一课






4.82/5 (83投票s)
2003 年 4 月 10 日
8分钟阅读

577445

10062
这是一篇多篇文章的贡献,详细分步介绍如何创建具有集成安装和自定义事件日志支持的自己的服务。
引言
最近我尝试使用 .NET 框架编写一个 C# 服务,遇到了许多文档中未涵盖或难以推断出解决方案的问题。因此,我决定写一系列文章,详细分步介绍我如何解决遇到的问题。
这是一篇多篇文章的贡献,内容如下:
- 创建项目、初始服务和安装
- 为应用程序添加其他服务
- 添加自定义事件日志支持
使用代码
让我们开始创建一个新的 C# 项目,它是一个控制台应用程序。Visual Studio .NET 提供了使用 C# 创建 Windows 服务的向导,但我将从头开始,以便没有 VS.NET 的用户也能跟上。图 1 显示了初始项目创建屏幕。
图 1
现在我们有了一个简单的控制台应用程序,我们需要更改向导创建的一些默认设置。让我们打开 *Class1.cs* 文件。这个文件包含应用程序的入口点,但我们将把类 Class
重命名为更能反映其在应用程序中角色的名称。在解决方案资源管理器中右键单击 *Class1.cs* 节点,然后选择 *重命名*。将文件重命名为 *Application.cs*。然后右键单击并选择 *查看代码*,以便我们可以直接编辑代码。我们将把类 Class
重命名为 Application
。
我们还将更改命名空间,以反映类在我们公司命名空间中的位置。我有一个用于示例代码的域,名为 CodeBlooded
,我将使用它作为我的公司名称。您可以自由使用任何您想要的名称。由于此服务是一个虚构的扑克游戏(我可能会随着时间推移将其完全开发出来)的一部分,因此我们将我们的命名空间命名为 CodeBlooded.Spades.Services
。编辑新的 *Application.cs* 文件,并将命名空间从默认的 SpadesServer
更改为 CodeBlooded.Spades.Services
。
为了避免我们在项目中添加的每个类都要进行这些更改,请通过右键单击解决方案中的 *SpadesServer* 项目并选择 *属性* 来打开项目属性对话框。在项目属性对话框中,转到 *通用属性 | 常规选项卡*,然后将 *应用程序 | 默认命名空间* 字段更改为 CodeBlooded.Spades.Services
。从现在开始,我们将创建的任何类都将自动分配到此命名空间。图 2 显示了该选项卡的外观。
图 2
在项目设置对话框中,转到 *配置属性 | 生成* 选项卡,并将 *输出 | XML 文档文件* 设置更改为 *SpadesServer.xml*。这是一个编译器将为我们创建的 XML 文件,以帮助我们构建项目的文档。有关支持的文档标签,请参阅 .NET 框架 SDK 文档。图 3 显示了该对话框的外观。
图 3
现在我们已经处理了一些配置细节,可以开始实际编写代码了。.NET 框架有一个基类 ServiceBase
,它提供了实现服务的接口。必须继承此类,才能添加我们创建所需服务的实际逻辑的代码。我们必须重写 OnStart
和 OnStop
方法的默认方法。服务控制管理器调用这些方法来实际控制服务。
问题在于 OnStart
和 OnStop
方法必须在 1 分钟内将控制权返回给服务控制管理器,服务控制管理器才能识别服务正在运行或已停止。那么,如果我们必须在一分钟内返回,如何实际完成工作呢?我们必须创建一个后台线程来执行我们的服务将执行的所有工作。
为了避免在本次教程中创建的每个类编写相同的代码,我将创建一个基类,负责创建工作线程以及与 SCM 进行通信。创建一个新类,我们称之为 SpadesServiceBase
。以下代码是该类应有的外观。
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
namespace CodeBlooded.Spades.Services
{
public class SpadesServiceBase : System.ServiceProcess.ServiceBase
{
public SpadesServiceBase()
{
// TODO: Add any initialization code here
}
/// <SUMMARY>
/// Set things in motion so your service can do its work.
/// </SUMMARY>
protected override void OnStart(string[] args)
{
// TODO: Add code here to start your service.
}
/// <SUMMARY>
/// Stop this service.
/// </SUMMARY>
protected override void OnStop()
{
// TODO: Add code here to perform any tear-down
// necessary to stop your service.
}
}
}
我们最终将创建 3 个服务来控制游戏的各个状态,并创建一个管理服务来控制和/或管理其他 3 个服务。为了处理这个问题,管理服务将继承自 SpadesServiceBase
类,但我们将创建另一个继承自 SpadesServiceBase
类的类,供 3 个子服务继承。这有助于我们更好地隔离共享代码,使最低级别的基类仅包含适用于所有服务的代码,而子服务基类将包含所有子服务共享的通用代码。
所以,让我们创建另一个名为 SpadesChildServiceBase
的类,并让它继承自 SpadesServiceBase
。您可以随意创建文件,但代码最终应如下所示:
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
namespace CodeBlooded.Spades.Services
{
public class SpadesChildServiceBase : SpadesServiceBase
{
public SpadesChildServiceBase()
{
// TODO: Add any initialization code here
}
/// <SUMMARY>
/// Set things in motion so your service can do its work.
/// </SUMMARY>
protected override void OnStart(string[] args)
{
// TODO: Add code here to start your service.
base.OnStart( args );
}
/// <SUMMARY>
/// Stop this service.
/// </SUMMARY>
protected override void OnStop()
{
// TODO: Add code here to perform any
// tear-down necessary to stop your service.
base.OnStop();
}
}
}
现在我们将编写代码来创建后台线程和由 SCM 用于启动和停止后台线程的同步对象。后台线程将定期唤醒并执行某些操作。如果您要编写一个依赖于来自客户端的消息的服务,那么您将使用任何 IPC 机制来控制通信。为了获得最佳性能,您应该使此代码事件驱动,而不是编写轮询机制。在未来的文章中,我可能会描述如何使用异步套接字调用来完成此操作,但现在我将创建一个简单的轮询机制来管理服务的 [工作流程]。
为了方便基类与更具体的派生类进行通信,基类将提供一个名为 Execute
的纯虚方法,该方法需要由服务实现。当我们的计时器定期收到运行信号时,基类会定期调用 Execute
方法。 Execute
方法应包含您需要定期执行的特定逻辑,以完成每个单独服务负责的任务。
我们需要向我们的 SpadesServiceBase
类添加对 System.Threading
的引用,并添加一个名为 m_thread
的 Thread
成员变量。我们还需要一个 Threading
命名空间中的 ManualResetEvent
,用于从 SCM 传递我们的停止请求。在 SpadesServiceBase
的 OnStart
方法中,我们将创建一个 Thread
对象实例以及 ManualResetEvent
。要运行线程,我们需要定义一个线程启动方法,它是线程的入口点。在我们的例子中,我们将方法称为 ServiceMain
,并使用框架中的 ThreadStart
类将我们的 ServiceMain
定义为委托方法。修改您的 SpadesServiceBase
类,使其外观如下代码块所示。
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
using System.Threading;
namespace CodeBlooded.Spades.Services
{
public class SpadesServiceBase : System.ServiceProcess.ServiceBase
{
public SpadesServiceBase() {
// create a new timespan object
// with a default of 10 seconds delay.
m_delay = new TimeSpan(0, 0, 0, 10, 0 );
}
/// <SUMMARY>
/// Set things in motion so your service can do its work.
/// </SUMMARY>
protected override void OnStart(string[] args) {
// create our threadstart object to wrap our delegate method
ThreadStart ts = new ThreadStart( this.ServiceMain );
// create the manual reset event and
// set it to an initial state of unsignaled
m_shutdownEvent = new ManualResetEvent(false);
// create the worker thread
m_thread = new Thread( ts );
// go ahead and start the worker thread
m_thread.Start();
// call the base class so it has a chance
// to perform any work it needs to
base.OnStart( args );
}
/// <SUMMARY>
/// Stop this service.
/// </SUMMARY>
protected override void OnStop() {
// signal the event to shutdown
m_shutdownEvent.Set();
// wait for the thread to stop giving it 10 seconds
m_thread.Join(10000);
// call the base class
base.OnStop();
}
/// <SUMMARY>
///
/// </SUMMARY>
protected void ServiceMain() {
bool bSignaled = false;
int nReturnCode = 0;
while( true ) {
// wait for the event to be signaled
// or for the configured delay
bSignaled = m_shutdownEvent.WaitOne( m_delay, true );
// if we were signaled to shutdow, exit the loop
if( bSignaled == true )
break;
// let's do some work
nReturnCode = Execute();
}
}
/// <SUMMARY>
///
/// </SUMMARY>
/// <RETURNS></RETURNS>
protected virtual int Execute() {
return -1;
}
protected Thread m_thread;
protected ManualResetEvent m_shutdownEvent;
protected TimeSpan m_delay;
}
}
现在我们已经处理了这些细节,让我们继续创建管理服务。目前该服务不会负责太多事情,但在接下来的几课中,我们将为其添加更多功能。现在创建一个名为 SpadesAdminService
的新类,并使其继承自 SpadesServiceBase
。下面的代码是管理服务的样板代码。
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
namespace CodeBlooded.Spades.Services
{
public class SpadesAdminService : SpadesServiceBase
{
public SpadesAdminService()
{
this.ServiceName = "SpadesAdminSvc";
}
/// <SUMMARY>
/// Set things in motion so your service can do its work.
/// </SUMMARY>
protected override void OnStart(string[] args)
{
base.OnStart( args );
}
/// <SUMMARY>
/// Stop this service.
/// </SUMMARY>
protected override void OnStop()
{
base.OnStop();
}
/// <SUMMARY>
///
/// </SUMMARY>
/// <RETURNS></RETURNS>
protected override int Execute() {
// for right now we'll just log a message in the
// Application message log to let us know that
// our service is working
System.Diagnostics.EventLog.WriteEntry("SpadesAdminSvc",
ServiceName + "::Execute()");
return 0;
}
}
}
好的,现在我们已经编写了一个服务,我们需要做一些更多的配置工作,以便告知 Windows 此可执行文件中存在服务。这样做很简单,我们只需要创建一个 ServiceBase
对象集合,然后调用基类 ServiceBase
的 Run
方法即可。将以下代码添加到 *Application* 对象中的 Main
方法。以下代码片段显示了启动我们的服务所需的所有内容。
static void Main(string[] args)
{
// we'll go ahead and create an array so that we
// can add the different services that
// we'll create over time.
ServiceBase[] servicesToRun;
// to create a new instance of a new service,
// just add it to the list of services
// specified in the ServiceBase array constructor.
servicesToRun = new ServiceBase[] { new SpadesAdminService() };
// now run all the service that we have created.
// This doesn't actually cause the services
// to run but it registers the services with the
// Service Control Manager so that it can
// when you start the service the SCM will call
// the OnStart method of the service.
ServiceBase.Run( servicesToRun );
}
我们离完成本课的结束越来越近了,要运行服务,我们只需要添加我们的 Installers
。安装程序可以通过我可能在未来的文章中介绍的设置程序进行调用,也可以通过随框架 SDK 提供的 *InstallUtil* 程序进行调用。
向项目中添加一个新的安装程序,我们称之为 SpadesInstaller
。运行 *添加类* 向导后,我们将向类构造函数添加一些代码。安装程序使用应用于类的属性,其中安装程序将创建具有应用了 RunInstaller
属性的类的实例。
我们只需要一个 ServiceProcessInstaller
实例来控制服务进程的启动方式,然后为我们将要安装的每个服务创建一个 ServiceInstaller
对象。在设置了这些对象的某些属性后,我们将将其添加到 Installers
集合中。以下代码显示了安装我们的服务所需的一切。
using System;
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;
using System.ServiceProcess;
namespace CodeBlooded.Spades.Services
{
/// <SUMMARY>
/// Summary description for SpadesInstaller.
/// </SUMMARY>
[RunInstaller(true)]
public class SpadesInstaller : System.Configuration.Install.Installer
{
public SpadesInstaller()
{
ServiceProcessInstaller process = new ServiceProcessInstaller();
process.Account = ServiceAccount.LocalSystem;
ServiceInstaller serviceAdmin = new ServiceInstaller();
serviceAdmin.StartType = ServiceStartMode.Manual;
serviceAdmin.ServiceName = "SpadesAdminSvc";
serviceAdmin.DisplayName = "Spades Administration Service";
// Microsoft didn't add the ability to add a
// description for the services we are going to install
// To work around this we'll have to add the
// information directly to the registry but I'll leave
// this exercise for later.
// now just add the installers that we created to our
// parents container, the documentation
// states that there is not any order that you need to
// worry about here but I'll still
// go ahead and add them in the order that makes sense.
Installers.Add( process );
Installers.Add( serviceAdmin );
}
}
}
编译后进行测试,我们只需要启动一个命令提示符并导航到我们的输出目录。目前,我们仅使用项目的 *bin\Debug* 子目录,并运行以下命令来安装我们的服务。
installutil SpadesServer.exe
如果您想卸载服务,请运行以下命令。
installutil SpadesServer.exe -U
完成后,我们应该能够从服务控制管理器随意启动和停止服务。
在接下来的几篇文章中,我们将为我们在这里创建的框架添加更多功能。我们将添加一些子服务,当它们启动时,如果管理服务尚未启动,则会启动管理服务。如果管理服务停止,它将关闭子服务。这类似于 IISAdmin 控制不同 Web 服务的方式。此外,我们还将添加一个自定义消息日志以及一个消息日志安装程序。
历史
- 1.0 - 2003 年 4 月 9 日
- 第一个发行版本。