网络数据包有效负载数据应该在适当的边界上对齐

时间:2009-04-15 19:58:05

标签: c++ c network-programming sockets payload

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

班级有效载荷 {   char field0;   int field1;   char field2;   int field3; };

使用像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; 
};

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

您对网络数据中的这种对齐问题有什么经验?

6 个答案:

答案 0 :(得分:8)

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

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

一个好的起点是Boost Serialization

答案 1 :(得分:4)

你应该像另一张海报所说的那样研究Google protocol buffers或Boost :: serialize。

如果您想自己动手,请正确行事。

如果你使用stdint.h中的类型(即:uint32_t, int8_t,等),并确保每个变量都有“原生对齐”(意味着它的地址可以按其大小均匀分割(int8_t s在任何地方,uint16_t在偶数地址上,uint32_t在地址上可被4分割,你不必担心对齐或打包。

在之前的工作中,我们通过XML定义的数据总线(以太网或CANbus或byteflight或串行端口)发送了所有结构。有一个解析器可以验证结构中变量的对齐(提醒您是否有人编写了错误的XML),然后为各种平台和语言生成头文件以发送和接收结构。这对我们来说非常非常,我们从不不得不担心手写代码来进行消息解析或打包,并保证所有平台都不会有愚蠢编码错误很少。我们的一些数据链路层受带宽限制,因此我们实现了像位域这样的功能,解析器为每个平台生成适当的代码。我们也有一些枚举,这非常好(你会惊讶于人类用手工编写枚举的位域是多么容易)。

除非你需要担心它在8051s和带有C的HC11上运行,或者在带宽受限的数据链路层上运行,否则你不会想出比协议缓冲区好的东西,你只会花很多钱时间试图与他们相提并论。

答案 2 :(得分:4)

我们今天在内存中使用直接叠加在二进制数据包上的压缩结构,而我在决定这样做的那一天。我们实现这一目标的唯一方法是:

  1. 根据编译环境(typedef unsigned int uint32_t
  2. 仔细定义特定于位宽的类型
  3. 插入适当的特定于编译器的编译指示以指定结构成员的紧密打包
  4. 要求所有内容都按一个字节顺序排列(使用网络或大端排序)
  5. 仔细编写服务器和客户端代码
  6. 如果你刚刚开始,我建议你跳过整个试图用结构来表示电线上的东西。只需单独序列化每个原始元素。如果您选择不使用像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_;
    };
    

    每个数据包类都有一个序列化为ByteBuffer或从ByteBuffer和偏移量反序列化的方法。这是我绝对希望能够及时回归正确的事情之一。我无法计算我花时间调试因忘记交换字节或未打包struct而导致的问题的次数。

    要避免的另一个陷阱是使用union表示字节或memcpy到unsigned char缓冲区来提取字节。如果你总是在线上使用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))......不是那么漂亮,但非常非常容易编写和维护。

答案 3 :(得分:2)

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

  1. 使用Tibco,CORBA,DCOM等高级框架或任何可以解决所有这些问题的框架。

  2. 在连接的两端编写自己的库,了解打包,字节顺序和其他问题。

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

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

答案 4 :(得分:1)

如果你想要任何类型的可移植性,你实际上不能使用类或结构。在您的示例中,int可能是32位或64位,具体取决于您的系统。你最有可能使用一个小端机器,但较旧的Apple macs是大端。编译器也可以随意填充。

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

答案 5 :(得分:1)

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