首先,我以前从未使用过 C(主要是 Java,这就是你会发现我编写一些简单的 C 代码的原因)。我正在用 C 编写一个简单的命令解释器。我有这样的东西:
//Initialization code
if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) {
perror("Select dead");
exit(EXIT_FAILURE);
}
....
....
//Loop through connections to see who has the data ready
//If the data is ready
if ((nbytes = recv(i, buf, sizeof(buf), 0)) > 0) {
//Do something with the message in the buffer
}
现在,如果我正在查看一长段命令之类的内容,很明显 256 字节缓冲区将无法获取整个命令。目前,我使用 2056 字节缓冲区来获取整个命令。但如果我想使用 256 字节缓冲区,我该怎么做呢?我是否跟踪哪个客户端给了我什么数据并将其附加到某个缓冲区?我的意思是,使用二维数组之类的东西?
是的,通常的方法是为每个客户端设置一个“我已收到但未处理的数据”的缓冲区,该缓冲区足够大以容纳最大的协议消息。
您读入该缓冲区(始终跟踪缓冲区中当前有多少数据),每次读取后,检查是否有完整的消息(或多个消息),因为您可能会同时收到两个消息! )。如果这样做,您将处理该消息,将其从缓冲区中删除,并将所有剩余数据移至缓冲区的开头。
大致如下:
for (i = 0; i < nclients; i++)
{
if (!FD_ISSET(client[i].fd, &read_fds))
continue;
nbytes = recv(client[i].fd, client[i].buf + client[i].bytes, sizeof(client[i].buf) - client[i].bytes, 0);
if (nbytes > 0)
{
client[i].bytes += nbytes;
while (check_for_message(client[i]))
{
size_t message_len;
message_len = process_message(client[i]);
client[i].bytes -= message_len;
memmove(client[i].buf, client[i].buf + message_len, client[i].bytes);
}
}
else
/* Handle client close or error */
}
顺便说一下,如果
errno == EINTR
返回 -1,你应该检查 select()
,然后再次循环 - 这不是致命错误。
我会为每个客户保留一个结构。每个结构都包含一个指向读入命令的缓冲区的指针。也许您可以在不使用缓冲区时释放它们,也可以保留它们。该结构还可以包含客户端的 fd。然后你只需要一个循环的客户端数组(或列表)。
除了 256 字节可能不够之外,您想要这样做的另一个原因是,recv 并不总是填满缓冲区。一些数据可能仍在通过网络传输。
但是,如果您为每个客户端保留缓冲区,则可能会遇到“slowloris”攻击,其中单个客户端不断发送少量数据并占用您的所有内存。
当您通过网络获取大量此类数据时,这可能会带来严重的痛苦。分配巨大的数组或多次读取与数据移动之间存在着不断的权衡。您应该考虑获取一个现成的缓冲区链表,然后在读取链表每个节点中的缓冲区时遍历该链表。这样它就可以优雅地扩展,并且您可以快速删除已处理的内容。我认为这是最好的方法,也是 boost asio 实现缓冲读取的方式。
如果您正在处理多个客户端,则可以使用一种常见的方法来为每个连接分叉/执行。您的服务器将侦听传入连接,当建立连接时,它将分叉并执行其自身的子版本,然后处理问题的“命令解释器”部分。
通过这种方式,您可以让操作系统管理客户端进程——也就是说,您的程序中不必有数据结构来管理它们。当子进程终止时,您仍然需要清理服务器中的子进程。
至于管理缓冲区...在发布回复之前您期望有多少数据?您可能需要准备好动态调整缓冲区的大小。