4

如果您有以下类作为网络数据包有效负载:

类有效载荷 { char field0; 整数字段1;字符字段2;整数字段3;};

在通过套接字接收数据时,使用像 Payload 这样的类是否会使数据的接收者容易受到对齐问题的影响?我认为该类要么需要重新排序,要么需要添加填充以确保对齐。

重新排序:

class Payload
{
    int  field1;
    int  field3;
    char field0;
    char field2;
};

或添加填充:

class Payload
{
    char  field0;
    char  pad[3];
    int   field1;
    char  field2;
    char  pad[3];
    int   field3; 
};

如果由于某种原因重新排序没有意义,我认为添加填充将是首选,因为它可以避免对齐问题,即使它会增加类的大小。

您对网络数据中的此类对齐问题有何经验?

4

6 回答 6

8

正确、盲目地忽略对齐可能会导致问题。即使在同一个操作系统上,如果 2 个组件是用不同的编译器或不同的编译器版本编译的。

最好...
1)通过某种序列化过程传递您的数据。
2)或者单独传递每个原语,同时仍然注意字节顺序==字节

一个好的起点是Boost Serialization

于 2009-04-15T20:00:11.187 回答
4

您应该查看Google protocol buffers或 Boost::serialize 就像另一位海报所说的那样。

如果你想自己动手,请做对。

如果您使用 stdint.h 中的类型(即:uint32_t, int8_t,等),并确保每个变量都具有“本机对齐”(这意味着它的地址可以被它的大小整除(int8_ts 在任何地方,uint16_ts 在偶数地址上,uint32_ts 在地址上可被 4 整除),您不必担心对齐或打包。

在之前的工作中,我们通过 XML 定义的数据总线(以太网或 CANbus 或 byteflight 或串行端口)发送所有结构。有一个解析器可以验证结构中变量的对齐方式(如果有人编写了错误的 XML,则会提醒您),然后为各种平台和语言生成头文件以发送和接收结构。这对我们来说非常有效,我们从来没有不必担心手写代码来进行消息解析或打包,并且可以保证所有平台都不会出现愚蠢的小编码错误。我们的一些数据链路层非常受带宽限制,因此我们实现了位域之类的东西,解析器为每个平台生成正确的代码。我们也有枚举,这非常好(你会惊讶于人类很容易在枚举上手动搞砸编码位域)。

除非您需要担心它在使用 C 的 8051 和 HC11 上运行,或者在带宽非常受限的数据链路层上运行,否则您不会想出比协议缓冲区更好的东西,您只会花费大量时间尝试与他们平起平坐。

于 2009-04-15T20:24:26.683 回答
4

我们今天使用直接覆盖在内存中的二进制数据包上的打包结构,我为我决定这样做的那一天感到后悔。我们让它工作的唯一方法是:

  1. 根据编译环境仔细定义位宽特定类型 ( typedef unsigned int uint32_t)
  2. 插入适当的编译器特定的编译指示以指定结构成员的紧密封装
  3. 要求一切都按一个字节顺序排列(使用网络或大端顺序)
  4. 仔细编写服务器和客户端代码

如果您刚刚开始,我建议您跳过尝试用结构表示线路上的内容的整个混乱。只需分别序列化每个原始元素。如果您选择不使用 Boost Serialize 之类的现有库或 TibCo 之类的中间件,那么可以通过在二进制缓冲区周围编写一个隐藏序列化方法细节的抽象来省去很多麻烦。瞄准如下界面:

class ByteBuffer {
public:
    ByteBuffer(uint8_t *bytes, size_t numBytes) {
        buffer_.assign(&bytes[0], &bytes[numBytes]);
    }
    void encode8Bits(uint8_t n);
    void encode16Bits(uint16_t n);
    //...
    void overwrite8BitsAt(unsigned offset, uint8_t n);
    void overwrite16BitsAt(unsigned offset, uint16_t n);
    //...
    void encodeString(std::string const& s);
    void encodeString(std::wstring const& s);

    uint8_t decode8BitsFrom(unsigned offset) const;
    uint16_t decode16BitsFrom(unsigned offset) const;
    //...
private:
    std::vector<uint8_t> buffer_;
};

您的每个数据包类都有一个方法可以序列化为 aByteBuffer或从 aByteBuffer和偏移量反序列化。这是我绝对希望我能回到过去并纠正的事情之一。我无法计算由于忘记交换字节或未打包struct.

要避免的另一个陷阱是使用 aunion表示字节或memcpy使用无符号字符缓冲区提取字节。如果您总是在线上使用 Big-Endian,那么您可以使用简单的代码将字节写入缓冲区,而不必担心这些htonl东西:

void ByteBuffer::encode8Bits(uint8_t n) {
    buffer_.push_back(n);
}
void ByteBuffer::encode16Bits(uint16_t n) {
    encode8Bits(uint8_t((n & 0xff00) >> 8));
    encode8Bits(uint8_t((n & 0x00ff)     ));
}
void ByteBuffer::encode32Bits(uint32_t n) {
    encode16Bits(uint16_t((n & 0xffff0000) >> 16));
    encode16Bits(uint16_t((n & 0x0000ffff)      ));
}
void ByteBuffer::encode64Bits(uint64_t n) {
    encode32Bits(uint32_t((n & 0xffffffff00000000) >> 32));
    encode32Bits(uint32_t((n & 0x00000000ffffffff)      ));
}

这仍然很好地与平台无关,因为数字表示在逻辑上始终是 Big-Endian。这段代码也非常适合使用基于原始类型大小的模板(想想encode<sizeof(val)>((unsigned char const*)&val))......不是那么漂亮,但非常非常容易编写和维护。

于 2009-04-15T21:17:20.393 回答
2

我的经验是首选以下方法(按优先顺序):

  1. 使用高级框架,如 Tibco、CORBA、DCOM 或任何可以为您管理所有这些问题的框架。

  2. 在连接的两端编写自己的库,这些库都知道打包、字节顺序和其他问题。

  3. 仅使用字符串数据进行通信。

尝试在没有任何中介的情况下发送原始二进制数据几乎肯定会导致很多问题。

于 2009-04-15T20:05:59.130 回答
1

如果您想要任何形式的可移植性,您实际上不能为此使用类或结构。在您的示例中,整数可能是 32 位或 64 位,具体取决于您的系统。您很可能使用的是小端机器,但较旧的 Apple mac 是大端机器。编译器也可以随意填充。

通常,在确保使用 n2hll、n2hl 或 n2hs 获得正确的字节顺序之后,您需要一种将每个字段一次写入缓冲区的方法。

于 2009-04-15T20:08:56.783 回答
1

如果结构中没有自然对齐,编译器通常会插入填充以使对齐正确。但是,如果您使用编译指示来“打包”结构(删除填充),则可能会产生非常有害的副作用。在 PowerPC 上,未对齐的浮点数会产生异常。如果您正在使用不处理该异常的嵌入式系统,您将获得重置。如果有一个例程来处理该中断,它可能会大大减慢您的代码,因为它将使用软件例程来解决未对齐问题,这会默默地削弱您的性能。

于 2009-04-15T21:58:03.587 回答