3、SOME/IP序列化与反序列化:PDU结构、TLV编码、数据类型映射
好,咱们今天聊聊SOME/IP里最实在的东西——序列化与反序列化。
说白了,就是把咱们程序里的结构体、数组、字符串,变成一串能在以太网上跑的二进制流。反过来,收到二进制流再还原成程序能认的数据结构。这个过程,就是SOME/IP通信的基石。
我刚开始接触SOME/IP时,觉得这不就是编解码嘛,有啥难的?后来踩了几个坑才明白,细节决定成败。尤其是PDU结构怎么设计、TLV怎么用、各种数据类型怎么映射,稍不留神就会出兼容性问题。
3.1 PDU结构:数据在线上怎么排
先看PDU。Protocol Data Unit,协议数据单元。SOME/IP的PDU结构其实挺清晰的,我画个简图你感受下:
+------------------+
| Message ID | 4字节
+------------------+
| Length | 4字节
+------------------+
| Request ID | 4字节
+------------------+
| Protocol Version | 1字节
| Interface Version| 1字节
| Message Type | 1字节
| Return Code | 1字节
+------------------+
| Payload | 变长
+------------------+
嗯,这里要注意:Length字段是从Request ID之后开始算的,不包括Message ID和Length本身。这个坑我见过好几次,有人把整个报文长度都填进去了,结果接收方解析出来全是错的。
Payload部分才是咱们真正要序列化的数据。它可以是固定长度的,也可以是TLV编码的。具体用哪种,取决于你的服务接口设计。
3.2 TLV编码:灵活但别滥用
TLV,Tag-Length-Value。说白了就是给每个数据字段加个标签和长度前缀。
为什么需要TLV?因为有些场景下,数据字段是可选的,或者长度不固定。比如诊断服务、配置更新这类场景,用TLV就特别灵活。
TLV的基本结构:
+--------+--------+--------+
| Tag | Length | Value |
| 2字节 | 2字节 | 变长 |
+--------+--------+--------+
Tag用来标识这是什么字段,Length告诉接收方Value有多长。接收方读到Tag不认识?直接跳过Length指定的字节数就行。这就是TLV最大的好处——向前兼容。
我曾经踩过的坑:TLV的Tag和Length字节数,不同协议栈可能不一样。有的用1字节Tag,有的用2字节。设计初期一定要定死,不然后面联调时全是泪。
我个人习惯,在车载项目里TLV的Tag用2字节,Length用2字节。这样Tag范围够大,Length也能覆盖大部分场景。如果数据特别大,可以考虑用4字节Length,但一般用不上。
3.3 数据类型映射:从代码到二进制
这块是序列化的核心。咱们程序里的各种数据类型,怎么变成线上一串0和1?
3.3.1 基础类型
| C++类型 | SOME/IP类型 | 字节数 | 说明 |
|---|---|---|---|
| uint8 | UINT8 | 1 | 无符号8位 |
| uint16 | UINT16 | 2 | 无符号16位,大端序 |
| uint32 | UINT32 | 4 | 无符号32位,大端序 |
| uint64 | UINT64 | 8 | 无符号64位,大端序 |
| sint8 | SINT8 | 1 | 有符号8位 |
| sint16 | SINT16 | 2 | 有符号16位,大端序 |
| float32 | FLOAT32 | 4 | IEEE 754单精度 |
| float64 | FLOAT64 | 8 | IEEE 754双精度 |
注意:SOME/IP统一使用大端序(网络字节序)。你想想看,如果有的ECU用小端,有的用大端,那不乱套了?所以协议里直接定死了,大家都用大端。
我在项目中遇到过一个问题:某供应商的MCU默认是小端,他们忘了做字节序转换,结果传过来的uint16数值全反了。排查了一整天,最后发现是字节序的问题。
3.3.2 结构体
结构体的序列化,说白了就是把成员变量按顺序排好,一个接一个地写进buffer里。
// 定义结构体
struct VehicleSpeed {
uint32 timestamp; // 4字节
float speed; // 4字节
uint8 quality; // 1字节
};
// 序列化后的二进制布局
+----------+-------+---------+
|timestamp | speed | quality |
| 4字节 | 4字节 | 1字节 |
+----------+-------+---------+
这里有个细节:对齐问题。SOME/IP序列化时,结构体成员是紧密排列的,不做内存对齐。也就是说,上面那个结构体序列化后就是9字节,不是12字节。
小技巧:设计结构体时,尽量把相同大小的成员放一起。比如所有uint32放前面,uint8放后面。这样序列化代码写起来更规整,也方便调试。
3.3.3 数组
数组分两种:固定长度数组和动态长度数组。
固定长度数组:直接按顺序把每个元素序列化就行,不需要长度前缀。
// 固定长度数组
uint8 rgb[3] = {0xFF, 0x00, 0x00};
// 序列化后
+------+------+------+
| 0xFF | 0x00 | 0x00 |
+------+------+------+
动态长度数组:需要在数据前面加一个长度字段,告诉接收方有多少个元素。
// 动态长度数组
vector<uint16> values = {100, 200, 300};
// 序列化后
+--------+------+------+------+
| length | val1 | val2 | val3 |
| 4字节 | 2字节 | 2字节 | 2字节 |
+--------+------+------+------+
// length = 3
嗯,这里要注意:长度字段的单位是元素个数,不是字节数。有些协议栈用字节数,但SOME/IP规范里明确说是元素个数。我见过有人搞混,结果解析出来的数组完全不对。
3.3.4 字符串
字符串在SOME/IP里,本质上就是个动态长度的字节数组。
// 字符串
string name = "Hello";
// 序列化后
+--------+-----+-----+-----+-----+-----+
| length | 'H' | 'e' | 'l' | 'l' | 'o' |
| 4字节 | 1字节| 1字节| 1字节| 1字节| 1字节|
+--------+-----+-----+-----+-----+-----+
// length = 5
字符串的编码方式,SOME/IP默认使用UTF-8。我个人建议,所有字符串都用UTF-8。为什么?因为UTF-8兼容ASCII,而且没有字节序问题。如果你用UTF-16,还得操心大端小端,麻烦得很。
核心要点:
- 基础类型:统一大端序,字节数固定
- 结构体:成员紧密排列,不做对齐
- 数组:固定长度直接排,动态长度加元素个数前缀
- 字符串:UTF-8编码,长度字段是字符数
3.4 反序列化:逆向操作,但更考验细节
反序列化就是序列化的逆过程。但说实话,反序列化比序列化更容易出bug。
为什么?因为序列化是你自己写的,你知道数据长什么样。反序列化时,你面对的是别人发来的数据,可能格式不对、长度不对、甚至恶意数据。
我写反序列化代码时,一定会做这几件事:
- 长度检查:读取每个字段前,先确认buffer里还有足够的字节
- 范围检查:数值是否在合理范围内?比如uint8不能超过255
- 版本检查:如果TLV的Tag不认识,是忽略还是报错?
举个例子,反序列化一个动态数组:
// 伪代码
uint32_t element_count = read_uint32(buffer);
if (element_count > MAX_ARRAY_SIZE) {
// 异常处理,防止内存溢出
return ERROR;
}
for (uint32_t i = 0; i < element_count; i++) {
uint16_t value = read_uint16(buffer);
array.push_back(value);
}
你看,先检查元素个数是否合理。如果不检查,对方发个element_count = 0xFFFFFFFF,你的程序直接崩了。
我曾经踩过的坑:有一次反序列化字符串时,忘了检查长度字段。对方发了个长度=0的字符串,结果我直接memcpy了0字节,看起来没问题。但后续代码以为字符串有内容,访问了空指针。这种bug特别隐蔽,很难复现。
3.5 实战建议
说了这么多,最后给几个实战中的建议:
- 序列化反序列化代码,一定要写单元测试。构造各种边界情况的数据,确保代码健壮。
- 用工具生成代码。手动写序列化代码太容易出错了。ARXML定义好接口,用工具自动生成,省心省力。
- 日志要详细。序列化失败时,把buffer内容以hex形式打印出来。调试时能省一半时间。
- 版本兼容性。设计PDU时,预留一些保留字段。后续加功能时,不用改协议版本号。
好了,序列化与反序列化这块就聊到这儿。说白了就是:写的时候规规矩矩,读的时候小心翼翼。下一章咱们聊聊SOME/IP的服务发现,那才是真正有意思的东西。