处理多个recv()调用和所有可能的场景

时间:2010-12-02 15:28:20

标签: c networking tcp recv

我是C的新手并编写TCP服务器,并且想知道如何从发送服务器将响应的命令的客户端处理recv()。为了这个问题,我们只是说头是第1个字节,命令标识符是第2个字节,有效载荷长度是第3个字节,然后是有效载荷(如果有的话)。

recv()这个数据的最佳方法是什么?我想调用recv()来读入缓冲区中的前3个字节,检查以确保头和命令标识符有效,然后检查有效负载长度并再次调用recv(),并将有效负载长度作为长度并将其添加到回到前面提到的缓冲区。阅读Beej的网络文章(特别是这里的这一部分:http://beej.us/guide/bgnet/output/html/singlepage/bgnet.html#sonofdataencap),然而,他建议使用“足够大的两个[最大长度]数据包”来处理诸如获取下一个数据包之类的情况。

处理这些类型的recv()的最佳方法是什么?基本问题,但我想有效地实施它,处理可能出现的所有情况。提前谢谢。

4 个答案:

答案 0 :(得分:7)

Beej提到的方法和AlastairG提到的方法是这样的:

对于每个并发连接,您维护一个读取但尚未处理的数据的缓冲区。 (这是Beej建议调整到最大数据包长度两倍的缓冲区)。显然,缓冲区开始为空:

unsigned char recv_buffer[BUF_SIZE];
size_t recv_len = 0;

每当您的套接字可读时,请读入缓冲区中的剩余空间,然后立即尝试处理您拥有的内容:

result = recv(sock, recv_buffer + recv_len, BUF_SIZE - recv_len, 0);

if (result > 0) {
    recv_len += result;
    process_buffer(recv_buffer, &recv_len);
}

process_buffer()尝试并将数据包作为数据包处理。如果缓冲区尚未包含完整数据包,则只返回 - 否则,它会处理数据并将其从缓冲区中删除。所以对于你的示例协议,它看起来像:

void process_buffer(unsigned char *buffer, size_t *len)
{
    while (*len >= 3) {
        /* We have at least 3 bytes, so we have the payload length */

        unsigned payload_len = buffer[2];

        if (*len < 3 + payload_len) {
            /* Too short - haven't recieved whole payload yet */
            break;
        }

        /* OK - execute command */
        do_command(buffer[0], buffer[1], payload_len, &buffer[3]);

        /* Now shuffle the remaining data in the buffer back to the start */
        *len -= 3 + payload_len;
        if (*len > 0)
            memmove(buffer, buffer + 3 + payload_len, *len);
    }
}

do_command()函数将检查有效的头和命令字节。

这种技术最终是必要的,因为任何 recv()都可以返回一个很短的长度 - 使用您提出的方法,如果您的有效负载长度为500,会发生下一个{ {1}}只返回400字节?您必须保存这400个字节,直到下次套接字可读时为止。

当您处理多个并发客户端时,每个客户端只需要一个recv()recv_buffer,并将它们填充到每个客户端结构中(这可能包含其他内容 - 就像客户端的套接字一样,也许他们的源地址,当前状态等。)。

答案 1 :(得分:5)

好问题。你想要多么完美?对于所有歌唱所有舞蹈解决方案,使用异步套接字,尽可能读取所有数据,并且每当您获得新数据时,在缓冲区上调用一些数据处理功能。

这允许你做大读。如果您获得大量流水线命令,则可以在不必再等待套接字的情况下处理它们,从而提高性能和响应时间。

在写作上做类似的事情。那就是命令处理函数写入缓冲区。如果缓冲区中有数据,则在检查套接字(select或poll)时检查可写性并尽可能多地写入,记住只删除实际从缓冲区写入的字节。

循环缓冲区在这种情况下运行良好。

有更简单的解决方案。不过这个很好。请记住,服务器可能会获得多个连接,并且可以拆分数据包。如果从套接字读入缓冲区只是为了找不到完整命令的数据,那么你对已经读过的数据做了什么?你在哪里存放?如果将它存储在与该连接相关联的缓冲区中,那么您也可以完全按照上面的描述读入缓冲区。

此解决方案还避免了为每个连接生成单独的线程 - 您可以处理任意数量的连接而不会出现任何实际问题。每个连接产生一个线程是一种不必要的系统资源浪费 - 除非在某些情况下会推荐多个线程,为此你可以让工作线程执行这样的阻塞任务,同时保持套接字处理单线程。

基本上我同意你所说的Beej所说的,但是不要一次读取整齐的位。一次读大块。编写像这样的套接字服务器,基于一点点套接字经验和手册页进行学习和设计,这是我参与过的最有趣的项目之一,而且非常有教育意义。

答案 2 :(得分:2)

Alastair描述的解决方案在性能方面是最好的。仅供参考 - 异步编程也称为事件驱动编程。换句话说,您等待数据进入套接字,将其读入缓冲区,处理什么/何时可以,然后重复。您的应用程序可以在读取数据和处理数据之间执行其他操作。

我发现一些非常相似的链接:

第二个是一个很棒的库来帮助实现所有这些。

至于使用缓冲区和尽可能多地阅读,这是另一个表现的事情。批量读取更好,系统调用(读取)更少。当您决定有足够的处理时,您可以处理缓冲区中的数据,但确保一次只处理一个“数据包”(您使用3字节标题描述的数据包),而不是破坏缓冲区中的其他数据

答案 3 :(得分:1)

如果您使用多个连接,基本上有两个假设,那么处理多个连接(无论是侦听套接字,readfd还是writefd)的最佳方法是使用select / poll / epoll。您可以根据您的要求使用其中任何一种。

关于你的第二个查询如何处理多个recv()可以使用这种做法: 每当数据到达时,只需查看标题(它应该是您所描述的固定长度和格式)。

    buff_header = (char*) malloc(HEADER_LENGTH);
    count =  recv(sock_fd, buff_header, HEADER_LENGTH, MSG_PEEK);
    /*MSG_PEEK if you want to use the header later other wise you can set it to zero
      and read the buffer from queue and the logic for the code written below would
      be changed accordingly*/

通过这个你得到了标题,你可以验证参数,并提取完整的消息长度。 获得完整的msg长度后,只需收到完整的msg

    msg_length=payload_length+HEADER_LENGTH;
    buffer =(char*) malloc(msg_length);
    while(msg_length)
    {
        count = recv(sock_fd, buffer, msg_length, 0);
        buffer+=count;
        msg_length-=count;
    }

因此,您不需要采用任何具有固定长度的数组,您可以轻松实现逻辑。