Window CE下编写网络服务程序—整理MSDN 内容

在 Windows CE 上实现网络服务
梁明辉 2007.11.10整理

发布日期 : 2/5/2005 | 更新日期 : 2/5/2005
John Spaith
Microsoft
适用于:
Microsoft Windows CE 4.0 和更新版本
摘要:本文重点讨论使用系统进程 services.exe 在 Windows CE 上编写网络服务。Services.exe 定义了一个接口,以使开发和管理服务变得更加容易。虽然本文重点讨论网络方案,但所提供的技术对于非网络服务也是有价值的。如果您希望在 Windows CE 上编写始终能够运行并且不需要直接访问物理设备的应用程序,则 services.exe 能够满足您的需要。
简介
当大多数人想到 Microsoft Windows CE 时,他们可能想到的第一个东西就是手持设备,如 Windows Mobile Pocket PC。很多人不知道的是,Windows CE 并非只能在个人数字助理 (PDA) 上运行。嵌入式设备的原始设备制造商 (OEM) 可以通过 Windows CE 5.0 中的一种相关产品(名为 Platform Builder)来使用它。
Windows CE 是一种模块式操作系统,它使 OEM 可以选择能够为它们的设备提供最佳支持的组件。例如,向电冰箱的温控系统中添加 Windows CE 的 OEM 可能会创建只带有超小内核的无头配置,而 Internet 设备的制造商却可能包含 Microsoft Internet Explorer 和 Web 服务器。
Windows CE 支持丰富的网络功能。在 Windows CE 中,有 TCP/IP、IrDA 和蓝牙协议栈,另外还有 Winsock、Winsock2、Wininet 和基于 MSXML3 SP1 的 XML 分析程序。在 Windows CE 4.1 中,添加了 IPv6 支持。尽管如此,由于它和 PDA 之间存在重大的联系,因此 Windows CE 通常只被视为网络事务中的客户端。可能令人吃惊的是,Windows CE 随附了许多服务器,包括 Web 服务器、通用即插即用 (UPnP) 和消息队列 (MSMQ)。
Services.exe 概述
尽管有这么多可供 Platform Builder 使用的出色功能(如 Internet Explorer 和 Microsoft DirectX 支持),但 Windows CE 在本质上是一个嵌入式操作系统。然而,它具有某些限制。一个限制是最多只能有 32 个进程同时运行。如果每个服务都在它自己的进程中运行,那么它们不仅会浪费资源,而且还会使系统更加接近它的 32 个进程最大限制。
在 Windows CE 3.0 和更低版本中,解决方案是将服务实现为在 device.exe 的上下文中运行的设备驱动程序。像其名称所暗示的那样,Device.exe 是 Windows CE 上所有设备驱动程序的主要宿主。(Device.exe 原来名为 horace.exe,但是由于某种原因,大多数人都没有将 horace 与设备驱动程序联系起来。)因此,在 Windows CE 3.0 和更低版本中,只要涉及到操作系统,Web 服务器就显示为设备驱动程序。让 device.exe 加载多个服务可减少同时运行的进程的数量,并且 device.exe 提供了一种方便的进程间通信手段(因为所有驱动程序都在相同的进程地址空间中运行)。
但是,使用 device.exe 也造成了一些问题。由于许多其他组件(包括网络协议栈和真正的设备驱动程序)都在 device.exe 中运行,因此这可能使跟踪资源泄漏变得非常困难。如果 device.exe 泄漏了内存,并且另外有 50 个设备驱动程序正在运行,那么如何找到出错的一方呢?更加糟糕的是,编写质量低下的 DLL 可能会破坏 TCP/IP 协议栈、PCMCIA 驱动程序或其他设备驱动程序中的内存,从而导致无法解释的故障。
创建 Services.exe 的目的就是为了处理这些问题。设计 Services.exe 是为了具有一个类似于 device.exe 的编程模型和应用程序编程接口 (API) 集,以便将实际上不是设备驱动程序的设备驱动程序(考虑一下 Web 服务器)从 device.exe 移动到 services.exe 中。如果您不熟悉 device.exe,请不必担心。本文将从基础知识开始讨论。如果您熟悉 Windows CE 上的设备驱动程序,则本文的很多内容将为您所熟悉。
Services.exe 提供了下列重要功能:
Services.exe 在系统启动时自动加载服务,并且只消耗一个进程。
Services.exe 导出一个 API 并且具有到文件系统 CreateFile 和 DeviceIoControl 函数的挂钩,这极大地简化了服务和设备上的其他应用程序之间的进程间通信。
可以将 services.exe 配置为侦听指定端口上的请求,它随后可以将请求转发给服务器。
使用该模型编写服务可以获得循序渐进的学习体验。
Windows CE 上的 services.exe 与 Windows XP 的 services.exe 有哪些共同之处?二者都用来承载其他服务(包括第三方服务)。它们具有相同的名称。但是,在 Windows CE 和 Windows XP 上,用于编写服务的 API 集和编程模型是完全不同的。
系统初始化:解释注册表
服务是在 DLL 中实现的。例如,Web 服务器位于 httpd.dll 中。有关必须导出的确切函数以及它们何时被调用的说明,请参阅实现服务的要素。
Services.exe 在 Windows CE 启动时自动启动。在启动时,它将枚举 HKEY_LOCAL_MACHINE\Services 的每个子项,其中每个子项都对应于一个服务。Services.exe 使用每个注册表项中的信息将服务加载到它的进程空间中,并调用该服务的初始化函数。
为了更具体地阐述该过程,本文包含了一个服务器,它实现了一个轻量级 Finger 服务器。该示例的灵感来自于 UNIX Finger 服务器,该服务器使用户可以查看远程计算机上的用户的状态。客户端可以运行一个命令(如 finger Bob@microsoft.com)来查询主机 microsoft.com,并请求用户 Bob 的状态。Bob 可以指定每当有人查找他时就返回的简单消息,如“I'm out to lunch now”或“Go Reds!”。
本文中的示例使用了一个比真正的 Finger 协议简单得多的协议。当远程客户端打开一个指向端口 79 上基于 Windows CE 的设备的 TCP 连接时,该基于 Windows CE 的设备将返回一个简单的消息。该消息可以由一个简单的 API 调用(也在以下示例中提供)动态更改。但是,单个用户无法创建自定义消息。返回的消息是为整个设备配置的。
因为该协议是如此简单,所以将不会包含任何自定义 Finger 客户端。相反,只需从装有 Windows XP 的计算机中,在命令提示处简单地运行 telnet yourCEDevice 79 以建立一个指向端口 79(而不是常用的 telnet 端口 23)的 TCP 连接。当 Windows CE Finger 服务器收到该 TCP 连接时,它会将设备消息发送给客户端。Telnet 客户端完全按照消息被接收时的内容回送该消息,然后退出。按照这种方式使用 telnet 客户端就可以满足本文的需要了。请记住,本文重点讨论在 Windows CE 上编写服务器,而不是客户端。
保密性如何呢?请考虑一下在设备上创建诸如 Finger 服务器之类的服务。Internet 上的每个人都需要能够获得 Bob 的状态吗?网络安全值得另外撰写一份白皮书(或有关该问题的书籍)加以讨论。至于现在,本文之所以提供该 Finger 服务器,是因为它易于理解,而不是因为它是保护隐私的值得效仿的示例。
您需要做的第一件事情是让 services.exe 知道如何加载您的 DLL。您需要的全部东西只是一个注册表条目:
HKEY_LOCAL_MACHINE\Services\FINGERSERVER
"Dll"=" fingerServer.dll"
"Order"=dword:10
"Prefix"="FIN"
"Index"=dword:0
"Context"=dword:1
有关这些注册表值的详细信息,请参阅 Services.exe Registry Settings for Windows CE 5.0 或 Services.exe Registry Settings for Windows CE 4.2。
以下为上述每个注册表值的简短说明:
"DLL" 只提供了实现该服务的 DLL 的名称。如果未提供完整路径,则 DLL 应当位于 \Windows 目录中。
“Order” 告诉 services.exe 按照哪种顺序加载服务(相对于 HKEY_LOCAL_MACHINE\Services 中的其他服务),其中,较低数字表示优先于较高数字。“Order” 值被设置为小于 10 的服务将在 fingerServer.dll 之前加载;“Order” 值大于 10 的服务将在 fingerServer.dll 之后加载。该值不必唯一,但是加载一系列具有相同次序的服务没有定义。
“Prefix” 指定服务的前缀。Services.exe 所关心的所有位于 fingerServer.dll 中的函数都必须具有前缀 FIN_。而且,希望通过使用 CreateFile 调用到服务中的应用程序会将文件名设置为 "FIN X :",其中 X 介于 0 和 9 之间(包括 0 和 9 在内)。
“Index” 是设备的初始文件系统引用的索引("FIN X :" 中的 X)。对于大多数服务(包括 Finger 服务器在内),可以将该值设置为 0。
“Context” 指定要传递给 FIN_Init(这是 fingerServer.dll 导出以处理它的初始化的一个函数)的初始 DWORD 值。值 1 是一个不可思议的值,services.exe 使用它来创建超级服务线程。
Services.exe 在启动过程中很晚才启动。到它被创建的时候,filesys.exe 和 device.exe 都已在运行。但是,这些程序在运行并不意味着,在服务初始化的时候,系统上的所有程序都在运行。设备可能还不具有动态主机配置协议 (DHCP) 地址,或者某个可移动的文件系统可能尚未启动。当您开发服务时,您需要知道这些限制。这些限制不会影响简单的 Finger 服务器。它可能会影响其他更为复杂的服务。例如,如果 Web 服务器在首次尝试打开它的日志文件时无法成功,那么它将休眠,然后重新尝试处理日志文件已经被配置为位于尚未初始化的文件系统上这一情况。
根据注册表值的不同,应用程序还可以通过编程方式来加载服务。要加载服务,请使用 ActivateService(LPWSTR szServiceName , DWORD dwReserved ),其中 szServiceName 是该服务在注册表中的名称。在本文包含的示例中,将使用 ActivateService("FINGERSERVER",0)。如果该服务已经卸载,则 ActivateService 是除重新启动设备以外重新加载该服务的唯一方式。
Services.exe 的应用程序接口
假设您已经编写、编译了 Finger 服务器并将其复制到设备中。当您观察它的时候,假设 serivces.exe 已经加载了它。应用程序如何使用该服务?该上下文中的应用程序意味着另一个程序,它在基于 Windows CE 的设备上运行,需要使用进程间通信来配置/控制/查询正在运行的服务的状态。它不是指想要检索该设备的消息的网络客户端。那已经通过 telnet yourCEDevice 79 解决了。
在 Windows CE 和 Windows XP 上,就像访问设备驱动程序一样,使用文件系统 API(如 CreateFile、WriteFile、ReadFile 和 DeviceIoControl)来访问服务。CreateFile 通过服务的名称调用,并且返回指向其他 API 访问的服务的句柄。DeviceIoControl 发送特定于服务和控制它的应用程序的自定义 I/O 控制调用。Services.exe 还定义了很多您可以根据情况来实现的 I/O 控制码,例如,用于停止或启动服务器的命令。这些 IOCTLS 保存在公共头文件 service.h 中 — 对于版本 4.0 和 4.1,该文件位于 Platform Builder 安装的 public\common\oak\inc 目录中;对于 4.2 和更高版本,该文件位于
public\common\sdk\inc 中。
在 Windows XP 中,可以将设备驱动程序命名为“\Device\TheFingerServer”、“\Device\ThisIsTheFingerServerAndItsReallyCool”或其他名称。在 Windows CE 中,其名称不能如此富于创造性,而这可能不是一件糟糕的事情。每个服务名称都被唯一标识,方法是向该服务的位于 HKLM\Services\{ServiceName} 下的 “Prefix” 和 “Index” 值中添加一个冒号。在 Finger 服务器示例中,服务的名称是 “FIN0:”。与 Windows XP 中不同,服务无需通过使用任何 API 将其自身显式注册为可由 CreateFile 调用。在服务已经由 services.exe 加载之后,应用程序将能够调用它。
那么,在这些 API 的背后发生了什么事情呢?当 CreateFile 获得的字符串具有三个字符、一个数字和一个冒号(如 “FIN0:”)时,它将假设它正在接收访问设备驱动程序或服务(而不是文件)的请求。Filesys.exe 将首先查询 device.exe,以查看它能否识别设备名称。如果它不能识别,则 filesys.exe 会调用到 services.exe 中。Services.exe 将在它的运行服务表中查找匹配项。如果 services.exe 找到了匹配项,它会创建一个句柄并将其返回到 filesys.exe,后者又将其返回到应用程序。当您用该句柄调用 DeviceIoControl、ReadFile 或类似的 API 时,内核将用该 API 的参数直接调用到 services.exe 中。
对于一些服务器而言,实现流接口(也就是说,它可以支持 ReadFile 和 WriteFile 操作)是有意义的。Telnet 服务器是一个经典示例。Telnet 将其本身设置为 cmd.exe 的 STDIN 和 STDOUT,因此它必须处理由 cmd.exe 生成并最终通过 filesys.exe API 执行的 scanf 和 printf 调用。
但是,在大多数情况下,使用 DeviceIoControl 是控制服务的最简单(通常也是唯一的)机制。DeviceIoControl 可以支持您定义的 IOCTL,并且同时采用输入和输出参数。ReadFile 和 WriteFile 无法同时采用参数。如果您希望检验流服务,请观察 Platform Builder 中的 %_WINCEROOT%\public\servers\sdk\samples\telnetd。为了防止降低 Finger 服务器的研究价值,没有将其实现为流服务。
在 Finger 服务器示例中,服务定义和实现了 IOCTL 代码,以获得和设置发送到 Finger 客户端的消息。您可以记录您使用的 IOCTL,并且让应用程序程序员直接向您的服务调用 CreateFile 和 DeviceIoControl 写入,但是还有一种更好的方式。您可以将文件系统调用的所有细节包装到一个小型库中,以供应用程序程序员链接。该方法正是 MSMQ 导出其接口的方法。例如,MQOpenQueue 是 msmqrt.lib 中的一个非常简短的函数,它将函数参数封送到对 DeviceIoControl 的调用中。该调用使应用程序程序员的工作变得更容易,并且还使您
的代码更易于移植到使用与 Windows CE 不同的进程间通信方法的平台。只需更改存根库的实现,调用应用程序就可以保持不变。下面给出了 Finger 运行时库的实现。
FingerRT.dll 库(RT 代表运行时)导出了两个函数。第一个函数是 SetFingerMessage。该函数告诉 Finger 服务器当客户端连接到它时显示什么消息。导出的另一个函数是 GetFingerMessage,它检索 Finger 服务器将返回给网络客户端的消息。
Fingerrt.dll Code:
[fingerService.h]
#define IOCTL_FINGER_SET_MESSAGE 1
#define IOCTL_FINGER_GET_MESSAGE 2
[FingerRT.cpp]
#include
#include
#include <..\inc\fingerService.h>
static HANDLE hInterface = INVALID_HANDLE_VALUE;
static int Initialize (void) {
if (hInterface == INVALID_HANDLE_VALUE) {
hInterface = CreateFile (L"FIN0:", 0, 0, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
}
return (hInterface != INVALID_HANDLE_VALUE);
}
static void UnInitialize(void) {
if (hInterface != INVALID_HANDLE_VALUE) {
CloseHandle(hInterface);
hInterface = INVALID_HANDLE_VALUE;
}
}
BOOL SetFingerMessage (CHAR *szMessage) {
if (!Initialize ())
return FALSE;
return DeviceIoControl (hInterface, IOCTL_FINGER_SET_MESSAGE, (void *) szMessage, strlen(szMessage)+1, NULL, 0, NULL, NULL);
}
BOOL GetFingerMessage (CHAR *szMessage, DWORD *pcchMessage) {
if (!Initialize ())
return FALSE;
return DeviceIoControl (hInterface, IOCTL_FINGER_GET_MESSAGE,NULL, 0, (void *) szMessage, *pcchMessage, pcchMessage, NULL);
}
实现服务的要素
服务导出以注册表中指定的 “Prefix” 值开头、并用定义良好的函数字符串终止的函数。以下为一个 fingerServer.dll .def 文件:
fingerServer.def:
LIBRARY FINGERSERVER
EXPORTS
FIN_Init
FIN_Deinit
FIN_Open
FIN_Close
FIN_IOControl
当 services.exe 对您的服务调用 LoadLibrary 时,加载程序将调用该服务的 DllMain 函数。DllMain 主要用于执行同步原语的创建(和销毁),并且在任何 Win32 编程方案中(而不仅仅是在服务和 Windows CE 中),DllMain 中都有很多不安全的操作。不要对 COM 或 Winsock 进行调用,不要创建线程或调用任何可能在另一个 DLL 中实现的函数。
在加载之后,services.exe 将调用 xxx _Init。(按照约定,xxx 表示服务的 “Prefix” 值,该值因服务而异;在该示例中,xxx=FIN。)Services.exe 将该服务的 “Context” 注册表值传递到函数中。xxx _Init 是对 COM、Winsock 或其他所有无法从 DllMain 调用的组件进行调用的地方。
在启动时,services.exe 在一个线程上创建服务,并阻塞它们对 xxx _Init 的调用。当您的服务在 xxx _Init 中执行时,没有其他服务可以启动,因此请不要在那里阻塞。相反,如果
您需要执行阻塞操作(例如 Winsock 接受调用),请让您自己的线程空转,或者,更好的办法是使用 services.exe 超级服务器功能来侦听传入的连接。
如果 xxx _Init 返回 FALSE,则服务会被卸载。假定它成功,则 services.exe 可能调用 xxx _IOControl 很多次以传入配置信息。Services.exe 生成的 IOCTL 代码(包括简短说明)位于头文件 service.h 中。您不必处理 service.h 中的所有 IOCTL(就该问题而言,您不必处理它们中的任何一个)。通过实现代码来支持服务管理 IOCTL(例如,IOCTL_SERVICE_STOP 和 IOCTL_SERVICE_START),您的服务的配置可以变得容易很多,并且可以使用与现有服务非常类似的接口。
当卸载服务实例时,会调用 xxx _DeInit。在您可以同时具有多个服务实例的情况(例如,telnet 具有“TEL0:”、“TEL1:”和“TEL2:”,每一个都对应于不同的 telnet 会话,并且当该特定实例关闭时,每一个都让 xxx _DeInit 对它们进行调用)下,xxx _DeInit 未必意味着 DLL 将要卸载。因为 Finger 服务器示例只创建“FIN0:”,所以被调用的 xxx _DeInit 告诉它 DLL 将要卸载。Services.exe 会忽略 xxx _DeInit 的返回值。在该函数返回之后,Finger 服务器始终会卸载。至关重要的一点是,当您的服务从 xxx _DeInit 返回时,它不会让任何辅助线程运行。在它返回之后,services.exe 会立即从内存中卸载您的服务 DLL。如果您具有仍在运行的辅助线程,则当它们尝试访问该 DLL 的已卸载代码页时,它们将崩溃。请让 xxx _DeInit 阻塞一秒钟、一个月或者一年,直到 Red Sox 赢得另一届 World Series 为止 — 但是在该服务准备好卸载之前,它无论如何都不应当返回。
只有当您要实现流服务(如 telnet 服务器)时,服务所导出的其余函数才会让您感到有趣。(Telnet 是一个非常有趣的示例,并且具有良好的注释,因此如果您要认真研究 Windows CE 服务,它可能是一个很好的研究主题。)当 services.exe 成功地将对 CreateFile("PrefixX:") 的调用映射到给定服务时,xxx _Open 将被调用。该示例必须返回 TRUE。返回 FALSE 将导致调用应用程序对 CreateFile 的调用失败。同样,xxx _Close 在对服务的句柄执行 CloseHandle 期间调用。只有当您创建流服务时,才需要实现 xxx _Read、xxx _Write 和 xxx _Seek。这些调用对应于 ReadFile、WriteFile 和 SetFilePosition。
Services.exe 提供最低限度的同步,因此所有同步都需要在您的服务中完成。尽管相关内容超出了本文的范围,但需要说明的是,实际上在某些情况下,可以从多个线程中同时调用 xxx _Init。不要将所有告诉您不要在该函数中阻塞的警告理解为可以忽视它的线程安全性。
以下为我们的 Finger 服务器中与网络无关的部分。稍后将讨论网络部分。
#include
#include
#include
// Use strsafe.h, rather that CRT string manipulation, since these
// functions are safer - especially when manipulating external strings
// such as those provided in IOCTL calls or those read from across the // network
#include
#include <..\inc\fingerService.h>
// Though not required, using a DWORD and macros defined in service.h
// to keep track of state allows for easier configuration.
DWORD g_dwServiceState;
// Buffer that contains what you'll send to network client on request.
CHAR g_szMessage[MAX_PATH];
// Length of string currently held in g_szMessage
DWORD g_ccMessage;
// Before the initial call to SetFingerMessage, use this as our default // response.
CHAR g_cszDefaultMsg[] = "Hello World! I am the sample Finger server!";
// Global critical section
CRITICAL_SECTION g_cs;
// Function that processes requests
extern "C" DWORD WINAPI FingerWorker(LPVOID lpv);
// Handle to the worker thread
HANDLE g_hWorkerThread;
// Use CE's built-in debugging framework.
#ifdef DEBUG
DBGPARAM dpCurSettings = {
TEXT("FingerServer"), {
TEXT("Error"),TEXT("Init"),TEXT("Network Client"),TEXT("Interface"),
TEXT("API"),TEXT(""),TEXT(""),TEXT(""),
TEXT(""),TEXT(""),TEXT(""),TEXT(""),
TEXT(""),TEXT(""),TEXT(""),TEXT("") },
0x0007 // Turn on Error, Init, and Client DEBUGZONE's by default
};
#define ZONE_ERROR DEBUGZONE(0)
#define ZONE_INIT DEBUGZONE(1)
#define ZONE_CLIENT DEBUGZONE(2)
#define ZONE_INTRF DEBUGZONE(3)
#define ZONE_API DEBUGZONE(4)
#define ZONE_NET DEBUGZONE(5)
#endif
extern "C" BOOL WINAPI DllEntry(HANDLE hInstDll, DWORD fdwReason,
LPVOID lpvReserved) {
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
g_dwServiceState = SERVICE_STATE_UNINITIALIZED;
InitializeCriticalSection (&g_cs);
// This DLL does not require thread attatch/deatch
DisableThreadLibraryCalls((HMODULE)hInstDll);
// Hook into CE system debugging
DEBUGREGISTER((HINSTANCE)hInstDll);
break;
case DLL_PROCESS_DETACH:
DeleteCriticalSection (&g_cs);
break;
}
return TRUE;
}
extern "C" DWORD FIN_Init (DWORD dwData) {
DEBUGMSG(ZONE_INTRF,(L"FINGERD: FIN_Init(0x%08x) called\r\n",dwData));
EnterCriticalSection (&g_cs);
if (g_dwServiceState != SERVICE_STATE_UNINITIALIZED) {
// Someone is trying to load multiple times (for example,
// trying to create "FIN1:").
// Not supported, so fail to load service.
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: Finger service already "
L"initialized on FIN_Init() call\r\n"));
LeaveCriticalSection (&g_cs);
return 0;
}
StringCchCopyA(g_szMessage,sizeof(g_szMessage),g_cszDefaultMsg);
g_ccMessage = strlen(g_cszDefaultMsg);
g_dwServiceState = SERVICE_STATE_STARTING_UP;
DEBUGMSG(ZONE_INIT,(L"FINGERD: FIN_Init success - service state is in "
L"starting up state\r\n"));
LeaveCriticalSection (&g_cs);
return 1;
}
extern "C" BOOL FIN_Deinit(DWORD dwData) {
DEBUGMSG(ZONE_INTRF,(L"FINGERD: FIN_DeInit(0x%08x) called\r\n",dwData));
EnterCriticalSection (&g_cs);
g_dwServiceState = SERVICE_STATE_UNLOADING;
// If worker threads are active, you MUST block until they
// have shutdown. Real services may take extra action to force
// the threads to end (that is, closing all open sockets workers
// are using) to speed up the shutdown process.
// This function is also the proper place to do
// uninitialization of other components that are not safe
// to be called from DllMain - that is, COM, Winsock, and so on
if (g_hWorkerThread) {
DEBUGMSG(ZONE_INIT,(L"FINGERD: Waiting for worker thread to complete "
L"before service shutdown\r\n"));
HANDLE hWorker = g_hWorkerThread;
LeaveCriticalSection (&g_cs);
// Block until the worker is through running.
WaitForSingleObject(hWorker,INFINITE);
}
else {
LeaveCriticalSection (&g_cs);
}
// Service is unloaded no matter what is returned.
DEBUGMSG(ZONE_INIT,(L"FINGERD: Completed shutdown. Returning to "
L"services.exe for unload\r\n"));
return 1;
}
extern "C" DWORD FIN_Open (DWORD dwData, DWORD dwAccess,
DWORD dwShareMode)
{
// Handles calls application made to CreateFile.
// Since no client state is maintained, no further work is required // here.
DEBUGMSG(ZONE_INTRF,(L"FINGERD: FIN_Open(0x%08x,0x%08x,0x%08x) called\r\n",
dwData,dwAccess,dwShareMode));
return 1;
}
extern "C" BOOL FIN_Close (DWORD dwData)
{
// Handles calls application made to CloseHandle.
// dwData is the value returned during the call to FIN_Open
// This is where resources allocated in the dwData handle
// (had there been any) would be freed.
DEBUGMSG(ZONE_INTRF,(L"FINGERD: FIN_Close(0x%08x) called\r\n",dwData));
return 1;
}
extern "C" BOOL FIN_IOControl(DWORD dwData, DWORD dwCode, PBYTE pBufIn,
DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut,
PDWORD pdwActualOut)
{
DWORD dwError = ERROR_INVALID_PARAMETER;
DEBUGMSG(ZONE_INTRF,(L"FINGERD: FIN_IOControl(0x%08x,0x%08x,0x%08x,0x%08x,"
L"0x%08x,0x%08x,0x%08x) called\r\n",
dwData, dwCode, pBufIn, dwLenIn,
pBufOut, dwLenOut, pdwActualOut));
EnterCriticalSection (&g_cs);
switch (dwCode) {
// Control code sent to start a service. (services start FIN0:)
case IOCTL_SERVICE_START:
// In real services, you need to be very careful about
// state changes and timing issues, and you'll almost
// certainly do more work than just changing a var's value.
// Finger server will accept connections.
if (g_dwServiceState != SERVICE_STATE_OFF) {
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: IOCTL_SERVICE_START fails "
L"because service is not off. "
L"Current State=<%d>\r\n",g_dwServiceState));
dwError = ERROR_SERVICE_ALREADY_RUNNING;
}
else {
DEBUGMSG(ZONE_INIT,(L"FINGERD: Service state change to on\r\n"));
g_dwServiceState = SERVICE_STATE_ON;
dwError = ERROR_SUCCESS;
}
break;
// Control code sent to refresh a service. (services refresh FIN0:)
case IOCTL_SERVICE_REFRESH:
// In real services, you need to be very careful about
// state changes and timing issues, and you'll almost
// certainly do more work than just changing a var's value.
if (g_dwServiceState != SERVICE_STATE_ON) {
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: IOCTL_SERVICE_REFRESH "
L"fails because service state is not on. "
L"Current State=<%d>\r\n",g_dwServiceState));
dwError = ERROR_SERVICE_NOT_ACTIVE;
}
else {
DEBUGMSG(ZONE_INIT,(L"FINGERD: Service stopping on a refresh\r\n"));
g_dwServiceState = SERVICE_STATE_SHUTTING_DOWN;
// Shut the service down, re-read configuration (if we have // any),
// and then restart.
DEBUGMSG(ZONE_INIT,(L"FINGERD: Service restarting on a refresh\r\n"));
g_dwServiceState = SERVICE_STATE_ON;
dwError = ERROR_SUCCESS;
}
break;
// Control code sent to stop a service. (services stop FIN0:)
case IOCTL_SERVICE_STOP:
// No longer accept new network connections. Close existing // connections
// In real services, you need to be very careful about
// state changes and timing issues, and you'll almost
// certainly do more work than just changing a var's value.
// Finger Server will stop accepting new connections.
if (g_dwServiceState != SERVICE_STATE_ON) {
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: IOCTL_SERVICE_STOP fails "
L"because service state is not on. "
L"Current State=<%d>\r\n",g_dwServiceState));
dwError = ERROR_SERVICE_NOT_ACTIVE;
}
else {
DEBUGMSG(ZONE_INIT,(L"FINGERD: Service stopping\r\n"));
g_dwServiceState = SERVICE_STATE_OFF;
dwError = ERROR_SUCCESS;
}
break;
// An application (possibly services.exe itself) is
// querying for the service's running state.
case IOCTL_SERVICE_STATUS:
// No need for critical section since this is an atomic read.
__try {
if (pBufOut && dwLenOut == sizeof(DWORD)) {
*(DWORD *)pBufOut = g_dwServiceState;
if (pdwActualOut)
*pdwActualOut = sizeof(DWORD);
dwError = ERROR_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR Invalid pointer "
L"passed on IOCTL_SERVICE_STATUS\r\n"));
dwError = ERROR_INVALID_PARAMETER;
}
break;
// Handle API call to SetFingerMessage.
case IOCTL_FINGER_SET_MESSAGE:
if (pBufIn == NULL)
break;
__try {
// Do NOT trust dwLenIn, since a malicious app may lie
// Calculate size of buffer required based on buffer // itself.
// Use a try block because you should not trust an // application to have
// passed a valid buffer and do not want it to be able to // put
// this service into an invalid state.
size_t dwStrLen;
if (FAILED(StringCchLengthA((CHAR*)pBufIn,sizeof(g_szMessage),&dwStrLen)))
break;
if (FAILED(StringCchCopyA(g_szMessage,sizeof(g_szMessage),(CHAR*)pBufIn)))
break;
g_ccMessage = dwStrLen;
dwError = ERROR_SUCCESS;
DEBUGMSG(ZONE_API,(L"FINGERD: SetFingerMessage() changed message "
L"to <%S>\r\n",g_szMessage));
}
__except (EXCEPTION_EXECUTE_HANDLER) {
// Invalid buffer. Reset message to system default.
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR Invalid pointer passed "
L"on SetMessage\r\n"));
StringCchCopyA(g_szMessage,sizeof(g_szMessage),g_cszDefaultMsg);
g_ccMessage = strlen(g_cszDefaultMsg);
dwError = ERROR_INVALID_PARAMETER;
}
break;
// Handle API call to GetFingerMessage.
case IOCTL_FINGER_GET_MESSAGE:
if (pBufOut==NULL)
break;
if (dwLenOut < (g_ccMessage+1)) {
dwError = ERROR_MORE_DATA;
break;
}
__try {
if (FAILED(StringCchCopyA((CHAR*)pBufOut,dwLenOut,g_szMessage)))
break;
if (pdwActualOut)
*pdwActualOut = g_ccMessage+1;
dwError = ERROR_SUCCESS;
DEBUGMSG(ZONE_API,(L"FINGERD: GetFingerMessage() retrieved "
L"message <%S>\r\n",g_szMessage));
}
__except (EXCEPTION_EXECUTE_HANDLER) {
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR Invalid pointer passed "
L"on GetMessage\r\n"));
dwError = ERROR_INVALID_PARAMETER;
}
break;
}
if (dwError != ERROR_SUCCESS)
SetLastError(dwError);
LeaveCriticalSection(&g_cs);
return (dwError==ERROR_SUCCESS);
}
以上就是全部内容。哦,除了实际侦听网络以处理请求(这是我们一开始就需要完成的任务)的细枝末节以外。
超级服务
回想一下在 device.exe 中运行的时期,每个网络服务都让它自己的线程空转来接受传入的连接。当连接到达时,接受线程只是衍生一个辅助线程来进行处理。虽然该方法比让每个服务具有一个进程好,但仍不理想。如果有多个服务侦听传入的连接,则让每个服务都具有一个线程以侦听传入的连接会很浪费。
上述方法的另一个缺点是配置。假设您希望 telnet 服务器在端口 553 上侦听,或者您希望 Web 服务器在端口 442 到 490 上侦听。您将完全受服务实现者的支配,并且需要提供正确的接口以对此进行设置 — 如果有一个这样的接口的话。它还增加了每个服务管理它自己的接受线程的复杂性,尤其是在尝试停止正在运行的服务的所有线程时。
超级服务器解决了上述所有问题。Services.exe 在系统启动时让一个线程空转,并且为请求它的服务侦听很多个(最多 64 个)套接字,而不是让每个服务都在自己的线程上接受连接。当传入的请求到达时,services.exe 确定与传入连接相关联的服务并且通知该服务。TCP 端口和服务之间的映射是通过注册表(在基础服务密钥下)配置的。在 Finger 服务器示例中,注册表如下所示:
HKEY_LOCAL_MACHINE\Services\FINGERSERVER\Accept\TCP-79"SockAddr"=hex:
02,00,00,4F,00,00,00,00,00,00,00,00,00,00,00,00
sockAddr 注册表值是适当协议的 SOCKADDR 结构的字节对应值,并且包含地址族和要侦听的端口。在 Windows CE 版本 4.0 中,超级服务器将只侦听 IPv4 TCP 连接,而不会侦听 UDP 和其他协议。在 Windows CE 版本 4.1 和更高版本中,services.exe 还会侦听 IPv6 TCP 地址。在前面的注册表示例中,services.exe 被命令侦听 IPV4 端口 79,并将请求转发给 Finger 服务器。注册表项的名称 (TCP-79) 是任意的。SockAddr 注册表值是 services.exe 读取的值,而不是项名称。考虑到 services.exe 的各个方面,可以将该注册表项命名为 Horace。
通过调用服务的 xxx _IOControl 来向其通告传入的连接。IOCTL_SERVICE_CONNECTION 被设置为传入的代码,而 pBufIn 参数是指向传入套接字的指针。Services.exe 在调用 xxx _IOControl 之前对该套接字调用 Accept。此时,服务使一个辅助线程空转以处理请求。服务完成该工作是非常重要的。对服务的超级服务器处理进行的所有调用都是在一个线程上执行的。在收到 IOCTL_SERVICE_CONNECTION 时阻塞其 xxx _IOControl 的服务可以防止所有其他服务接收传入的连接。
要在 Finger 服务器中实现该机制,请使用以下代码。
extern "C" BOOL FIN_IOControl(DWORD dwData, DWORD dwCode, PBYTE pBufIn,
DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut,
PDWORD pdwActualOut)
{
switch (dwCode) {
// COPY AND PASTE THESE STATEMENTS INTO THE
// FIN_IOControl CODE ABOVE
// A new listen socket has been created and associated
// with this service.
case IOCTL_SERVICE_REGISTER_SOCKADDR:
if ((g_dwServiceState == SERVICE_STATE_STARTING_UP) ||
(g_dwServiceState == SERVICE_STATE_ON)) {
dwError = ERROR_SUCCESS;
DEBUGMSG(ZONE_NET,(L"FINGERD: SOCKADDR registered\r\n"));
}
else {
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR SOCKADDR registration "
L"failed because state != on or starting up. "
L"State = <%d>\r\n",g_dwServiceState));
dwError = ERROR_SERVICE_NOT_ACTIVE;
}
break;
// A socket associated with this service is closed.
case IOCTL_SERVICE_DEREGISTER_SOCKADDR:
// Since finger service doesn't maintain any state
// based on registered sockets, no action is required here.
DEBUGMSG(ZONE_NET,(L"FINGERD: SOCKADDR deregistered\r\n"));
dwError = ERROR_SUCCESS;
break;
// Services.exe has completed super-service init for this service. Incoming
// Super-service connections can come in at anytime now.
case IOCTL_SERVICE_STARTED:
// In real services, you need to be very careful about
// state changes and timing issues, and you'll almost
// certainly do more work than just changing a var's value.
// Finger Server will accept connections.
if (g_dwServiceState != SERVICE_STATE_STARTING_UP) {
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: IOCTL_SERVICE_STARTED "
L"failed because state != starting up. State=<%d>\r\n",
g_dwServiceState));
dwError = ERROR_SERVICE_ALREADY_RUNNING;
}
else {
DEBUGMSG(ZONE_INIT,(L"FINGERD: IOCTL_SERVICE_STARTED changed "
L"state to SERVICE_STATE_ON\r\n"));
g_dwServiceState = SERVICE_STATE_ON;
dwError = ERROR_SUCCESS;
}
break;
// A new incoming request on a super-service port has arrived.
case IOCTL_SERVICE_CONNECTION:
{
if (dwLenIn != sizeof(SOCKET)) {
dwError = ERROR_INVALID_PARAMETER;
break;
}
SOCKET sock;
__try {
sock = * ((SOCKET*)pBufIn);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
dwError = ERROR_INVALID_PARAMETER;
break;
}
if (g_dwServiceState != SERVICE_STATE_ON) {
// Do not accept connections unless the service is on
// ALWAYS close the socket, even if you return
// FALSE. Services.exe leaves it to the
// service to close the socket in all cases.
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: Service Conn fails because "
L"Service State != ON. State = <%d>\r\n",g_dwServiceState));
closesocket(sock);
dwError = ERROR_SERVICE_NOT_ACTIVE;
break;
}
if (g_hWorkerThread != NULL) {
// For simplicity, only allow one connection at a time.
// Most real servers cannot be this simple.
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: Finger service is "
L"servicing another connection\r\n"));
closesocket(sock);
dwError = ERROR_BUSY;
break;
}
g_hWorkerThread = CreateThread(NULL, 0, FingerWorker,
(LPVOID*) sock, 0, NULL);
if (g_hWorkerThread == NULL) {
DEBUGMSG(ZONE_ERROR,(L"FINGERD: ERROR: CreateThread fails. "
L"GetLastError() returns <0x%08x>\r\n",GetLastError()));
closesocket(sock);
dwError = ERROR_INTERNAL_ERROR;
break;
}
// Worker thread will close the socket.
DEBUGMSG(ZONE_CLIENT | ZONE_NET,(L"FINGERD: Spinning up a worker thread "
L"handle=<0x%08x> to service incoming request\r\n",
g_hWorkerThread));
dwError = ERROR_SUCCESS;
}
break;
}
}
extern "C" DWORD WINAPI FingerWorker(LPVOID lpv) {
// In real services, you'd call select() and recv() and process
// the request based on client input. For instance,
// the real Finger protocol has a mechanism of specifying that
// the client wants information about the user "Bob".
// To keep things simple, always send the same message.
SOCKET sock = (SOCKET)lpv;
// In real services, the worker will release the global critical
// section quickly (if it needs it at all)
EnterCriticalSection (&g_cs);
DEBUGMSG(ZONE_CLIENT,(L"FINGERD: Sending message <%S> to client\r\n",g_szMessage));
send(sock, g_szMessage, g_ccMessage,0);
closesocket(sock);
CloseHandle(g_hWorkerThread);
g_hWorkerThread = NULL;
LeaveCriticalSection (&g_cs);
return 1;
}
}
Services.exe 命令行接口和服务配置
太好了!Finger 服务器现在可以运行了!但是,假设您希望停止它然后再启动它,应该怎么做呢?或者,假设在调试期间,您希望卸载它,重新编译,然后重新加载经过修改的 DLL,应该怎么做呢?请观察前文中介绍的 FIN_IOControl,您将注意到 Finger 服务器已经实现了名为 SERVICE_IOCTL_STOP 和 SERVICE_IOCTL_START 的 IOCTL。
按照约定,当服务收到 SERVICE_IOCTL_STOP 时,它应当停止它正在执行的任何操作。如果存在打开的网络连接,则它们应当关闭,并且就网络服务而言,不应当接受新的连接。SERVICE_IOCTL_START 会使已经停止的服务重新启动。这些 IOCTL 不会导致服务加载或卸载。您可以将 IOCTL 视为暂停和继续执行服务的请求。如果服务愿意,则可以完全忽略这些 IOCTL。
卸载服务有一点儿麻烦。首先,您需要 services.exe 在最初加载该服务时创建的句柄。要获得该句柄,请调用 GetServiceHandle("FIN0:") 并将返回值传递给 DeregisterService。该句柄与您在调用 CreateFile("FIN0:") 时获得的句柄不同。实际上,它甚至不是真正的内核句柄,因此无需调用 CloseHandle。代码要比说明更简单:
HANDLE h = GetServiceHandle("FIN0:");
if (h != INVALID_HANDLE_VALUE)
DeregisterService(h);
您可以编写一个对 DeviceIoControl 或 DeregisterService 进行适当调用的小型应用程序,以便配置服务。好消息是您无需这么做。在 Windows CE 设备上的命令提示窗口中,可以用命令行参数运行 services.exe 以配置任何正在运行的服务,前提是该服务实现了 service.h 中的适当 IOCTL。通过运行 services help 可以找到所有选项的说明。
下面是其中一些选项:
services list
FIN0: 0x00040d60 FingerServer.dll Running
现在停止该服务。当服务停止时,services.exe 将停止侦听绑定到该服务的任何超级服务套接字。同样,当服务重新启动时,services.exe 将再次侦听服务。
services stop FIN0:
services start FIN0:
To unload:
services unload FIN0:
在服务卸载之后加载该服务与其他管理选项稍有不同。因为 services.exe 在它的运行表中不再具有 “FIN0:”,所以我们必须通过服务在 HKLM\Services\{ServiceName} 注册表字段中的条目来引用它。
services load FINGERSERVER
因为本文已经如此热烈地讨论了 telnet 服务器,所以令人宽慰的是,现在终于可以使用
它了。运行 services.exe 命令行的示例是从在 Windows CE 设备上运行的 telnet 会话中复制并粘贴的。
Welcome to the Windows CE Telnet Service on jspaithcepc3
Pocket CMD v 4.2
\> services list
FIN0: 0x00040d60 fingerServer.dll Running
HTP0: 0x0004cd00 HTTPD.DLL Running
TEL0: 0x00052da0 TELNETD.Dll Running
TEL1: 0x0005cf90 telnetd.dll Running
FTP0: 0x0004ebd0 FTPD.Dll Off
MMQ1: 0x00057830 MSMQD.Dll Off
NTP0: 0x0005b1c0 timesvc.dll Running
\>
接下来,停止并立即重新启动 Finger 服务器。在该示例中,Services.exe 只是向指定的服务发送 IOCTL_SERVICE_STOP 或 IOCTL_SERVICE_START。如果有错误,则 Services.exe 只会打印文本。第二次试图停止 FIN0: 时失败,因为它已经停止了。
\> services stop FIN0:
\> services stop FIN0:
Operation failed. Error code 0x00000426
\> services start FIN0:
\>
现在,卸载然后重新加载 Finger 服务器。请记住,如果成功,则不会显示任何文本。
\> services unload FIN0:
\> services unload FIN0:
FIN0: is not a valid service
\> services load FINGERSERVER
维护状态
Finger 服务器示例具有很少可供跟踪的状态 — 只有一个代表它的运行状态的状态 DWORD,以及一个包含要发送到查询它的客户端的数据的缓冲区。并非所有服务都如此简单。以 telnet 服务器为例。对于每个 telnet 会话,都会创建 cmd.exe 的一个副本以处理用户输入的命令。在 services.exe 内部,telnet 服务器为每个会话维护它的连接套接字、最近键入的命令的历史记录、与该会话相关联的 cmd.exe 进程的句柄以及其他状态变量。通常,服务在它的 xxx _Open 实现中分配一些资源。它将在它的 xxx _Close 实现中释放这些资源。因为这些资源直接映射到内核句柄,所以如果进程关闭时没有显式对这些句柄调用 CloseHandle,则仍将由 services.exe 通过 xxx _Close 调用该服务。服务在任何阶段都应当准备好让它的调用进程关闭。
如果进程即将死亡,但是让某个线程在调用到服务中的时候阻塞,则会发生另一种棘手的情况。例如,假设 cmd.exe 在对由 telnet 服务器返回的句柄调用 ReadFile 时阻塞。在调用到 services.exe 中时阻塞的辅助线程返回之前,内核不会杀死 cmd.exe。为了警告 telnet 服务器它需要开启与即将死亡的进程相关联的调用方,内核会向 telnet 服务器发送 IOCTL_PSL_NOTIFY IOCTL。有关所有这一切的详细信息,请参阅关于 IOCTL_PSL_NOTIFY 的 MSDN 文档。
如果您的服务从来不让可能在很长时间内阻塞的代码调用到它的内部(我们的示例就是这种情况),则您无需关心该 IOCTL。
小结
本文应当传达了两个意思。首先,您应当开始将 Windows CE 视为一个网络服务平台。该平台提供了丰富的基础结构,包括 Web 服务器、XML、SOAP 和 MSMQ。其次,在编写低级别服务时,应当考虑为 services.exe 框架编写该服务。Services.exe 简化了与编写服务有关的管理和开发过程,从而使服务实现者和任何利用它所提供的扩展的程序员的工作变得更加容易。

posted on 2007-11-17 14:46 梁明辉 阅读(596) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航:   博客园   博客园最新博文   博问   管理


导航

<2026年6月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

统计

常用链接

留言簿

文章档案

搜索

最新评论