4. SOME/IP序列化(Serialization):基本数据类型序列化、结构体序列化、字符串与数组序列化、序列化对齐规则
好,咱们今天聊聊序列化。说白了,序列化就是把内存里的数据结构,变成一串能在网络上传输的字节流。反过来叫反序列化。SOME/IP的序列化规则,我当年刚接触时也觉得有点繁琐,但用久了你会发现,它其实设计得很精巧——既考虑了效率,又兼顾了灵活性。
4.1 基本数据类型序列化
SOME/IP支持的基本类型包括:uint8、uint16、uint32、uint64、sint8、sint16、sint32、sint64、float32、float64、boolean等。序列化时,它们都采用小端字节序(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序列化。写一个专门的序列化/反序列化函数,手动处理每个字段的对齐和填充。虽然代码量多了点,但绝对值得。