用于多客户端的简单、健壮且可扩展的 Winsock 服务器,易于添加新服务






4.93/5 (19投票s)
如何构建一个简单、健壮且易于扩展的多客户端服务器
引言
我创建这个客户端服务器项目是为了争取在一家美国公司位于新西伯利亚(俄罗斯)分支机构的工作。他们承认我的项目是他们收到的最好的项目,因此我相信它对 Code Project 的会员来说会很有用。
我之所以相信该项目可能有用,还有另一个原因:在实现它之前,我研究了 Code Project 提供的解决方案,可能是我这辈子第一次,我在这个精彩的网站上没有找到一个可以成为我项目起点的解决方案,而且实现起来足够简单,大约一周就能完成。
该项目的主要目标是创建一个简单、健壮、小巧、易于扩展且易于实现的多客户端服务器。服务器只有大约 100 KB,实现为一个 Microsoft Windows 服务,可自我安装,且不使用 MFC。客户端是基于 MFC 对话框的,但它不使用 MFC 套接字类。
Tasks(任务)
作为用户服务(我将称之为“任务”,以避免与 Microsoft Windows 服务应用程序混淆)的示例,我使用了
- 获取服务器时间
- 从服务器获取指定目录及其所有子目录中的文件和目录列表。同时也会显示文件大小。
添加新任务
要添加一个新任务,只需编写一个用于在客户端存储和显示数据的类(例如,请参阅 `CTimeTask`),一个用于在服务器端获取数据的类(例如,请参阅 `CTimeTskN`),以及在 `CVMprotocolCr` 类中添加三个类似 XML 的标签。所有这些类都不知道网络和套接字。
负责连接和数据传输的工作线程(例如,在 `vmSrvr` 中查看 `LStnrThrdN.cpp`)则不知道任务。它们通过任务类工厂(例如,请参阅 `CTskFbrckN`)提供的抽象类指针与任务进行交互。工作网络线程主要使用抽象任务类的一个虚拟函数,其意义是“执行它”。因此,您无需修改工作线程。
您还应该在客户端 (`CTskFbrck`) 和服务器 (`CTskFbrckN`) 的任务工厂中添加一行来创建您的新任务。
无需修改其他类,除了客户端 `interface`,我们只需要添加一个新的按钮,并且可能根据需要添加命令 `string`。
服务器
服务器实现为一个 Microsoft Windows 服务。要安装,请使用 `cmd` 文件或在命令行中输入 `vmSrvr.exe -i 5105`,其中 `5105` 是端口(您可以使用任何端口代替 `5105`)。如果未指定端口,则默认端口为 `5105`。安装后,服务将立即启动。它也会在 Microsoft Windows 启动时自动启动。
要卸载,请使用 `cmd` 文件或在命令行中输入:`vmSrvr.exe -u`。
服务器可靠性
为了使服务器可靠,所有套接字都以异步模式使用,因此服务器永远不会无限期挂起。为避免处理器过载,工作线程会等待超时。为了等待新连接、数据接收端口可用以及发送数据,使用带有相应超时设置的 `select` 函数,因此服务器响应迅速。
为避免因长时间关闭套接字而挂起,我使用套接字进行强制连接模式,在 `linger` 结构中设置相应变量(例如,请参阅 *LStnrThrdN.h* 中的线程 `LstnrThrdN`)。
主服务器循环(请参阅 *yvrrn.cpp* 文件中的 `ServiceMain`)位于 `try` 块中,因此服务器在发生致命错误时可以自行重启。
清除服务器资源
大多数资源都封装在更安全的类中,并在函数退出或对象销毁时在析构函数中自动释放。大多数更安全的类位于 *vmSafeWinSock.h*(*common* 项目文件夹)中。
当客户端断开连接或关闭时,它会向服务器发送命令,相应的线程负责与客户端通信并退出,清除其所有资源。
然而,即使客户端被异常终止,例如通过终止进程或关闭计算机电源,服务器也会在大约 15 秒内断开连接并清除资源。
为了检测客户端何时断开连接,使用了文档记录不佳的一个技巧。如果我们使用 `select` 来检测端口何时可读,那么 `recv` 函数在连接存在时总是返回非零字节。在连接断开的情况下,它会立即返回零字节(请参阅 `CCnctnVMN::Recieve()` 函数及其内部注释)。此解决方案在以下常规实际条件下进行了测试。客户端和服务器计算机位于不同的网络中,没有直接连接。实际上,我使用了两个相互竞争的独立互联网提供商。因此,计算机没有直接连接,并使用了独立的交换机和其他设备连接到互联网。客户端和服务器都在 Windows XP 下运行。客户端在 Windows 任务管理器中被终止。服务器在事件发生后约 15 秒内按预期以绝对常规的方式清除资源。当然,始终有可能开发一个防火墙来欺骗服务器,在客户端崩溃后模仿连接。在这种情况下,您才需要自定义解决方案,但我认为这超出了本文的范围。
主服务器循环每 5 秒检查一次哪些连接线程已退出(通过其存储在 `CThrdPullVMN` 中的句柄,请参阅 `CVMObsrvr::Obsrv()` 函数),并关闭已退出线程的句柄。
服务器同步
为了简化,我没有使用 I/O 完成端口、Microsoft Windows 事件和回调进行同步。相反,我使发送和接收数据到客户端的线程彼此独立,并且独立于主程序,以至于它们几乎不需要任何同步。它们还使用 `select` 函数来查看数据是否可用或端口是否易于发送。
实际上,服务器只使用一个事件来停止所有线程,以及一个关键部分来检查线程是否处于活动状态。线程每约 5 秒定期检查一次停止事件,如果设置了停止事件则退出。停止事件用于在服务被服务控制管理器停止时(请参阅 *yvrrn.cpp* 文件中的 `ServiceMain`)优雅地关闭所有线程。
服务器类和主函数列表
所有函数和类都有注释,我相信代码很容易理解。研究服务最方便的起点是文件 *YVRRN.CPP*。一些小型且易于理解的辅助类已从列表中省略。
- *yvrrn.cpp* 是服务的श्मुख्य启动文件。在此文件中,您会找到启动服务的两个主函数 `ServiceMain` 和 `main`。
- `CVMObsrvr` 负责启动、停止和清除工作服务集。最重要的函数是 `CVMObsrvr::Obsrv()`。这尤其会启动监听器线程,该线程监听新的客户端连接。
- *LStnrThrdN.cpp* 包含 `CLStnrThrdN` 类,该类启动监听器线程函数 `LstnrThrdN`。`LstnrThrdN` 监听新的客户端连接,并为每个客户端启动线程函数 `WrkTrhdVMN`。所有这些函数都在同一个 *LStnrThrdN.cpp* 文件中。
- `CCnctnVMN` 负责向客户端发送数据和接收数据。它不知道它处理的数据。
- `CFileiSzTskN` 是一个任务示例。该类负责获取指定目录及其所有子目录中的文件名和大小。它使用 `CCnctnVMN` 将数据发送到客户端,并且不知道发送过程。它以 HTML 文件中表格的形式发送数据。
- `CInstlUninstl` 负责服务的安装和卸载。
- `CIntrprttr` 分析客户端发送的 `string` 并提取命令 ID 和命令 `string`。
- `CLstnrTpsprtN` 本质上是一个结构,用于通过其参数将数据传递给监听器线程函数。
- `CThrdPullVMN` 是一个保存所有工作线程句柄的类,用于关闭已退出线程的句柄,并在设置停止事件后监视线程停止。
- `CTrdClssCr`:这个抽象类的指针在工作线程 `WrkTrhdVMN` 中用于为客户端执行特定作业。`WrkTrhdVMN` 不知道它执行的是什么类型的任务。此指针由任务工厂(`CTskFbrckN`)提供。
- `CTimeTskN` 是另一个任务示例。它比 `CFileiSzTskN` 更简单。`CTimeTskN` 获取服务器时间并将其返回给客户端。它使用 `CCnctnVMN` 将数据发送到客户端,并且不知道发送过程。
- `CTskFbrckN` 是任务工厂。它将抽象类 `CTrdClssC` 的指针提供给工作线程(*LStnrThrdN.cpp* 中的 `WrkTrhdVMN`),工作线程不知道它为客户端执行什么类型的任务。
- `CVMprotocol` 包含辅助函数,用于检查从客户端接收到的命令的起始和结束标签是否存在。
- `CVMprotocolCr` 对于客户端和服务器是通用的。它包含客户端命令和服务器数据的起始/结束 XML 风格标签集,以及使用它们的辅助函数。
- `CWrkThrdPsprtN` 本质上是一个结构,用于将数据传递给工作线程 `WrkTrhdVMN`。
客户端
客户端应用程序旨在与安装在远程服务器上的 `vmSrvr` 服务配合使用。它通过 TCP/IP 协议通过网络与服务器通信。在使用此客户端之前,您应该知道 `vmSrvr` 正在监听的端口。默认端口是 `5105`。
要在同一台计算机上模拟客户端服务器,您可以使用 localhost 作为计算机名或 127.0.0.1 作为 IP 地址。
任何新任务都会中断前一个任务。
连接后,客户端将保持连接状态,直到您明确要求断开连接。根据规范,在连接到新的服务器名称或 IP 地址之前,您还必须明确断开连接(您可以轻松更改此行为)。
研究客户端最方便的起点是文件 *CvmClntHlpr.cpp*(函数 `CvmClntHlpr::DoCmmnd`)和 *CvmClntHlpr.h*。
所有函数和类都附带注释,所以我认为理解代码应该没有问题。
客户端类和主函数列表
一些小型或易于理解的类已从列表中省略。
- `CvmClntHlpr` 是启动客户端的主类。最重要的函数是 `DoCmmnd`。如果尚未连接,它将连接并执行作业。
- `CVMThrd` 启动线程 `VmThrdFnctn`,该线程负责与服务器进行数据交换。`VmThrdFnctn` 位于 *VMThrd.cpp* 文件中。
- `CCnctnVM` 负责向服务器发送命令和接收数据。它不知道它处理的数据。
- `CDoItVM` 是工作线程用来发送命令和接收数据的主类。
- `CDscnnctVM` 是断开连接任务,它与文件和时间示例任务具有相同的父类。它向服务器发送断开连接命令。它是最简单的任务示例。
- `CFileSzTsk` 是一个任务示例。它收集服务器发送给客户端的关于目录、文件及其大小的信息。它还显示收集到的数据。
- `CIntrprttr` 分析服务器发送给客户端的数据。它的主要工作是找到数据的起始和结束 XML 标签。
- `CTimeTask`:另一个任务示例,比 `CFileSzTsk` 更简单。它获取服务器时间并显示它。它使用 `CCnctnVM` 从服务器接收数据。它不知道发送过程和网络。
- `STskOnly`:一个结构,包含线程的“只读”信息,如端口和 IP 地址。
- `CTskFbrck` 是任务工厂。它将抽象类 `CTrdClssCr` 的指针提供给工作线程,工作线程不知道它执行的是什么类型的任务。
- `CVMprotocolCr` 对于客户端和服务器是通用的。它包含客户端命令和服务器数据的起始/结束标签集,以及使用它们的辅助函数。
项目
该项目是在 Microsoft Visual Studio 2005 下编译的。
要编译,您应该为客户端和服务器都将 *common* 目录设置为附加包含目录(C/C++ / 常规)。
您还应该为这两个项目都设置为“不使用预编译头文件”(C/C++ / 创建/使用预编译头文件)。
研究客户端最方便的起点是文件 *CvmClntHlpr.cpp*(函数 `CvmClntHlpr::DoCmmnd`)和 *CvmClntHlpr.h*。
研究服务最方便的起点是文件 *YVRRN.CPP*。
可测试的预编译可执行文件位于项目内的 *release* 目录中。
关注点
- 点击此处阅读关于一个文档记录不佳的技巧。