【问题标题】:Partial receipt of packets from socket C++部分接收来自套接字 C++ 的数据包
【发布时间】:2012-06-01 06:43:18
【问题描述】:

我遇到了麻烦,我的服务器应用程序发送了 8 个字节长度的数据包 - AABBCC1122334455,但我的应用程序通过“recv”函数分两部分接收此数据包AABBCC1122334455,我该如何解决?

谢谢!

【问题讨论】:

  • 您需要添加更多信息(例如,一些代码sn-p)

标签: c++ sockets network-programming packet


【解决方案1】:

这就是recv() 在大多数平台上的工作方式。您必须检查收到的字节数并继续循环调用它,直到获得所需的数字。

【讨论】:

  • 怎么样?我不知道数据包服务器将发送给我的长度。据我所知,字节数 - 所有缓冲区的大小,而不是数据包长度
  • 您需要能够识别数据包的结尾。你可以知道,先验长度,你可以在数据包完成之前在字节流中发送长度,你可以有一个特殊的字节序列来表示数据包结束,或者你可以有发送数据包后服务器关闭流。
  • @Becker,你可以控制两端的消息格式吗?如果是这样,请在消息的前面加上一个消息长度并始终接收,直到您获得该长度或发生错误。
【解决方案2】:

您可以通过从 TCP 套接字循环读取来“修复”该问题,直到您获得足够的字节以对您的应用程序有意义。

【讨论】:

    【解决方案3】:

    我的服务器应用程序发送 8 字节长度的数据包

    不是真的。您的服务器发送 8 个单独的字节,而不是 8 个字节长的数据包。 TCP 数据通过字节流而不是数据包流发送。 TCP 既不尊重也不维护您可能想到的任何“数据包”边界。

    如果您知道您的数据是以 N 字节为单位提供的,则在循环中调用 recv

    std::vector<char> read_packet(int N) {
      std::vector buffer(N); 
      int total = 0, count;
      while ( total < N && (count = recv(sock_fd, &buffer[N], N-total, 0)) > 0 )
        total  += count;
      return buffer;
    }
    
        std::vector<char> packet = read_packet(8);
    

    如果您的数据包是可变长度的,请尝试在数据本身之前发送它:

    int read_int() {
      std::vector<char> buffer = read_packet(sizeof (int));
      int result;
      memcpy((void*)&result, (void*)&buffer[0], sizeof(int));
      return result;
    }
    
      int length = read_int();
      std::vector<char> data = read_buffer(length); 
    

    【讨论】:

    • 您的代码隐藏了 recv 上的错误。返回的向量的大小始终为 N,如果 recv 失败,将用 NULL 字符填充。由于连接断开,无法验证零是真实数据还是虚拟字节。
    • 只是一个挑剔但是.... TCP 数据 IS 作为数据包流传输,只是数据包边界由底层网络堆栈、连接对等方和路由器确定一路上。本质上,TCP 所做 是定义一种方法,在不可靠的网络交换成帧数据包的情况下,在应用程序之间提供可靠的、有序的字节流。此外,数据流的这种底层打包性质通常是recv() 从应用程序角度过早返回的原因。
    • @tomasz:是的。除了演示这一点之外,我不会将我的代码按原样使用。
    • @BrianMcFarland:是的。感谢您提供更完整的解释。我的观点仍然存在:TCP 不尊重或记住 OP 的数据包边界。
    • 你能帮我吗,当我收到这个分割的数据包而不创建额外的缓冲区和循环时,我如何解密数据包(我使用 ARC4 密码)? (我在迂回的recv函数中需要这个)
    【解决方案4】:

    总结一下:

    1. TCP 连接不能在应用程序级别处理数据包或消息,您正在处理字节流。从这个角度来看,它类似于从文件中写入和读取。
    2. sendrecv 可以发送和接收比参数中提供的更少的数据。您必须正确处理它(通常通过在调用周围应用适当的循环)。
    3. 在处理流时,您必须找到将其转换为应用程序中有意义的数据的方法。换句话说,你必须设计序列化协议

    根据您已经提到的内容,您很可能想要发送某种消息(嗯,这通常是人们所做的)。关键是要正确发现消息的边界。如果您的消息是固定大小的,您只需从流中获取相同数量的数据并将其转换为您的消息;否则,您需要一种不同的方法:

    • 如果你能想出一个在你的消息中不存在的字符,它可能是你的分隔符。然后,您可以阅读流,直到您到达角色,这将是您的消息。如果您传输 ASCII 字符(字符串),您可以使用零作为分隔符。

    • 如果您传输二进制数据(原始整数等),所有字符都可以出现在您的消息中,因此没有任何字符可以充当分隔符。在这种情况下,最常见的方法可能是使用包含消息大小的固定大小前缀。这个额外字段的大小取决于消息的最大大小(使用 4 个字节可能是安全的,但如果您知道最大大小是多少,则可以使用较小的值)。然后您的数据包看起来像SSSS|PPPPPPPPP...(字节流),其中S 是附加大小字段,P 是您的有效负载(应用程序中的真实消息,P 字节数由值确定S)。您知道每个数据包都以 4 个特殊字节(S 字节)开头,因此您可以将它们读取为 32 位整数。一旦知道了封装消息的大小,就可以读取所有 P 字节。处理完一个数据包后,就可以从套接字读取另一个数据包了。

    不过,好消息是,您可以想出一些完全不同的东西。您只需要知道如何从字节流中反序列化您的消息以及send/recv 的行为方式。祝你好运!

    编辑:

    将任意字节数接收到数组中的函数示例:

    bool recv_full(int sock, char *buffer, size_t size)
    {
      size_t received = 0;
      while (received < size)
      {
        ssize_t r = recv(sock, buffer + received, size - received, 0);
        if (r <= 0) break;
        received += r;
      }
    
      return received == size;
    }
    

    以及接收具有 2 字节前缀定义有效负载大小的数据包的示例(有效负载大小限制为 65kB):

    uint16_t msgSize = 0;
    char msg[0xffff];
    
    if (recv_full(sock, reinterpret_cast<char *>(&msgSize), sizeof(msgSize)) &&
        recv_full(sock, msg, msgSize))
    {
      // Got the message in msg array
    }
    else
    {
      // Something bad happened to the connection
    }
    

    【讨论】:

    • 感谢您的回答。你能举一些这个前缀大小方法实现的例子吗?如何添加我理解的前缀,但如何正确接收和读取循环中的所有数据?抱歉,我以前用过 C# :)
    • @Becker 你对最大消息大小有任何限制吗?特别是,它们总是小于 32kB 吗? (它会简化代码)
    • 你能帮我吗,当我收到这个分割的数据包而不创建额外的缓冲区和循环时,我如何解密数据包(我使用 ARC4 密码)? (我在迂回的recv函数中需要这个)
    • 也许我应该使用另一种可以正确处理分包的密码?
    • @Becker 迂回的 recv 函数是什么意思?为什么你不能有循环和缓冲区?如果您有更具体的问题,请更新您的问题。
    猜你喜欢
    • 1970-01-01
    • 2015-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-08
    • 2010-11-13
    • 1970-01-01
    • 2018-10-23
    相关资源
    最近更新 更多