【问题标题】:Serialization/Deserialization of a struct to a char* in C将结构序列化/反序列化为 C 中的 char*
【发布时间】:2010-12-11 20:09:41
【问题描述】:

我有一个结构

struct Packet {
    int senderId;
    int sequenceNumber;
    char data[MaxDataSize];

    char* Serialize() {
        char *message = new char[MaxMailSize];
        message[0] = senderId;
        message[1] = sequenceNumber;
        for (unsigned i=0;i<MaxDataSize;i++)
            message[i+2] = data[i];
        return message;
    }

    void Deserialize(char *message) {
        senderId = message[0];
        sequenceNumber = message[1];
        for (unsigned i=0;i<MaxDataSize;i++)
            data[i] = message[i+2];
    }

};

我需要将其转换为 char* ,最大长度 MaxMailSize > MaxDataSize 以便通过网络发送,然后在另一端反序列化它

我无法使用 tpl 或任何其他库。

有什么方法可以让这变得更好吗?我对此不太满意,或者这是我们能做的最好的。

【问题讨论】:

    标签: c++ c serialization


    【解决方案1】:

    您可以使用协议缓冲区来定义和序列化结构和类。这是google内部使用的,并且有一个非常小的传输机制。

    http://code.google.com/apis/protocolbuffers/

    【讨论】:

      【解决方案2】:
      int senderId;
      int sequenceNumber;
      ...    
      char *message = new char[MaxMailSize];
      message[0] = senderId;
      message[1] = sequenceNumber;
      

      您在这里覆盖了值。 senderId 和 sequenceNumber 都是整数,在大多数架构上将占用超过 sizeof(char) 个字节。尝试更多类似的方法:

      char * message = new char[MaxMailSize];
      int offset = 0;
      memcpy(message + offset, &senderId, sizeof(senderId));
      offset += sizeof(senderId);
      memcpy(message + offset, &sequenceNumber, sizeof(sequenceNumber));
      offset += sizeof(sequenceNumber);
      memcpy(message + offset, data, MaxDataSize);
      

      编辑: 固定的代码写在一个昏迷中。此外,正如评论中所述,由于字节序差异,任何此类数据包都不可移植。

      【讨论】:

      • memcpy 的第二个参数采用 const void*,但您将它传递给 int。应该是:memcpy(message + offset, &amp;senderId, sizeof(senderId))。此外,您不需要最后一个 memcpy,因为 data 不需要序列化,因为它只是用于反序列化的缓冲区。
      • 在将int 分配给char 时,他没有覆盖任何内容。不过,他(可能)正在削减他们的价值观。
      • -1 将memcpy 用于senderIdsequenceNumber 是不可移植的,因为您不能保证这些值的字节顺序。最好通过移位和掩蔽来做到这一点。
      • 我显然不应该在深夜回答问题。 @Charles,我知道他在截断值,但我在考虑他的意图并且一直在使用 memcpy 的目标偏移量,他会覆盖 senderId 的最后 3 个字节
      【解决方案3】:

      取决于你是否有足够的地方......你可以简单地使用流:)

      std::string Serialize() {
        std::ostringstream out;
        char version = '1';
        out << version << senderId << '|' << sequenceNumber << '|' << data;
        return out.str();
      }
      
      void Deserialize(const std::string& iString)
      {
        std::istringstream in(iString);
        char version = 0, check1 = 0, check2 = 0;
        in >> version;
        switch(version)
        {
        case '1':
          senderId >> check1 >> sequenceNumber >> check2 >> data;
          break;
        default:
          // Handle
        }
        // You can check here than 'check1' and 'check2' both equal to '|'
      }
      

      我欣然承认它需要更多的地方......或者它可能。

      实际上,在 32 位架构上,int 通常包含 4 个字节(4 个字符)。如果值高于 9999,则使用流对它们进行序列化只需要超过 4 个 'char',这通常会留出一些空间。

      还请注意,您可能应该在您的信息流中加入一些守卫,只是为了检查您何时将其取回是否正常。

      版本控制可能是个好主意,它不会花费太多,并且允许以后进行计划外的开发。

      【讨论】:

        【解决方案4】:

        正如其他帖子中提到的,senderIdsequenceNumber 都是 int 类型,它可能大于 char,因此这些值将被截断。

        如果这是可以接受的,那么代码就可以了。如果不是,那么您需要将它们拆分为它们的组成字节。鉴于您使用的协议将指定多字节字段的字节顺序,因此最便携、最不模糊的方法是通过移位。

        例如,假设senderIdsequenceNumber都是2字节长,协议要求高字节在前:

        char* Serialize() {
            char *message = new char[MaxMailSize];
        
            message[0] = senderId >> 8;
            message[1] = senderId;
        
            message[2] = sequenceNumber >> 8;
            message[3] = sequenceNumber;
        
            memcpy(&message[4], data, MaxDataSize);
        
            return message;
        }
        

        我还建议将 for 循环替换为 memcpy(如果可用),因为它的效率不太可能降低,而且它会使代码更短。

        最后,这一切都假设char 是一个字节长。如果不是,则需要屏蔽所有数据,例如:

            message[0] = (senderId >> 8) & 0xFF;
        

        【讨论】:

          【解决方案5】:

          您可以有一个类来表示您在软件中使用的对象,包括所有细节和成员函数以及您需要的任何东西。然后你就有了一个“序列化”的结构,它更多地描述了最终会出现在网络上的内容。

          为确保编译器会执行您告诉他执行的任何操作,您需要指示它“打包”结构。我在这里使用的指令是针对 gcc 的,如果你不使用 gcc,请查看你的编译器文档。

          然后序列化和反序列化例程只是在两者之间转换,确保字节顺序和类似的细节。

          #include <arpa/inet.h>   /* ntohl htonl */
          #include <string.h>      /* memcpy */
          
          class Packet {
              int senderId;
              int sequenceNumber;
              char data[MaxDataSize];
          public:
              char* Serialize();
              void Deserialize(char *message);
          };
          
          struct SerializedPacket {
              int senderId;
              int sequenceNumber;
              char data[MaxDataSize];
          } __attribute__((packed));
          
          void* Packet::Serialize() {
              struct SerializedPacket *s = new SerializedPacket();
              s->senderId = htonl(this->senderId);
              s->sequenceNumber = htonl(this->sequenceNumber);
              memcpy(s->data, this->data, MaxDataSize);
              return s;
          }
          
          void Packet::Deserialize(void *message) {
              struct SerializedPacket *s = (struct SerializedPacket*)message;
              this->senderId = ntohl(s->senderId);
              this->sequenceNumber = ntohl(s->sequenceNumber);
              memcpy(this->data, s->data, MaxDataSize);
          }
          

          【讨论】:

          • 使用简单的delete ptr 释放序列化返回的 void * 是否安全?自从我在 C++ 中工作以来已经有一段时间了,但是由于您正在分配堆内存,因此您需要在某个地方释放它。
          • 嗯,不,可能不会(至少不能保证)。使用 malloc() 和 free() 可能会更好。
          • 或者更好的是,忘记在堆上分配内存;只需使用相同的内部堆栈缓冲区进行序列化和反序列化
          • @246tNt 当签名在实现中是 void*,在减速中是 char*,但是你要返回的变量是 SerializedPacket 时,这如何工作?
          • @246tNt,我想用它来通过网络传输数据。字节序差异会有什么问题吗?
          【解决方案6】:

          一般来说,C++ 没有反射机制,因此手动序列化和反序列化按类定义的函数是您能做的最好的事情。话虽如此,您编写的序列化函数会破坏您的数据。这是一个正确的实现:

          char * message = new char[MaxMailSize];
          int net_senderId = htonl(senderId);
          int net_sequenceNumber = htonl(sequenceNumber);
          memcpy(message, &net_senderId, sizeof(net_senderId));
          memcpy(message + sizeof(net_senderId), &net_sequenceNumber, sizeof(net_sequenceNumber));
          

          【讨论】:

            【解决方案7】:

            由于这是通过网络发送的,我强烈建议您在发送之前将这些数据转换为网络字节顺序,并在接收时转换回主机字节顺序。这是因为字节顺序在各处都不相同,并且一旦您的字节顺序不正确,反转它们可能会变得非常困难(取决于接收端使用的编程语言)。字节排序函数与套接字一起定义,命名为htons()htonl()ntohs()ntohl()。 (在这些名称中:h 表示“主机”或您的计算机,n 表示“网络”,s 表示“短”或 16 位值,l 表示“长”或 32 位值)。

            那么你只能靠你自己的序列化,C 和 C++ 没有自动的方法来执行它。一些软件可以为您生成代码,例如 ASN.1 实现 asn1c,但它们很难使用,因为它们涉及的不仅仅是通过网络复制数据。

            【讨论】:

            • 感谢您的帮助!我会试试这个。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2010-10-18
            • 2017-08-09
            • 1970-01-01
            • 2010-11-16
            • 2017-02-21
            • 1970-01-01
            • 2021-10-15
            相关资源
            最近更新 更多