【问题标题】:Data through Sockets in C++数据通过 C++ 中的套接字
【发布时间】:2012-12-13 06:23:48
【问题描述】:

我目前正在做一个使用网络的项目。我必须发送一个结构

    struct Header
    {
     uint32_t   magic;
     uint32_t   checksum;
     uint32_t   timestamp;
     uint16_t   commandId;
     uint16_t   dataSize;
    };

    struct Packet
    {
     struct Header  header;
     char       data[128];
    };

我正在尝试使用 TCP 将结构数据包从一个套接字发送到另一个套接字。我试图像这样发送我的结构

      send(socket, &my_struct, sizeof(my_struct), 0);

但它不起作用,所以我尝试将我的结构序列化为 char*

unsigned char               *Serialization::serialize_uint32(unsigned char *buffer, uint32_t arg)
{
 buffer[3] = (arg >> 24);
 buffer[2] = (arg >> 16);
 buffer[1] = (arg >> 8);
 buffer[0] = (arg);
 return (buffer + sizeof(uint32_t));
}

unsigned char               *Serialization::serialize_uint16(unsigned char *buffer, uint16_t arg)
{
 buffer[1] = (arg >> 8);
 buffer[0] = (arg);
 return (buffer + sizeof(uint16_t));
}

    unsigned char                           *Serialization::deserialize_uint32(unsigned char *buffer, uint32_t *arg)
    {
      memcpy((char*)arg, buffer, sizeof(uint32_t));
      return (buffer + sizeof(uint32_t));
    }

    unsigned char                           *Serialization::deserialize_uint16(unsigned char *buffer, uint16_t *arg)
    {
     memcpy((char*)arg, buffer, sizeof(uint16_t));
     return (buffer + sizeof(uint16_t));
    }

即使客户端简单地发送一个 struct 标头数据在我读取服务器端时也已损坏 为什么数据损坏?

客户端发送循环

    TcpSocket                     tcp;
    Packet                        p;
    std::stringstream             ss;
    int                           cpt = 0;
    int                           ret = 0;
    char                          *serialized;

    tcp.connectSocket("127.0.0.1", 4242);
    while (getchar())
    {
      ss.str("");
      ss.clear();
  ss << cpt++;
  p.header.magic = 0;
  p.header.checksum = 1;
  p.header.timestamp = 2;
  p.header.commandId = 3;
  p.header.dataSize = ss.str().length();
  memset(p.data, 0, 128);
  memcpy(p.data, ss.str().c_str(), ss.str().length());
  serialized = new char[sizeof(Header) + ss.str().length()];
  bzero(serialized, sizeof(Header) + ss.str().length());
  Serialization::serialize_packet(serialized, p);
  hexDump("serialized", serialized+1, sizeof(Header) + ss.str().length());
  ret = tcp.write(serialized+1, sizeof(Header) + ss.str().length());
}

服务器接收循环:(由 select() 调用的函数)

  buff = new char[bav];
  socket->read(buff, bav);
  hexdump("buff", buff, bav);

socket->read() :

    int                     TcpSocket::read(char *buff, int len)
    {
      int                   ret;

      ret = recv(this->_socket, buff, len, 0);
      return (ret);
    }

当我运行这些程序时:

    ./server
    [Server] new connexion :: [5]
    recv returns : 17
    buff serialized:
      0000  00 00 00 00 14 00 00 00 1c 00 00 00 1a 00 00 00  ................
      0010  1b

    ./client
    serialized data:
      0000  00 00 00 00 00 00 01 00 00 00 02 00 03 00 01 30  ...............0
      0010  00
    send returns : 17

【问题讨论】:

  • 我们是否假设客户端正在以适当的反向算法解包(请注意,您应该使用 unsigned char 作为打包缓冲区)。此外,您在第一种情况下的结构包装以及机器端格式 将发挥作用。
  • 对于 uint16 和 uint32,您可以只使用 ntohl、ntohs、htonl 和 htons。简短的回答是:您必须在字节级别精确定义数据格式,并在发送和接收时正确转换为“有线格式”。
  • @DavidSchwartz 我同意,但是当我几天前提出这个作为解决类似问题的解决方案时,我立刻被一边跳一边跳,因为它不在“标准”。仍然被那个刺痛。(顺便说一句,我仍然会使用 POSIX 函数来做这件事,就像你可能会做的那样)。
  • 有两个明显的问题。首先, send(socket, &my_struct, sizeof(my_struct)) 将不起作用,因为 send() 需要 四个 参数。其次,序列化应该使用无符号字符(即“字节”)而不是字符。正如已经指出的那样,消除字节序问题的“网络”功能甚至更好。
  • @WhozCraig:由于这个问题被标记为CC++ 并使用uint32_t,我无法想象“标准”会是什么。 :)

