5

我对 C 和编写 TCP 服务器相当陌生,并且想知道如何处理recv()来自客户端的 s,客户端将发送服务器将响应的命令。为了这个问题,假设标题是第一个字节,命令标识符是第二个字节,有效载荷长度是第三个字节,然后是有效载荷(如果有的话)。

获取这些数据的最佳方法是什么recv()?我正在考虑调用recv()将前 3 个字节读入缓冲区,检查以确保标头和命令标识符有效,然后检查有效负载长度并recv()以有效负载长度作为长度再次调用,并将其添加到上述缓冲区的后面。然而,阅读 Beej 的网络文章(特别是数据封装之子部分),他建议使用“一个足够大的数组容纳两个 [最大长度] 数据包”来处理诸如获取一些下一个数据包的情况。

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

4

4 回答 4

9

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,但下一个recv()仅返回 400 个字节会发生什么?无论如何,您必须保存这 400 个字节,直到下次套接字变得可读为止。

当您处理多个并发客户端时,您只需拥有一个recv_bufferrecv_len每个客户端,并将它们填充到每个客户端的结构中(其中可能还包含其他内容 - 例如客户端的套接字,可能是它们的源地址,当前状态等)。

于 2010-12-03T01:29:52.393 回答
5

好问题。你想去多完美?对于所有唱歌跳舞的解决方案,请使用异步套接字,尽可能读取所有数据,并在获得新数据时调用缓冲区上的某些数据处理函数。

这使您可以进行大量读取。如果您获得大量流水线命令,您可以处理它们而无需再次等待套接字,从而提高性能和响应时间。

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

循环缓冲区在这种情况下工作得很好。

有更轻更简单的解决方案。不过这一款是不错的。请记住,服务器可能会获得多个连接,并且可以拆分数据包。如果您从套接字读取到缓冲区只是发现您没有完整命令的数据,您将如何处理已经读取的数据?你把它存放在哪里?如果您将它存储在与该连接关联的缓冲区中,那么您不妨一头扎进缓冲区,并首先如上所述读入缓冲区。

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

基本上我同意你所说的 Beej 所说的,但不要一次读一些零碎的东西。一次读大块。编写这样的套接字服务器,根据一点点套接字经验和手册页学习和设计,是我从事过的最有趣的项目之一,而且很有教育意义。

于 2010-12-02T15:48:38.590 回答
2

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

我发现另外几个链接对做一些非常相似的事情很有帮助:

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

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

于 2010-12-02T19:24:34.107 回答
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_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;
    }

因此,通过这种方式,您无需采用任何具有固定长度的数组,并且可以轻松实现您的逻辑。

于 2013-08-27T18:53:11.453 回答