使用联合或继承来解析数据

C++ Using unions or inheritance to parse data

本文关键字:数据 继承      更新时间:2023-10-16

我有一个场景,我在缓冲区中接收二进制数据(来自com端口,套接字或其他一些生产者等)。接收到的数据以不同的方式进行解释,通常在第一个字节或前几个字节中标记消息头。我正在寻找最好的类结构来处理和解析这样的数据。这似乎很容易开发,但由于某些原因,它对我来说并不明显。

我想到的一个选项是在POD类型类中使用联合。例如:

class Message {
  public:
    void DoSomething();
    int packetId;
    union {
      struct packetType1 { int A, int B, ... };
      struct packetType2 { float M, short N, ... };  // may be different size than packetType1
      ...
    };
};
void Message::DoSomething() {
  switch (packetId) {
  case 1:
    // do something using packetType1
    break;
  case 2:
    // do something using packetType2
    break;
  }
}

将指向这样一个对象的指针传递给以缓冲区作为输入的函数是可以接受的做法吗?这可以编译并且看起来可以工作。例如:

Message msg;
recvfrom(sock, (char*) &msg, sizeof(msg), ...);
msg.DoSomething();

这样做的一个缺点是Message中的成员变量是公共的。我宁愿使它们私有并提供只读访问方法。如果Message中的成员变量是私有的(或受保护的),这仍然有效吗?我想没有,但我不敢肯定。

我考虑使用继承,但问题是一个不知道哪个派生类是需要的,直到数据被接收和头解析后。这个示例可以编译并且看起来可以工作,但这似乎是一种糟糕的方法。

class Message {
  virtual void DoSomething();
  int packetId;
};
class PacketType1 : public Message {
  void DoSomething();
  int A, B;
};
class PacketType2 : public Message {
  void DoSomething();
  float M;
  short N;
};
void Message::DoSomething() {
  switch (packetId) {
  case 1:
    ((PacketType1 *) this)->DoSomething();
    break;
  case 2:
    ((PacketType2 *) this)->DoSomething();
    break;
  }
}

用法:

Message* msg = (Message*) new unsigned char [MAX_MESSAGE_SIZE];
recvfrom(sock, (char*) msg, MAX_MESSAGE_SIZE, ...);
msg->DoSomething();

对于这样的场景,最佳实践是什么?请温柔一点,我不是一个从事软件工作的人,但有时出于需要。这就是其中一次。: -)谢谢。

编辑:有几个人提到了字节顺序。我忘了在最初的帖子中提到,消息源可能与我的系统具有相同或不同的端序,但这是已知的先验。我的意图是让Message类方法之一在必要时处理这个问题。例如:

void Message::ByteSwap() {
  ByteSwap4(packetId);      // helper function that byte swaps a 4-byte word
  switch(packetId) {
  case 1:
    ByteSwap4(A);
    ByteSwap4(B);
    ...
    break;
  case 2:
    ByteSwap4(M);
    ByteSwap2(N);          // helper function that byte swaps a 2-byte word
    ...
    break;
  }
}

对于方法一,我忽略了提及我必须在定义Message类的.h文件中使用编译器指令#pragma pack(1)来强制成员按字节对齐。

关于消息的来源,我无法控制那些系统。我所拥有的是定义要发送的消息的字节结构的正式文档。

感谢所有人的输入!

你的第二个方法完全被打破了,因为你在recvfrom中得到一个字节序列,然后将其解释为非pod类型,这是未定义的并且可能崩溃,因为Message可能需要一个变量表和为它设置的东西,这是不太可能正确的,除非收到的数据是由同一机器上的完全相同的进程发送的。即使发送方在同一台机器上运行相同的代码,也可能无法工作。

第一个方法涉及各种实现定义的关于结构体和联合的填充和对齐的细节,但只要发送方是用针对相同体系结构的相同编译器构建的,就可以正常工作。如果您使用stint .h中的固定大小类型,并仔细安排内容,使对齐不太可能需要填充,那么它很可能是ok的,但是如果您尝试在不同的体系结构之间这样做,则仍然有潜在的端序问题。

要做到这一点,最好的方法是咬紧牙关,将消息定义为字节流,并编写代码显式地将对象转换为字节流,并从字节流构建新对象。一种相当有效的方法是使用带有虚拟编码和静态解码方法的继承层次结构:
class Message {
    virtual void DoSomething();
    virtual size_t Encode(char *buffer, size_t limit);
    static Message *Decode(char *buffer, size_t size);
};
class Type1 : public Message {
    virtual void DoSomething();
    virtual size_t Encode(char *buffer, size_t limit);
    static Type1 *Decode(char *buffer, size_t size);
};
Message *Message::Decode(char *buffer, size_t size) {
    if (size < 1) return 0; /* or throw some exception */
    switch(buffer[0]) {
    case 1: return Type1::Decode(buffer, size);
    case 2: return Type2::Decode(buffer, size);
    default: return 0; // or throw an exception
    }
}

编辑

如果你不关心编码,你可以省略encode方法——但是如果你为通信的两端创建代码,那么将encode和Decode保持在一起以保持一致是有意义的。

Decode方法是静态的,因为它们在被解码类型的对象存在之前被调用(甚至在您知道该类型是什么之前)。它们根据消息创建一个对象并返回该对象。继续,你可以输入:
Type1 *Type1::Decode(char *buffer, size_t size) {
    if (size != 9) return 0; // or throw an exception
    Type1 *rv = new Type1;
    rv->A = extract4byteInt(buffer+1);
    rv->B = extract4byteInt(buffer+5);
    return rv;
}
int extract4byteInt(char *buffer) {
    return ((buffer[0] & 0xff) << 24) +
           ((buffer[1] & 0xff) << 16) +
           ((buffer[2] & 0xff) << 8) +
           (buffer[3] & 0xff);
}

请注意,我们在这里显式地设置了消息所有部分的大小、填充和字节顺序,而不是依赖于编译器如何安排这些内容。

您的方法的问题是内存中类成员字段的对齐。不保证对不同的编译器或平台的对齐是相等的。此外,您还需要考虑小端序/大端序问题。

因此,为了保存,您应该自己解析数据。您可以首先读取' packketid ',然后根据该值解析剩余的数据。例如,您可以为每种消息类型定义不同的解析器类。然后,根据'packetId'使用正确的类实例来解析具体的消息。

由于您的消息大小可能不同,因此我将使用继承、不同的结构和包装函数。

struct MessageType1_t {
  int A, B;
}
class Message
{
  public:
  virtual Message(unsigned char * message)=0;
  virtual ~Message()=0;
  virtual void DoSomething()=0;
  Message * create(unsigned char * message)
  {
     // get first four bites and check message type
     // create a new instance of the derived class you need and return it
}
class PacketType1 : public Message {
public:
   void PacketType1(unsigned char * message)
   {
      myMessage = (messageType1_t *)message;
   }
   void DoSomething()
   {
      // do something
   }
private:
   MessageType1_t * myMessage;
};

代码不完整,只是想法。确保您的平台使用与消息源系统相同的端型。或者注意纠正字节顺序