在 Windows CE 上实现网络服务2

本文探讨了超级服务器如何优化网络服务管理,通过一个线程空转并监听多个套接字,减少资源浪费和简化配置过程。特别介绍了如何通过注册表配置端口和服务之间的映射,以及服务如何通过超级服务器接口处理传入连接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 超级服务

回想一下在 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_STOPSERVICE_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);

您可以编写一个对 DeviceIoControlDeregisterService 进行适当调用的小型应用程序,以便配置服务。好消息是您无需这么做。在 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_STOPIOCTL_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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值