4. SOME/IP序列化(Serialization):基本数据类型序列化、结构体序列化、字符串与数组序列化、序列化对齐规则

好,咱们今天聊聊序列化。说白了,序列化就是把内存里的数据结构,变成一串能在网络上传输的字节流。反过来叫反序列化。SOME/IP的序列化规则,我当年刚接触时也觉得有点繁琐,但用久了你会发现,它其实设计得很精巧——既考虑了效率,又兼顾了灵活性。

4.1 基本数据类型序列化

SOME/IP支持的基本类型包括:uint8uint16uint32uint64sint8sint16sint32sint64float32float64boolean等。序列化时,它们都采用小端字节序(Little-Endian)。

为什么是小端?我个人习惯这么理解:大多数嵌入式处理器(比如ARM、x86)本身就是小端架构,直接用内存里的字节顺序发送,省去了字节序转换的开销。你想想看,在车载ECU这种资源受限的环境里,少一次字节交换就是一次性能提升。

核心规则:所有基本类型按原始大小直接编码,不填充额外字节。

举个例子,一个uint32的值0x12345678,序列化后的字节流是:78 56 34 12。嗯,这里要注意,如果你在调试时抓包看到这个顺序,别慌,这是对的。

// 伪代码示例:uint32序列化
void serialize_uint32(uint32_t value, uint8_t* buffer) {
    buffer[0] = (value >> 0)  & 0xFF;  // 最低字节
    buffer[1] = (value >> 8)  & 0xFF;
    buffer[2] = (value >> 16) & 0xFF;
    buffer[3] = (value >> 24) & 0xFF;  // 最高字节
}

对于boolean类型,SOME/IP规定用uint8表示,0x00为假,0x01为真。我曾经在项目中遇到过有人把true序列化成0xFF,结果接收方死活解析不对。记住,只有0x01才是真,其他值都是未定义的。

4.2 结构体序列化

结构体的序列化,说白了就是把里面的每个成员按顺序依次序列化。但这里有个坑——对齐规则。

SOME/IP的结构体序列化遵循8字节对齐原则。什么意思?就是每个成员的起始偏移地址,必须是其自身大小的整数倍,并且整个结构体的大小必须是8的倍数。

注意:这个对齐规则和C语言结构体的自然对齐不一样。C语言里结构体对齐由编译器决定(通常是4字节或8字节),但SOME/IP强制使用8字节对齐。如果你直接把C结构体指针强转成字节流发送出去,大概率会出问题。

来看个例子。假设有这样一个结构体:

struct Example {
    uint8_t  a;   // 1字节
    uint16_t b;   // 2字节
    uint32_t c;   // 4字节
};

按照SOME/IP的序列化规则,内存布局是这样的:

偏移 内容 说明
0 a uint8_t,占1字节
1 填充 对齐到2字节边界,填充1字节
2-3 b uint16_t,占2字节
4-7 c uint32_t,占4字节
8-15 填充 整个结构体对齐到8字节,填充8字节

你看,实际有效数据只有7字节,但序列化后占16字节。为什么最后还要填充8字节?因为SOME/IP要求结构体总大小是8的倍数,这样接收方可以按固定步长解析结构体数组。

我的建议:在设计接口时,尽量把成员按大小从大到小排列(uint64、uint32、uint16、uint8),这样可以减少填充字节,节省带宽。我在做ADAS系统时,一个结构体优化后从32字节降到了24字节,效果很明显。

4.3 字符串与数组序列化

字符串和数组属于动态长度类型,序列化时需要在数据前面加上长度字段

4.3.1 字符串序列化

SOME/IP中的字符串采用UTF-8编码,序列化格式为:

  • 长度字段:4字节(uint32),表示字符串的字节数(不含终止符)
  • 字符串数据:UTF-8编码的字节序列
  • 终止符:一个0x00字节

举个例子,字符串"Hello"序列化后:

05 00 00 00  48 65 6C 6C 6F 00
|--长度=5--|  |--H e l l o--| \0

嗯,这里要注意,长度字段是5,不包括最后的\0。但发送时一定要带上\0,这是SOME/IP的规范要求。我曾经在调试一个车载信息娱乐系统时,发现字符串总是多出乱码,排查了半天才发现是忘了加终止符。

4.3.2 固定长度数组

固定长度数组的序列化比较简单,直接把每个元素依次序列化即可,不需要长度字段。比如uint8[4]就是4个连续的字节。

4.3.3 动态长度数组

动态长度数组(比如vector<uint32>)的序列化格式为:

  • 长度字段:4字节(uint32),表示数组元素的个数
  • 元素数据:每个元素按自身类型序列化,依次排列

举个例子,一个包含3个uint32的数组[0x01, 0x02, 0x03]

03 00 00 00  01 00 00 00  02 00 00 00  03 00 00 00
|--个数=3--|  |--元素1--|  |--元素2--|  |--元素3--|

关键点:数组元素的对齐规则和基本类型一样,每个元素都从自身大小的整数倍地址开始。如果数组元素是结构体,则每个结构体都按8字节对齐。

4.4 序列化对齐规则总结

我把SOME/IP的序列化对齐规则总结成一张表,方便你查阅:

数据类型 对齐要求 填充规则
uint8 / sint8 / boolean 1字节对齐 无填充
uint16 / sint16 2字节对齐 如果起始地址是奇数,填充1字节
uint32 / sint32 / float32 4字节对齐 如果起始地址不是4的倍数,填充到4的倍数
uint64 / sint64 / float64 8字节对齐 如果起始地址不是8的倍数,填充到8的倍数
结构体 8字节对齐 成员间按各自对齐要求填充,末尾填充到8的倍数
字符串 无对齐要求 长度字段后紧跟数据,末尾加\0
动态数组 元素按自身类型对齐 长度字段后紧跟元素,元素间按对齐要求填充

最后说一个我踩过的坑。有一次做OTA升级服务,结构体里有个uint8字段后面跟着一个uint64字段。按照SOME/IP对齐规则,uint64需要8字节对齐,所以uint8后面要填充7个字节。我当时没注意,直接用C结构体序列化,结果接收方解析出来的uint64值全是错的。后来加上手动填充才搞定。

所以我的建议是:永远不要依赖编译器的内存布局来做SOME/IP序列化。写一个专门的序列化/反序列化函数,手动处理每个字段的对齐和填充。虽然代码量多了点,但绝对值得。