分类:
2008-10-14 14:57:17
用 C++ 创建简单的 Win32 服务程序
作者:Nigel Thomson(MSDN 技术组)
翻译:
原文出处:
下载 例子源代码
下载
例子源代码
下载
例子源代码
摘要
本文描述如何用 Visual C++ 创建 Windows NT 服务程序。创建该服务仅用到一个 C++
类,这个类提供服务与操作系统之间一个简单的接口。使用这个类实现自己的服务非常简单,只要改写少数几个基类中的虚拟函数即可。在本文有三个源代码参考例子:
简介
Windows NT
中的服务实际上是一个程序,只要计算机操作系统一启动,服务就可以运行其中。它不需要用户登陆。服务程序是一种与用户无关的任务,比如目录复制,进程监控或网络上供其它机器使用的服务,比如
HTTP 协议支持。
创建 Windows NT 服务程序并不是很难。但调试某个服务程序不是一件容易的事。就我自己而言,我喜欢用 Visual C++
编写自己的 C++ 程序。大多数 Win32 服务都是用 C 写的,所以我觉得如果用某个 C++ 类来实现 Win32
服务的基本功能一定很有意思。有了这个 C++ 类,谁要想用 C++ 创建 Win32 服务就是一件很简单的事情了。我为此开发了一个 C++
基类,用它作为编写 Win32 服务的起点应该没有什么大问题。
创建服务程序除了编写服务代码外,还必须做一些其它额外的编码工作:
大多数服务程序都是使用一个安装程序来安装,而用另外一个程序来卸载。本文我将这些功能内建在服务程序自身当中,使之一体化,这样只分发一个.EXE文件即可。你可以从命令行直接运行服务程序,并且可以随心所欲地安装和卸载或报告其版本信息。NTService 支持下列的命令行参数:
默认情况下,当系统启动该服务时没有命令行参数传递。
创建应用程序框架
我一直都是创建基于 MFC 的应用程序。当我刚接触 Win32 服务程序时,我先是用 Visual C++ AppWizard 创建一个
SDI/MFC 程序。然后去掉其中的文档和视图类、图标以及其它一些无用的东西,只剩下框架。结果到最后什么都去掉了,包括主窗口(服务程序不能有这个东东),什么也没有留下,非常愚蠢。我不得不
又回过头到 AppWizard,并用单个的源文件创建控制台程序,此源文件包含main
入口函数,我将这个文件命名为 NTServApp.cpp。我用此 cpp 扩展而不是用 C,因为我只想用C++ 来写程序,而不是直接用
C。稍后我们会讨论该文件代码实现。
因为我想用 C++ 类来构建服务,所以我创建了 NTService.h 和 NTService.cpp 文件,用它们来实现 CNTService
基类。我还创建了 MyService.h 和 MyService.cpp 文件用于实现自己的服务类(CMyService),它派生于
CNTService。稍后我们会看到代码。
建立新工程时,我喜欢尽快看到运行结果,所以我决定服务程序要做的第一件事情是建立一个系统应用程序日志记录。借助这个日志记录机制,我能跟踪服务何时启动,
何时停止等等。我还可以记录服务中发生的任何出错信息。创建这个日志记录比我想象的要复杂得多。
建立日志记录
我想,既然日志文件是操作系统的一部分,那么肯定有应用程序编程接口(API)来支持建立日志记录。所以我开始搜索 MSDN CD,直到发现 ReportEvent
函数为止。如果你不熟悉这个函数,你可能会想,这个函数应该知道在哪个日志文件建立记录,以及你想要插入的文本信息。没错,这都是它要做的事情,但是为了简化出错信息的国际化,该函数有一个消息
ID 作为参数,并在你提供的消息表中查找消息。所以问题无非是你想将什么消息放入日志,以及如何将这些消息添加到你的应用程序中,下面我们一步一步来做:
下面让我们仔细一下这些文件,以便弄明白你自己需要创建什么,以及消息编译器要为你创建些什么。我们不用研究整个消息集,只要看看其中一二个如何工作的即可。下面是例子程序消息源文件 NTServMsg.mc 的第一部分:
MessageId=100 SymbolicName=EVMSG_INSTALLED Language=English The %1 service was installed. . MessageId= SymbolicName=EVMSG_REMOVED Language=English The %1 service was removed. . MessageId= SymbolicName=EVMSG_NOTREMOVED Language=English The %1 service could not be removed. .
每一条都有一个消息ID,如果不特别设置,那么 ID
的取值就是指其前面所赋的值。每一条还有一个代码中使用的符号名,语言标示符以及消息文本。消息可以跨多个行,并用含有一个句号的单独一行终止。
消息编译器输出一个库文件,该库文件被用作应用程序的资源,此外还输出两个要在代码中包含的文件。下面是我的 .RC 文件:
// NTServApp.rc #include// 包含由消息编译器(MC)产生的消息表资源脚本 #include "NTServMsg.rc" Here''s the .RC file the message compiler generated: LANGUAGE 0x9,0x1 1 11 MSG00001.bin
正像你所看到的,这些文件中内容不多!
消息编译器产生的最后一个文件是你要包含到代码中的头文件,下面就是这个头文件的部分内容:
[..........] // // MessageId: EVMSG_INSTALLED // // MessageText: // // The %1 service was installed. // #define EVMSG_INSTALLED 0x00000064L // // MessageId: EVMSG_REMOVED // // MessageText: // // The %1 service was removed. // #define EVMSG_REMOVED 0x00000065L [...........]
你可能已经注意到了有几个消息包含参数替代项(如 %1)。让我们看看将消息写入某个系统日志文件时如何在代码中使用消息ID和参数替代项。以事件日志中记录成功安装信息的部分安装代码为例。也就是 CNTService::IsInstalled 函数部分:
[....] LogEvent(EVENTLOG_INFORMATION_TYPE, EVMSG_INSTALLED, m_szServiceName); [....]
LogEvent 是另一个 CNTService 函数,它使用事件类型(信息,警告或错误),事件消息的 ID,以及形成日志消息的最多三个参数的替代串:
// This function makes an entry into the application event log. void CNTService::LogEvent(WORD wType, DWORD dwID, const char* pszS1, const char* pszS2, const char* pszS3) { const char* ps[3]; ps[0] = pszS1; ps[1] = pszS2; ps[2] = pszS3; int iStr = 0; for (int i = 0; i < 3; i++) { if (ps[i] != NULL) iStr++; } // Check to see if the event source has been registered, // and if not then register it now. if (!m_hEventSource) { m_hEventSource = ::RegisterEventSource(NULL, // local machine m_szServiceName); // source name } if (m_hEventSource) { ::ReportEvent(m_hEventSource, wType, 0, dwID, NULL, // sid iStr, 0, ps, NULL); } }如你所见,其主要工作是由 ReportEvent 系统函数处理。
服务回调函数
因为 ServiceMain 和 Handler
函数都是由系统来调用,所以它们必须遵循操作系统的参数传递规范和调用规范。也就是说,它们不能简单地作为某个 C++
类的成员函数。这样就给封装带来一些不便,因为我们想把 Win32 服务的功能封装在一个 C++ 类中。为了解决这个问题,我将 ServiceMain
和 Handler 函数创建成 CNTService 类的静态成员。这样就使我得以创建可以由操作系统调用的函数。
但是,这样做还没有完全解决问题,因为系统不允许给被调用的函数传递任何形式的用户数据,所以我们无法确定对 C++ 对象特定实例的 ServiceMain
或 Handler 的调用。用了一个非常简单但有局限的方法来解决这个问题。我创建一个包含 C++
对象指针的静态变量。这个变量是在该对象首次创建是进行初始化的。这样便限制你每个服务应用只有一个C++对象。我觉得这个限制并不过分。下面是 NTService.h
文件中的声明:
class CNTService { [...] // 静态数据 static CNTService* m_pThis; // nasty hack to get object ptr [...] };
下面是初始化 m_pThis 指针的方法:
CNTService::CNTService(const char* szServiceName) { // Copy the address of the current object so we can access it from // the static member callback functions. // WARNING: This limits the application to only one CNTService object. m_pThis = this; [...] }
CNTService 类
当我创建 C++ 对象封装 Windows 函数时,我尝试为我封装的每个 Windows API
除了创建成员函数外,还做一些别的工作,我尝试让对象更容易使用,降低实现特定项目所需的代码行数。因此我的对象是基于“我想让这个对象做什么?”而不是“Windows
用这些 APIs 做什么?”
CNTService
类包含一些用来解析命令行的成员函数,为了处理服务的安装和拆卸以及事件日志的记录,你得在派生类中重写一些虚拟函数来处理服务控制管理器的请求。下面我们将通过本文的例子服务实现来研究这些函数的使用。
如果你想创建尽可能简单的服务,只需要重写 CNTService::Run 即可,它是你编写代码实现具体服务任务的地方。你还需要实现 main
函数。如果服务需要实现一些初始化。如从注册表读取数据,还需重写 CNTService::OnInit。如果你要向服务发送命令消息
,那么可以在服务中使用系统函数 ControlService,重写 CNTService::OnUserControl 来处理请求。
在例子应用程序中使用 CNTService
NTService 在 CMyService 类中实现了它的大多数功能,CMyService 由 CNTService 派生。 MyService.h
头文件如下:
// myservice.h #include "ntservice.h" class CMyService : public CNTService { public: CMyService(); virtual BOOL OnInit(); virtual void Run(); virtual BOOL OnUserControl(DWORD dwOpcode); void SaveStatus(); // Control parameters int m_iStartParam; int m_iIncParam; // Current state int m_iState; };
正像你所看到的,CMyService 改写了 CNTService 的 OnInit、Run 和 OnUserControl。它还有一个函数叫 SaveStatus,这个函数被用于将数据写入注册表,那些成员变量用来保存当前状态。例子服务每隔一定的时间对一个整型变量进行增量处理。开始值和增量值都存在注册表的参数中。这样做并没有别的意图。只是为了简单示范。下面我们看看这个服务是如何实现的。
实现 main 函数
有了从 CNTService 派生的 CMyService,实现 main 函数很简单,请看 NTServApp.cpp 文件:
int main(int argc, char* argv[]) { // 创建服务对象 CMyService MyService; // 解析标准参数 (安装, 卸载, 版本等.) if (!MyService.ParseStandardArgs(argc, argv)) { // 未发现任何标准参数,所以启动服务, // 取消下面 DebugBreak 代码行的注释, // 当服务启动后进入调试器, //DebugBreak(); MyService.StartService(); } // 到这里,服务已经停止 return MyService.m_Status.dwWin32ExitCode; }这里代码不多,但执行后却发生了很多事情,让我们一步一步来看。首先,我们创建一个 MyService 类的实例。构造函数设置初始化状态和服务名字(MyService.cpp):
CMyService::CMyService():CNTService("NT Service Demonstration") { m_iStartParam = 0; m_iIncParam = 1; m_iState = m_iStartParam; }
接着调用 ParseStandardArgs 检查命令行是否包含服务安装(-i)、卸载(-u)以及报告其版本号(-v)的请求。CNTService::ParseStandardArgs
分别调用 CNTService::IsInstalled,CNTService::Install 和 CNTService::Uninstall
来处理这些请求。如果没有可识别的命令行参数,则假设该服务控制管理器试图启动该服务并调用 StartService。该函数直到服务停止运行才返回。当你调试完代码,即可把用于调试的代码行注释掉或删除。
安装和卸载服务
服务的安装由 CNTService::Install 处理,它用 Win32
服务管理器注册服务并在注册表中建立一个条目以支持服务运行时日志消息。
服务的卸载由 CNTService::Uninstall 处理,它仅仅通知服务管理器该服务已经不再需要。CNTService::Uninstall
不会删除服务实际的可执行文件。
编写服务代码
现在我们来编写实现服务的具体代码。对于 NTService 例子,有三个函数要写。他们涉及初始化,运行服务的细节和响应控制请求。
初始化
注册表有一个给服务用来存储参数的地方:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services
我就是选择这里来存储我的服务配置信息。我创建了一个 Parameters 键,并在此存储我要保存的值。所以当服务启动时,OnInit 函数被调用;这个函数从注册表中读取初始设置。
BOOL CMyService::OnInit() { // Read the registry parameters. // Try opening the registry key: // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\
--------------------next---------------------