繁体   English   中英

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

[英]Handling multiple recv() calls and all possible scenarios

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

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

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

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_len每个客户端一个recv_bufferrecv_len ,并将它们填充到每个客户端的结构中(其中可能还包含其他内容 - 例如客户端的套接字,可能是它们的源地址、当前状态等)。

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

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

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

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

有更轻更简单的解决方案。 然而,这是一个很好的。 请记住,服务器可能会获得多个连接,并且可以拆分数据包。 如果您从套接字读取到缓冲区中却发现您没有完整命令的数据,那么您如何处理已经读取的数据? 你把它存放在哪里? 如果您将它存储在与该连接相关联的缓冲区中,那么您最好完全按照上述方式读入缓冲区。

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

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

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

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

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

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

如果您使用多个连接,则基本上有两个假设,那么处理多个连接(无论是侦听套接字、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

    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;
    }

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM