65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.66/5 (15投票s)

2007年6月18日

CPOL

8分钟阅读

viewsIcon

46177

downloadIcon

1199

用几行代码创建一个 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,以强化以下理念:使用此类,每个服务应用程序只能创建一个服务线程,因此不能有两个以上的实例。

此外,我在创建类时遵循了一些基本设计原则:

  1. 要求最少量的用户代码:这是通过要求不进行类派生来实现的。其用户可以专注于编写服务函数。
  2. 没有强制函数调用和设置:这一点未能实现,因为需要调用 nt_service::start 方法来启动服务。我放弃了这一原则,因为它导致了非常奇怪且难以阅读的代码。
  3. 保持类 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

缺点和已知问题

到目前为止,我已识别出我的类有两个问题:

  1. 用户函数不是直接提供给服务控制管理器。而是提供类方法,并在其中调用用户函数。因此,每次调用用户的服务主函数时,都会产生一次函数调用开销。每次服务响应您的请求时也会发生这种情况。对于大多数服务应用程序来说,这是完全可以忽略的。
  2. 它没有实现 Windows API 提供的所有用于创建服务的功能。我选择仅实现最常见的功能以保持类的简单性。许多功能实际上非常高级且复杂,而且我承认,我并不完全掌握它们。

另一件可能被视为问题的事情是:该类不输出错误信息。问题在于,每个开发人员都有自己喜欢的错误日志记录方式:流、日志文件、OutputDebugString 调用……由于这是一个相当简单的主题,我让最终用户扩展类的功能来完成此操作。

最后,我不是英语母语者。因此,对于本文中的任何拼写错误和语法错误,我提前表示歉意。

历史

  • 2007/06/18 - 发布第一个版本
  • 2007/08/08 - 修复了一个设计缺陷:删除了大量全局变量,改用 private static 成员。
© . All rights reserved.