标签: c++ c sockets serialization tcp


【解决方案1】:

所以,这是错误的,肯定会出错。

buff = new char[bav];
socket->read(buff, bav);
hexdump("buff", buff, bav);
socket->read() :

int TcpSocket::read(char *buff, int len)
{
    return recv(this->_socket, buff, len, 0);
}

recv() 的返回值不能被忽略。

来自man 2 recv

返回值 这些调用返回接收到的字节数,如果出错则返回 -1 发生了。 对于 TCP 套接字,返回值 0 表示对端已关闭其一半 连接的一侧。

那么,你收到了多少字节?如果您丢弃来自recv() 的结果,则无法判断。可能recv() 失败了,不检查返回值你永远不会发现。也许它只填满了你的缓冲区的一部分。 您必须检查来自recv() 的返回码。这是人们在编写使用 TCP 的程序时犯的第一个错误。

您将需要更改您的代码以处理以下情况:

  1. recv() 调用可能会完全填满您的缓冲区。

  2. recv() 调用可能会部分填满您的缓冲区。

  3. recv() 调用可能返回 0,表示发送方已关闭连接。

  4. recv() 调用可能指示 EINTR,因为它被系统调用中断。

  5. recv() 调用可能指示ECONNRESET,因为发送方突然关闭了连接或已经消失。

  6. recv() 调用可能会遇到一些其他错误。

记住:当使用 TCP 时,仅仅因为你 send() 16 字节并不意味着其他对等方将 recv() 16 字节——它可能会被分解成块。 TCP 是一种流协议。与 UDP 不同,相邻的数据块可以任意连接或拆分。

【讨论】:

  • @WhozCraig:我认为没有粗体也可以。
  • 我的客户端发送返回是 17,我的服务器接收返回也是 17 :(
  • @CamilleTolsa:我想你误会了。告诉我返回值是什么是没有意义的,因为返回值可能每次都不同。您必须修改您的程序,使其对recv() 可以给出的每个返回值都正确运行。
  • 我明白,但是,我使用循环缓冲区来处理案例 [1] 和 [2],案例 [3] 是句柄,案例 [4]、[5] 或 [6] 从未发生在我的测试。
  • @CamilleTolsa:尝试使用 Valgrind 运行客户端和服务器。由于您尚未发布至少处理案例 1 和 2 的固定代码,因此我无法知道它是否正确。
【解决方案2】:
  1. 每次只需要屏蔽低8位:

    buffer[3] = (arg >> 24) & 0xff;
    buffer[2] = (arg >> 16) & 0xff;
    buffer[1] = (arg >> 8) & 0xff;
    buffer[0] = (arg) & 0xff;
    
  2. 反序列化时也这样做

【讨论】:

  • 你能解释一下为什么吗
  • 因为buffer可能会被签名。
  • 这个答案是错误的。 这里的掩码在任何 8 位字节的平台上都不起作用。由于argbuffer 都是无符号的,所有算术都是按照C 标准模块化的。所以如果xunsigned char 类型,如果unsigned char 有8 位,那么x = argx = arg &amp; 0xff 产生完全相同的结果。
  • @DietrichEpp - 给出答案时,问题指定 Buffer 已签名,问题在回答后 2 小时被编辑。
  • @BinyaminSharet:当然可以,但即使它已签名,也不会在二进制补码平台(例如所有平台)上产生影响。
【解决方案3】:

如果我是你,我不会重新发明轮子。有很多文档齐全且经过测试的库/协议正是您正在寻找的目的。我刚想到的一个小清单:

【讨论】:

  • 我知道,但我正在做一个学校项目,我们不允许使用任何库,我们不能使用文本协议:(
  • 请定义“库”? C-Library 是一个库吗? (例如,你可以使用 htonX / ntohX 函数吗?)
  • 我们可以使用 libc htons/htonl & ntohX 是允许的,但我已经用过它们了,我不知道该怎么做
猜你喜欢
  • 1970-01-01
  • 2018-05-23
  • 2014-03-16
  • 2018-07-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多