一个独立的 NT 服务类:无需派生






4.66/5 (15投票s)
用几行代码创建一个 NT 服务。
引言
本文旨在提供一个简单易用的类来创建 NT 服务。用户只需编写服务自身的函数并将它们传递给该类即可。以下示例展示了其工作原理。
// this is the service main
void WINAPI my_service_main(DWORD argc, char_* argv[])
{
static DWORD i = 0;
try
{
wofstream fout(L"C:\\output.txt",ios_base::app);
if ( fout.is_open() ){
fout << "service main counting: " << ++i << endl;
fout.close();
}
Sleep(5000);
}
catch(...){
nt_service::stop( -1 );
}
}
void my_shutdown_fcn(void)
{
Beep(1000,1000);
}
// this is the application main
void wmain(DWORD argc, LPWSTR* argv)
{
// create an access point to the service Framework
nt_service& service = nt_service::instance(L"my_test_service");
// register "my_service_main" to be executed as the service main method
service.register_service_main( my_service_main );
// set the service to accept stop controls. Do nothing when it happens
service.accept_control( SERVICE_ACCEPT_STOP );
// set the service to accept shutdown commands.
// Call my_shutdown_fcn() when it happens
service.register_control_handler( SERVICE_CONTROL_SHUTDOWN, my_shutdown_fcn );
// All set, start the service
service.start();
}
如果您不熟悉 NT 服务创建过程,建议您阅读全文。如果您已经熟悉 NT 服务,只需下载代码即可开始使用!该类 interface
是自解释的。
动机
网上有很多封装了创建简单服务所需的 API 工作的类。其中大多数都写得很好,功能也很强大。但这类类(至少是我找到的)有一个问题:用户被迫从某个提供的基类派生一个类才能创建 NT 服务。这种方法本身没有错。缺点是用户需要花费时间深入研究基类的实现,以弄清楚如何正确地进行派生,并了解该类可以做什么和不能做什么。这种知识学起来很枯燥,而且容易忘记。几个月后,当您需要创建另一个服务时,很可能需要重新学习一遍……
背景
本文中的代码是用纯 C++ 编写的,并使用了一些 STL 类和 Win32 API 调用。
本文不包含创建 NT 服务的基础知识。另外,我认为阅读别人的 C++ 封装不是一个好的起点。对于初学者,我推荐 Yevgeny Menaker 撰写的一篇非常好的文章,可在此处找到。
实现细节
nt_service
类实现为 Meyer's singleton。Singleton 是一种特殊的类,在程序中只能有一个实例。为了解释它为什么是 singleton,我需要深入探讨一下服务创建过程。一个服务应用程序可以向服务控制管理器 (SCM) 传递多个服务主函数。支持这一点会导致类更复杂,但我从未需要创建处理两个或多个服务主线程的服务。因此,我将 nt_service
类实现为 singleton,以强化以下理念:使用此类,每个服务应用程序只能创建一个服务线程,因此不能有两个以上的实例。
此外,我在创建类时遵循了一些基本设计原则:
- 要求最少量的用户代码:这是通过要求不进行类派生来实现的。其用户可以专注于编写服务函数。
- 没有强制函数调用和设置:这一点未能实现,因为需要调用
nt_service::start
方法来启动服务。我放弃了这一原则,因为它导致了非常奇怪且难以阅读的代码。 - 保持类
interface
简洁直观。
最后,该类可以设置为支持 UNICODE (wchar_t
) 和非 UNICODE (char
) string
。
用法
nt_service
类是 singleton。因此,它的构造函数、复制构造函数和赋值 (=) 运算符都保持为 private
。要创建对 nt_service
类的访问点,请调用
// creates an access point to the service class
nt_service& service = nt_service::instance(L"my_service");
"
my_service
"
是该类在创建您的服务时使用的名称。它不是出现在服务控制管理器中的名称,仅用于内部。SCM 显示的服务名称在安装服务时设置。有关详细信息,请参阅“安装和管理您的服务”部分。
注册您的服务主函数
// register "my_service_main" to be executed as the service main function
service.register_service_main( my_service_main );
my_service_main
必须是 LPSERVICE_MAIN_FUNCTION
类型。此常量在 winsvc.h 中被“类型定义”为 void WINAPI (*)(DWORD, LPWSTR *)
(这是 UNICODE 版本,非 UNICODE 使用 LPSTR)。因此,您的服务主函数必须声明为完全相同。
当服务收到 SCM 的控制时,它可以执行三件事:它可以拒绝控制(默认行为),它可以接受控制但不执行任何操作,或者它可以接受控制并安排在收到控制时调用某个函数。
下面的示例演示如何仅接受某个命令。
// set the service to accept stop controls. Do nothing when it happens
service.accept_control( SERVICE_ACCEPT_STOP );
接受控制并注册在服务收到控制时调用的函数
// set the service to accept shutdown control and do something when received
service.register_control_handler( SERVICE_CONTROL_SHUTDOWN, my_shutdown_fcn );
最后,启动您的服务,请调用
service.start();
您必须尽快调用 start()
。如果 start()
函数未在 30 秒内被调用,SCM 将终止您的服务并报告错误。如果您的服务需要执行耗时的初始化,您不应将初始化代码放在服务主函数或主应用程序函数中。相反,您应该调用
service.register_init_function( my_init_fcn );
创建并放置初始化代码到 my_init_fcn
函数中是用户的任务。
有时用户需要从服务主函数中停止服务。下一个示例演示了如何做到这一点。
// stops the service, reporting -1 as exit code.
nt_service::stop( -1 );
stop()
方法必须使用完全限定名(nt_service::stop()
)调用,因为它是一个 static
方法。
安装和管理您的服务
安装服务时,我使用命令行工具 sc.exe。它是 Windows 资源工具包的一部分。假设您的服务应用程序名为 my_service.exe 且位于 c:\。以下示例显示如何创建一个服务并将其命名为 my_test_service
。
>sc.exe create my_test_service binpath= c:\my_service.exe
这是服务控制管理器用来引用您服务的名称。要启动您的服务,请调用
sc.exe start my_test_service
如果您希望您的服务在启动时自动启动,有两种方法可以设置:使用服务控制管理器(开始->控制面板->管理工具->服务)或在安装服务时使用“start= auto”选项。
>sc.exe create my_test_service binpath= c:\my_service.exe start= auto
技巧和陷阱
命令行参数
编写服务应用程序时,您需要处理两个不同的命令行:您的应用程序的命令行和您的服务主函数的命令行。您可以在使用 sc.exe 安装服务时将命令行参数传递给您的应用程序。
>sc.exe create my_test_service binpath= "c:\my_service.exe param1 param2 ..."
这些参数可以通过应用程序主函数的 argv[]
参数访问。要直接将参数传递给服务主函数,有两种方法:您可以在服务控制管理器中传递(或修改)它(仅当您的服务已安装时),或在使用 sc.exe 启动服务时传递。
>sc.exe start my_test_service param1 param2 .....
您可以通过服务主函数的 argv[]
参数以 usual way 访问这些参数。
编写您的服务主函数
这是在 nt_service
类内部执行用户服务主函数的代码段。
while ( service_status.dwCurrentState == SERVICE_RUNNING )
{
try
{
user_service_main( argc, argv );
}
catch(...)
{
nt_service::stop( -1 );
}
}
如上面的代码所示,只要服务正在运行,user_service_main
函数就会不间断地执行。如果您不在服务主函数中包含暂停它或将其置于等待状态的代码,它将一遍又一遍地被调用,直到您停止服务。除非您正在编写一个名为 cpuloader.exe 的服务应用程序,否则这样做不是一个好主意……
ACCEPT 和 CONTROL 常量,以及为什么它们容易混淆
用户在使用 nt_service
类时需要处理两种类型的常量:SERVICE_ACCEPT_*
常量和 SERVICE_CONTROL_*
常量。
当您想让服务仅接受某个控制时,可以使用 ACCEPT*
常量。特别是接受停止和暂停-继续控制非常有用。如果您不将这些控件设置为可接受,您的服务将无法停止或暂停。ACCEPT*
常量仅用作 accept_control()
方法的参数。
当您希望服务在收到命令时执行某些操作时,可以使用 CONTROL*
常量。为此,您需要调用 register_control_handler()
,并将控件类型(CONTROL*
常量之一)和回调函数作为参数传递。当然,当您选择处理一个控件时,您期望您的服务也接受该控件。而这正是该类所做的。
问题在于,很容易在需要 ACCEPT*
常量的地方使用 CONTROL*
常量,反之亦然。它们的名称和功能相似。CONTROL*
常量和 ACCEPT*
常量之间没有简单的对应关系。不同的控件与相同的 ACCEPT*
常量相关,有些则根本没有接受对应的常量。这就是为什么我们需要两者的原因。
因此,如果您的服务开始表现异常,请确保您在正确的位置使用了 ACCEPT*
和 CONTROL*
常量。这是一个微妙且常见的错误来源。
有人可能会指出,可以使用枚举来解决 CONTROL*
vs. ACCEPT*
常量问题。这是正确的,我计划在下一个版本中这样做。但我决定包含以上部分,因为我相信这可以帮助任何玩过 NT 服务的人,无论是否使用 nt_services
。
缺点和已知问题
到目前为止,我已识别出我的类有两个问题:
- 用户函数不是直接提供给服务控制管理器。而是提供类方法,并在其中调用用户函数。因此,每次调用用户的服务主函数时,都会产生一次函数调用开销。每次服务响应您的请求时也会发生这种情况。对于大多数服务应用程序来说,这是完全可以忽略的。
- 它没有实现 Windows API 提供的所有用于创建服务的功能。我选择仅实现最常见的功能以保持类的简单性。许多功能实际上非常高级且复杂,而且我承认,我并不完全掌握它们。
另一件可能被视为问题的事情是:该类不输出错误信息。问题在于,每个开发人员都有自己喜欢的错误日志记录方式:流、日志文件、OutputDebugString
调用……由于这是一个相当简单的主题,我让最终用户扩展类的功能来完成此操作。
最后,我不是英语母语者。因此,对于本文中的任何拼写错误和语法错误,我提前表示歉意。
历史
- 2007/06/18 - 发布第一个版本
- 2007/08/08 - 修复了一个设计缺陷:删除了大量全局变量,改用
private static
成员。