车载以太网之要火系列 - 第53篇:郭大侠学DDS(数据帧):数据入帧君需知,序列化后力道施
写在开篇·蓉儿继续挖坑上回说到郭靖搞清楚了Topic是数据“主题”架构师在Excel里定好名字、类型工具生成代码工程师填业务逻辑。郭靖合上笔记本若有所思“蓉儿我大概知道Topic是什么了。但我有个疑问——Topic里的数据结构比如刹车指令是怎么变成RTPS报文中那一串字节的还有writerId和writerSN这两个家伙到底是谁定义的”黄蓉咬了口糖葫芦“问得好这就是序列化要解决的问题。今天就把数据怎么放入数据帧讲透——从内存里的结构体到RTPS报文里的字节流中间经历了什么。顺便把writerId和writerSN的来历讲清楚。”一、问题数据在内存里和网络上的形态不一样黄蓉在白板上画了一个简单的对比┌─────────────────────────────────────────────────────────────────────┐ │ 数据在内存里 vs 数据在网络上的形态 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 内存里发布者 │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ struct BrakeCommand { │ │ │ │ uint16_t pressure 500; // 内存地址0x1000: 0x01F4 │ │ │ │ uint8_t is_emergency 1; // 内存地址0x1002: 0x01 │ │ │ │ uint32_t timestamp 1700000000; // 内存地址0x1004: ... │ │ │ │ } │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ 序列化Serialization │ │ ▼ │ │ 网络上RTPS报文 │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ [DATA子消息头][writerId][writerSN][...][序列化后的数据] │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ 01 F4 01 65 5B 5B 00 │ │ │ │ └pressure┘└is_emergency┘└timestamp─┘│ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘郭靖“内存里是结构体网络上是字节流。这两者之间怎么转换的”黄蓉“序列化Serialization——把内存中的结构体按约定规则转换成字节流。接收方再做反序列化Deserialization把字节流还原成结构体。”二、一个简单的例子刹车指令Topic黄蓉用刹车指令来举例因为数据类型简单容易理解。Topic定义Topic名称/vehicle/brake/cmd 数据类型BrakeCommand BrakeCommand结构 ├── pressure刹车压力uint160-1000对应0%-100% ├── is_emergency是否紧急刹车uint80/1 └── timestamp时间戳uint32发布者内存中的数据pressure 500 50%刹车 is_emergency 1 紧急刹车 timestamp 1700000000三、序列化把结构体变成字节流黄蓉画了序列化的过程┌─────────────────────────────────────────────────────────────────────┐ │ 序列化过程以刹车指令为例 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 步骤1内存中的结构体 │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ pressure 500 (0x01F4) │ │ │ │ is_emergency 1 (0x01) │ │ │ │ timestamp 1700000000 (0x655B5B00) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 步骤2按字段顺序排列CDR规范 │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ [pressure 2字节][is_emergency 1字节][timestamp 4字节] │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 步骤3根据字节序转换大端/小端 │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 采用大端网络字节序 │ │ │ │ pressure → 0x01 0xF4 │ │ │ │ is_emergency→ 0x01 │ │ │ │ timestamp → 0x65 0x5B 0x5B 0x00 │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 步骤4得到最终的字节流 │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 01 F4 01 65 5B 5B 00 │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘郭靖恍然大悟“哦原来结构体里的字段是按顺序一个接一个排进字节流里的”四、writerId和writerSN谁定义的怎么来的郭靖指着报文“蓉儿你之前说的writerId和writerSN这两个家伙到底是谁定义的工程师能自己指定吗”1. writerId写入者ID属性说明长度4字节谁分配的DDS协议栈在发现阶段自动分配不是人工指定的唯一性同一个DomainParticipant内每个Writer有唯一的writerId作用接收方根据writerId知道“这条数据是哪个发布者发的”工程师能改吗不能。99%的情况下不需要关心发现阶段的分配流程摄像头ECU发布者 域控制器订阅者 │ │ │ ① Participant发现互相打招呼 │ │─────────────────────────────────────────│ │ │ │ ② Writer发现摄像头告诉域控我要发数据 │ │ “我创建了一个Writer我的writerId0x0001” │ │──────────────────────────────────────────│ │ │ │ ③ Reader发现域控告诉摄像头我要收数据 │ │ “我创建了一个Reader我匹配你的writerId” │ │──────────────────────────────────────────│2. writerSN写入者序列号属性说明长度8字节谁维护的每个Writer自己维护每发一个样本1初始值通常从1开始作用接收方检测丢包跳号了就知道丢了、去重重复的丢弃、可靠传输时请求重传工程师能改吗不能。DDS自动维护序列号的使用场景写入者摄像头 读取者域控 │ │ │ DataSN1 │ │──────────────────────────────────────────│ 收到记下SN1 │ DataSN2 │ │──────────────────────────────────────────│ 收到记下SN2 │ DataSN3 │ │──────────────────────────────────────────│ 收到记下SN3 │ DataSN5 ← 跳过了4 │ │──────────────────────────────────────────│ 检测到丢包 │ │ │ AckNack“我缺SN4” │ │──────────────────────────────────────────│ │ │ │ DataSN4 ← 重传 │ │──────────────────────────────────────────│小结对比writerIdwriterSN谁定的DDS发现阶段自动分配Writer自己维护从1开始递增工程师能改吗不能不能有什么用接收方识别数据来源检测丢包、去重、重传从哪里看到Wireshark抓包Wireshark抓包需要关心吗99%不用除非深度调试99%不用除非排查丢包五、完整的RTPS DATA子消息报文拆解下面是一个完整的RTPS DATA子消息报文十六进制逐字段拆解52 54 50 53 02 02 01 00 11 22 33 44 55 66 77 88 99 AA BB CC 15 03 00 30 00 00 01 00 00 00 02 00 00 00 00 00 00 00 01 00 00 00 00 01 F4 01 65 5B 5B 00 00 00 00 00第一部分RTPS消息头24字节字节偏移字段值长度定义和作用0-3RTPS标识52 54 50 534字节固定值R T P S。Wireshark靠这4个字节识别这是RTPS报文4Protocol Version (major)021字节主版本号25Protocol Version (minor)021字节次版本号26-7Vendor ID01 002字节供应商标识。0x0100RTI常用DDS供应商8-19GUID Prefix11 22 33 44 55 66 77 88 99 AA BB CC12字节全局唯一参与者标识区分不同的DDS应用第二部分DATA子消息头4字节字节偏移字段值长度定义和作用20Submessage ID151字节子消息类型DATA0x15告诉解析器“后面是数据”21Flags031字节标志位bit01大端bit11内联QoS22-23Submessage Length00 302字节子消息长度48字节不含头部第三部分DATA子消息体字节偏移字段值长度定义和作用24-27readerId00 00 01 004字节读取者ID。匹配的Reader的实体ID告诉数据发给谁28-31writerId00 00 02 004字节写入者ID。标识是哪个发布者在发数据DDS发现阶段自动分配32-39writerSN00 00 00 00 00 00 00 018字节写入者序列号。这是该Writer发送的第1个样本自动递增40-41inlineQoS00 002字节内联QoS本例中无额外QoS42-43表示标识符00 002字节PL_CDR_BE表示后面是CDR序列化数据大端字节序44-45表示选项00 002字节选项标志0x0000表示无额外选项46-52serializedPayload01 F4 01 65 5B 5B 007字节序列化后的刹车指令数据├──01 F42字节pressure50050%刹车├──011字节is_emergency1紧急刹车└──65 5B 5B 004字节timestamp170000000053-56填充00 00 00 004字节对齐填充保证下一个子消息从4字节边界开始关键字段总结字段一句话作用工程师要不要管RTPS标识52 54 50 53告诉网卡“我是DDS”❌ 不用GUID Prefix区分不同的DDS应用❌ 不用writerId标识“谁发的”❌DDS自动分配不用管writerSN标识“第几个”用于丢包检测❌DDS自动维护不用管readerId标识“发给谁”❌ DDS自动匹配serializedPayload你的数据✅这就是你填的刹车指令六、反序列化接收方怎么还原数据黄蓉画了接收方的过程和序列化相反┌─────────────────────────────────────────────────────────────────────┐ │ 反序列化过程接收方 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 步骤1从RTPS报文中提取serializedPayload │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 01 F4 01 65 5B 5B 00 │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 步骤2按字段顺序解析CDR规范 │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ [0-1字节] pressure 0x01F4 500 │ │ │ │ [2字节] is_emergency 0x01 1紧急 │ │ │ │ [3-6字节] timestamp 0x655B5B00 1700000000 │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 步骤3填入内存中的结构体 │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ BrakeCommand cmd; │ │ │ │ cmd.pressure 500; // 50%刹车 │ │ │ │ cmd.is_emergency 1; // 紧急刹车 │ │ │ │ cmd.timestamp 1700000000; │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘七、CDR规范序列化的“交通规则”郭靖问“那序列化有没有统一的规则万一发布者用大端订阅者用小端不就乱套了”黄蓉“这就是CDRCommon Data Representation规范的作用。”CDR规则说明字节序可配置大端/小端在RTPS头部标志位标明基本类型长度uint162字节uint324字节uint648字节字符串编码先4字节长度后面跟UTF-8字符数组对齐基本类型按自身长度对齐结构体按最大成员对齐八、工程师真的需要关心这些吗郭靖问出了最关键的问题“蓉儿我实际写代码的时候需要自己写序列化代码吗需要自己定义writerId吗”黄蓉摇头不需要。DDS代码生成器会帮你生成序列化和反序列化代码。writerId和writerSN由DDS协议栈自动分配和维护。工程师做工具/DDS做定义Topic和数据类型IDL或设计文档生成序列化/反序列化代码填业务逻辑生成发布/订阅框架调用publish()负责把结构体变成字节流实现回调函数负责把字节流还原成结构体—自动分配writerId—自动维护writerSN郭靖松了口气“那就好我还以为要自己算每个字段占几个字节、大端小端、还要自己维护序列号……”黄蓉笑了“那是DDS协议栈的事不是你的事。你只管填数据DDS帮你打包。writerId和writerSN是系统自动管的你抓包能看到但写代码时不用操心。”九、黄蓉的小本本郭靖翻开她的笔记本上面写着数据放入数据帧的完整流程1. 定义数据结构架构师在设计文档里定好└── 刹车指令pressure is_emergency timestamp2. 工具生成序列化代码└── 把结构体按CDR规范转成字节流3. 发布者填数据└──cmd.pressure 500; cmd.is_emergency 1;4. DDS协议栈序列化└──500→0x01F41→0x01时间戳 →0x655B5B005. 装进RTPS DATA子消息└── 加上writerId自动分配、writerSN自动递增等塞进serializedPayload6. 接收方反序列化└── 字节流 → 结构体回调函数收到数据writerId和writerSNwriterIdDDS发现阶段自动分配标识数据来源writerSNWriter自己维护每发一个1用于丢包检测工程师不用管DDS自动搞定一句话工程师填结构体DDS帮你序列化。你不用操心字节怎么排也不用管writerId/writerSN。写在最后郭靖合上笔记本“原来数据放入数据帧中间有序列化这一步。writerId是DDS自动分配的writerSN是自动递增的工程师都不用管。结构体里的字段按顺序排成字节流装进RTPS DATA子消息的serializedPayload里。接收方再反序列化还原。”黄蓉咬了口糖葫芦“全明白了”郭靖点头“明白了。我不需要关心字节怎么排也不需要关心writerId/writerSN只需要关心数据本身。”黄蓉眨眨眼“那你知道谁负责发布、谁负责订阅吗怎么让DDS知道‘这个Topic我要发’、‘那个Topic我要收’”郭靖摇头。“下篇预告一收多发轻松办发布订阅各司职——Publisher和Subscriber。”打完收工886。