4、SOME/IP序列化与反序列化:PDU结构、序列化规则、数据类型映射、对齐与填充
好,咱们今天聊点硬核的。序列化与反序列化,说白了就是把内存里的数据结构,变成能在网线上跑的字节流,然后再变回来。这事儿听起来简单,但做起来坑特别多。我在好几个项目里都栽过跟头,后来才慢慢摸清楚门道。
4.1 PDU结构:数据在线上到底长什么样?
先说说PDU,Protocol Data Unit。你可以把它想象成一个信封。SOME/IP的PDU结构,我习惯分成三块来看:
- 头部(Header):固定16字节,包含Service ID、Method ID、Length、Client ID等。这部分是硬性规定,谁也别想改。
- 载荷(Payload):真正要传的数据。这部分怎么组织,就是我们今天要聊的重点。
- 尾部(Trailer):有些协议会加,但SOME/IP基本不用。
嗯,这里要注意:Length字段是从Payload开始算的,不包括Header自身。我见过有人把整个PDU长度填进去,结果对端解析直接崩了。
核心要点:SOME/IP的PDU头部固定16字节,Payload的序列化方式由AUTOSAR的序列化规则决定。
4.2 序列化规则:字节序与数据布局
序列化规则,说白了就是「怎么把数据排成一排」。AUTOSAR里规定得很细,我挑几个关键点说说。
4.2.1 字节序(Byte Order)
SOME/IP默认用大端序(Big-Endian)。为什么?因为网络协议大多用大端,方便调试。你想想看,用Wireshark抓包时,大端序一眼就能看出数值,小端序还得在脑子里反转一下。
不过,AUTOSAR也允许用小端序(Little-Endian),但必须在某个地方声明。我个人建议:统一用大端。我在一个项目里遇到过,ECU A用大端,ECU B用小端,两边都没声明,结果通信数据全是乱码。排查了整整两天才发现是字节序问题。
4.2.2 数据布局规则
AUTOSAR的序列化规则,我总结成三条:
- 基本数据类型:直接按字节序排列。比如uint32,就是4个字节,大端序高位在前。
- 复合数据类型:按成员顺序排列,每个成员独立序列化。
- 数组和字符串:先序列化长度(如果有),再序列化元素。
举个例子,一个结构体:
struct VehicleSpeed {
uint8_t status; // 1字节
uint16_t speed; // 2字节
uint32_t timestamp; // 4字节
};
序列化后的字节流(大端序):
| status (1B) | speed high (1B) | speed low (1B) | timestamp high (1B) | ... | timestamp low (1B) |
你看,status占1字节,speed占2字节,timestamp占4字节。就这么简单直接。
4.3 数据类型映射:C语言类型与SOME/IP类型的对应关系
这个映射表,我建议你打印出来贴在工位上。我在项目中经常需要查,后来干脆背下来了。
| SOME/IP类型 | C语言类型(AUTOSAR) | 字节数 | 说明 |
|---|---|---|---|
| UINT8 | uint8 | 1 | 无符号8位整数 |
| SINT8 | sint8 | 1 | 有符号8位整数 |
| UINT16 | uint16 | 2 | 无符号16位整数 |
| SINT16 | sint16 | 2 | 有符号16位整数 |
| UINT32 | uint32 | 4 | 无符号32位整数 |
| SINT32 | sint32 | 4 | 有符号32位整数 |
| FLOAT32 | float32 | 4 | IEEE 754单精度浮点数 |
| FLOAT64 | float64 | 8 | IEEE 754双精度浮点数 |
| BOOLEAN | boolean | 1 | 0x00为false,非0为true |
| STRING | uint8[] | 可变 | UTF-8编码,前4字节为长度 |
个人经验:BOOLEAN类型在SOME/IP里只占1字节,但有些实现会把它当成4字节。我建议你在设计阶段就明确约定好,否则联调时会有惊喜。
4.4 对齐与填充:字节对齐的坑
对齐,是序列化里最容易出问题的地方。为什么要有对齐?因为CPU访问对齐的数据更快。但网络传输时,对齐反而会浪费带宽。
4.4.1 AUTOSAR的对齐规则
AUTOSAR规定:每个数据成员的起始偏移量,必须是其自身大小的整数倍。比如:
- uint8:偏移量必须是1的倍数(其实就是任意位置)
- uint16:偏移量必须是2的倍数
- uint32:偏移量必须是4的倍数
- uint64:偏移量必须是8的倍数
如果前一个成员结束位置不满足对齐要求,就需要填充(Padding)字节。
4.4.2 填充示例
看这个结构体:
struct Example {
uint8_t a; // 偏移0,占1字节
// 填充1字节(因为b需要2字节对齐)
uint16_t b; // 偏移2,占2字节
uint32_t c; // 偏移4,占4字节
};
序列化后的字节流:
| a (1B) | padding (1B) | b high (1B) | b low (1B) | c high (1B) | ... | c low (1B) |
你看,a和b之间多了一个填充字节。这个填充字节的值是0x00。
避坑指南:我曾经在一个项目里,结构体定义时没注意成员顺序,结果序列化后多了4个填充字节。对端解析时直接按顺序读,数据全错位了。后来我养成了一个习惯:把大的数据类型放在前面,小的放后面,这样可以减少填充。
4.4.3 结构体末尾的填充
AUTOSAR还规定:结构体末尾也要填充到最大成员大小的整数倍。比如上面的Example结构体,最大成员是uint32(4字节),总大小是1+1+2+4=8字节,已经是4的倍数,所以不需要末尾填充。
但如果结构体是:
struct BadExample {
uint32_t x; // 偏移0,占4字节
uint8_t y; // 偏移4,占1字节
// 填充3字节(因为结构体总大小需要是4的倍数)
};
序列化后总大小是8字节,其中最后3字节是填充。
4.5 实战建议:序列化与反序列化的实现
说了这么多理论,来点实际的。我一般这样实现序列化:
// 序列化函数示例
void serialize_VehicleSpeed(const VehicleSpeed* data, uint8_t* buffer) {
uint32_t offset = 0;
// 序列化status(uint8,无需对齐)
buffer[offset++] = data->status;
// 对齐到2字节边界
if (offset % 2 != 0) {
buffer[offset++] = 0x00; // 填充字节
}
// 序列化speed(uint16,大端序)
buffer[offset++] = (data->speed >> 8) & 0xFF;
buffer[offset++] = data->speed & 0xFF;
// 对齐到4字节边界
while (offset % 4 != 0) {
buffer[offset++] = 0x00;
}
// 序列化timestamp(uint32,大端序)
buffer[offset++] = (data->timestamp >> 24) & 0xFF;
buffer[offset++] = (data->timestamp >> 16) & 0xFF;
buffer[offset++] = (data->timestamp >> 8) & 0xFF;
buffer[offset++] = data->timestamp & 0xFF;
}
我的习惯:写序列化代码时,我会在关键位置加断言(assert),检查偏移量是否正确。比如序列化完uint16后,断言offset % 2 == 0。这样一旦出错,调试阶段就能发现,不用等到实车测试。
反序列化就是反过来,从buffer里按同样的规则读出数据。注意:填充字节要跳过,不要读进来。
嗯,序列化这块内容不少,但核心就是:理解PDU结构、遵守序列化规则、搞清数据类型映射、处理好对齐与填充。把这四点吃透了,SOME/IP的序列化你就掌握了八成。剩下的两成,是在实际项目中踩坑积累的经验。
下一章,咱们聊聊SOME/IP的服务发现(SD),那又是一个容易出幺蛾子的地方。