小型 C 语言 TCP 服务器框架
用于编写小型到中型跨平台 TCP 服务器的框架
引言
本文介绍了一个用 C 语言编写的、采用面向对象方法、使用标准 Berkeley 套接字接口,并旨在跨平台的小到中型 TCP 服务器编写框架。开发在安装了 Ubuntu Linux 发行版的系统上进行,之后在 Windows 2003 上使用 Visual Studio Express 2008 进行了移植和少量测试。
背景
该框架整合了我多年在银行和零售自动化领域工作中遇到的许多 TCP 服务器的经验。这些服务器通常处理几十到几百个连接,运行在私有本地网络上。
描述
框架使用非阻塞套接字执行所有通信功能,并且所有连接都由一个线程管理,从而提供了高度的可扩展性。然而,这种可扩展性会受到标准 Berkeley 套接字函数内置的固有局限性的制约。特别是 `select` 套接字函数的限制,它能够处理几百个连接,但通常无法处理几千个连接。
使用此框架编写服务器时,业务逻辑由应用程序以可动态配置的线程数形式提供。这些线程与实际连接是解耦的,因此应用程序无需担心连接问题。配置应用程序线程的数量和类型相对容易。
缓冲区管理也得到了简化,从而最大限度地减少了与内存管理相关的错误。可以配置静态缓冲区、与连接关联的缓冲区以及特殊的临时缓冲区,如果其他方案失败则分配。此外,复制操作也被最小化:唯一需要的复制是从操作系统缓冲区到框架缓冲区的复制。
在线路上交换的消息使用计数器和标志,以提供记录边界和额外的可靠性。
还提供了一些方便的函数,以帮助编写客户端应用程序,尽管框架的主要目标是编写服务器。
假设使用该框架编写的服务器通常是针对特定业务需求而设计的,并用于运行在私有网络上的内部应用程序。这些服务器并非设计用于互联网上的通用服务器,例如 Web 服务器等。
下面是一个非正式的 UML 类结构图,描绘了构成框架的整体类。

框架的核心是 `ConnectionManager` 对象。它拥有一个 `ConnectionTable` 实例,该实例拥有许多 `Connection` 对象。每个连接又拥有一个 `Socket` 实例,该实例执行实际的通信工作。每个连接还拥有一个或两个 `Message` 实例,一个用于从连接的客户端接收消息,另一个用于向其发送消息。
连接管理器顾名思义,负责管理 TCP 连接。对于每个连接,它会跟踪正在发送和接收的消息:已传输多少字节,还有多少字节未完成。它还管理新连接请求,并处理所有套接字操作产生的错误,并在必要时关闭出错的连接。
框架的一般操作流程如下图所示的时序图所示。

每个应用程序 `Thread` 都处于一个循环中,等待输入消息变为可用以供处理。等待是通过调用 `QueueManager::waitInputMessage` 方法完成的。当连接管理器完成组装输入消息时,它通过调用 `QueueManager::addInputMessage` 方法向队列管理器发出信号。此方法将完整的消息添加到 `QueueManager` 的输入消息队列中,并唤醒一个应用程序线程来处理输入消息。请注意,消息不一定立即处理:这取决于是否有空闲的应用程序线程。
应用程序线程在处理完输入消息后,可能会生成一个要发送给客户端应用程序的输出消息。应用程序线程通过调用 `QueueManager::dispatchOutputMessage` 方法使输出消息可供框架使用。收到此调用后,队列管理器会将输出消息添加到其输出消息队列中,并通过调用 `ConnectionManager::notifyOutputMessage` 方法通知连接管理器新输出消息的存在。然后,连接管理器从输出消息队列中移除输出消息,并安排与该消息关联的连接进行传输。
请注意,应用程序线程可以重用输入消息作为输出消息。或者,它可以调用 `QueueManager::disposeMessage` 方法(将消息返回到空闲消息队列)来处理输入消息,然后通过调用 `QueueManager::getFreeMessage` 方法请求一个空闲消息作为输出消息。应用程序线程如何管理其消息取决于它自己,只要它对它们执行了某些操作:在处理完输入消息后再次阻塞之前,调用 `dispatchOutputMessage` 或 `disposeMessage`。另请注意,`Message` 对象始终属于另一个对象:可以是 `MessageQueue`、`Thread` 或 `Connection`。
类图中有另外三个类值得解释。
`Server` 类是一个外观类,为编写服务器应用程序提供了对最重要方法的便捷访问。服务器类中的所有方法实际上都是属于其他类的方法的 `#define`。
`Client` 类也像服务器类一样是一个外观类,用于编写客户端应用程序。虽然该框架旨在轻松编写服务器应用程序,但其所有基础设施都可以轻松利用,以编写客户端应用程序。客户端类拥有一系列 `#define` 以辅助其作为外观类的作用,但它也通过用于编写客户端应用程序的特定方法增加了一些自己的功能。
最后,`Log` 类将大量关于框架在应用程序运行时所做操作的信息写入日志文件。此日志功能具有通常的严重性级别:信息、警告、错误、致命、调试。它还可以用于跟踪通信缓冲区,并且可以轻松地从应用程序内部调用。
有关框架的更完整文档,可以轻松更改 `doxygen` 配置文件以获取更多信息。(Doxygen 是用于生成此项目文档的实用程序。)特别是,可以激活两个有用的选项:“`CALL_GRAPH`”和“`CALLER_GRAPH`”。这些选项生成的输出非常有用,但对于大型项目,由于图像尺寸过大,可能会弄乱浏览器中显示的输出布局。
第一个示例:你好?
好的,现在来看一些更有趣的东西。让我们看看如何创建一个“`hello world`”类型的应用程序。
首先,主程序
int main(void)
{
// does whatever initialization the framework needs
server_init();
// starts 3 threads of the same code
server_addThreads(3, threadFunc, "example thread");
// runs the server
server_run();
// that's it!
return 0;
}
现在,应用程序线程。请记住,框架将启动 3 个线程实例。
// a minimal thread
static threadfunc threadFunc(void* arg)
{
for (;;)
{
// uses "server_printf" (and not plain printf) because of
// contention issues between threads
server_printf("Hello from a minimal thread...\n");
server_sleep(3);
}
return 0;
}
上面每个线程都会在控制台上打印一条消息,休眠 3 秒钟,然后一遍又一遍地重复。
现在是这个小程序使用的头文件。
// common configuration options & declarations (always include first)
#include "config.h"
// application includes
#include "Server.h" /* server_xxx functions */
// prototypes
static threadfunc threadFunc(void*);
是的,就是这样,整个程序。
值得关注的是 `server_xxx` 函数和 `threadfunc typedef`,它们由框架提供。
另一个示例:回显
现在,TCP/IP 书籍中的第一个服务器程序:回显服务器。
首先,主程序
int main(void)
{
// the port on which the server waits for connection requests
server_setServicePort(12345);
// how much detail is written to the log file
server_setLogLevel(LOG_LEVEL_DEBUG);
// does whatever initialization it's needed
server_init();
// starts 5 threads of the same code
server_addThreads(5, threadFunc, "example thread");
// runs the server
server_run();
// that's it!
return 0;
}
现在再次是应用程序线程。
static threadfunc threadFunc(void* arg)
{
Message *msg;
for (;;) // the thread main loop
{
// waits for a message from a client
msg = server_waitInputMessage();
server_logDebug("received a message");
// echoes back the received message
server_logInfo("ok, replying");
server_dispatchOutputMessage(msg);
}
// yep, that's it!
return 0;
}
现在值得关注的是 `Message typedef`,它由框架提供,并包含客户端发送的数据。
另一个示例:仍然回显,但什么也没做
现在只显示应用程序线程。它仍然没有做什么,但至少它展示了如何访问和修改客户端发送的数据。(实际上,这里显示的方式有点不安全,但很简单。还有一些其他方法可以更安全地访问消息内容,尽管正如 C 语言的 C 一样,你可以做任何你想做的事情,包括自残。当然,你可以很快做到。)
threadfunc threadFunc1(void* arg)
{
uint size, count = 0;
char *bufIn, *bufOut;
Message *msgIn, *msgOut;
for (;;)
{
// waits for a message from a client
msgIn = server_waitInputMessage();
// ok, message received
bufIn = server_messageBuffer(msgIn);
size = server_messageSize(msgIn);
// requests an (output) message to be sent as reply to the client
msgOut = server_getFreeMessage();
bufOut = server_messageBuffer(msgOut);
// processes the (input) message received from the client
server_printf("* message: length=%02d buf=[%.20s]\n", size, bufIn);
// blah blah blah
// creates the (output) message to be sent as reply to the client
// (uses the same bytes and size, just so the client can check them)
memcpy(bufOut, bufIn, size);
server_setMessageSize(msgOut, size);
if (!(++count % 10))
server_logInfo("%d messages processed now", count);
// copies connection information from the input message to the output
// message (this is needed so that the framework knows which client to
// send the output message to)
server_copyConnectionFromMessage(msgOut, msgIn);
// releases the input message, it's not needed anymore
// (this wouldn't be needed if the input message were reused)
server_disposeMessage(msgIn);
// makes the message available to be sent to the client
server_dispatchOutputMessage(msgOut);
}
// that's it
return 0;
}
这里有几个值得关注的点。
- `server_messageBuffer` 和 `server_messageSize` 方法,它们提供了对客户端发送的原始数据及其大小的访问。
- `server_getFreeMessage` 方法,应用程序线程通过它获取一个新的 `Message` 实例,该实例将用作对客户端的回复。
- `server_setMessageSize`,它设置回复消息的大小。
- `server_copyConnectionFromMessage`,它设置将发送回复的客户端的 IP 地址。
- `server_disposeMessage`,应用程序线程使用它将未使用的消息返回给框架。
现在怎么办?
代码本身可以进行多项改进。
代码中散布着大量的 `assert` 调用。应审查这些调用,并为有意义的情况提供适当的错误处理。
可以通过移除现有的全局静态变量,使核心类 `ConnectionManager` 更具类属性。这样做将使我们能够编写拥有多个连接管理器的应用程序,即服务于两个或多个 IP 地址或服务端口。
编写一些单元测试也不会有坏处……只有一个类有测试,因为它们绝对是必要的。
值得注意的是,代码大量使用了 `uint` 和 `ushort`,这可能会让喜欢原始 `int` 和 `short` 的人感到恼火。
历史
- 2010 年 4 月 7 日:文章的第一个版本,涵盖了框架的 1.0.0 版本。
- 2010 年 4 月 14 日:版本 1.01 - 更新了源代码。
- 修正:将“`assert`”调用内的代码移出。
- 包含发行版构建。
- 包含文件与 C++ 语言的兼容性。
- 包含 C++ 示例。
- 2010 年 4 月 29 日:版本 1.02 - 更新了源代码。
- 修正:在创建新连接之前检查连接数。
- 包含“通用客户端”类和示例。
- 在 Windows 中将“`CreateThread`”更改为“`_beginthreadex`”。
- 重新组织了 `ConnectionManager.c` 中的代码。