车载以太网之要火系列 - 第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(刹车压力):uint16(0-1000,对应0%-100%)
├── is_emergency(是否紧急刹车):uint8(0/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,我的writerId=0x0001” │
      │──────────────────────────────────────────>│
      │                                           │
      │  ③ Reader发现(域控告诉摄像头:我要收数据) │
      │  “我创建了一个Reader,我匹配你的writerId”  │
      │<──────────────────────────────────────────│

2. writerSN(写入者序列号)

属性 说明
长度 8字节
谁维护的 每个Writer自己维护,每发一个样本+1
初始值 通常从1开始
作用 接收方检测丢包(跳号了就知道丢了)、去重(重复的丢弃)、可靠传输时请求重传
工程师能改吗 不能。DDS自动维护

序列号的使用场景

复制代码
写入者(摄像头)                          读取者(域控)
      │                                           │
      │  Data(SN=1)                             │
      │──────────────────────────────────────────>│ 收到,记下SN=1
      │  Data(SN=2)                             │
      │──────────────────────────────────────────>│ 收到,记下SN=2
      │  Data(SN=3)                             │
      │──────────────────────────────────────────>│ 收到,记下SN=3
      │  Data(SN=5)  ← 跳过了4!                 │
      │──────────────────────────────────────────>│ 检测到丢包!
      │                                           │
      │  AckNack(“我缺SN=4”)                     │
      │<──────────────────────────────────────────│
      │                                           │
      │  Data(SN=4)  ← 重传                      │
      │──────────────────────────────────────────>│

小结

对比 writerId writerSN
谁定的 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-3 RTPS标识 52 54 50 53 4字节 固定值 'R' 'T' 'P' 'S'。Wireshark靠这4个字节识别这是RTPS报文
4 Protocol Version (major) 02 1字节 主版本号=2
5 Protocol Version (minor) 02 1字节 次版本号=2
6-7 Vendor ID 01 00 2字节 供应商标识。0x0100=RTI(常用DDS供应商)
8-19 GUID Prefix 11 22 33 44 55 66 77 88 99 AA BB CC 12字节 全局唯一参与者标识,区分不同的DDS应用

第二部分:DATA子消息头(4字节)

字节偏移 字段 长度 定义和作用
20 Submessage ID 15 1字节 子消息类型=DATA(0x15),告诉解析器"后面是数据"
21 Flags 03 1字节 标志位:bit0=1(大端),bit1=1(内联QoS)
22-23 Submessage Length 00 30 2字节 子消息长度=48字节(不含头部)

第三部分:DATA子消息体

字节偏移 字段 长度 定义和作用
24-27 readerId 00 00 01 00 4字节 读取者ID。匹配的Reader的实体ID,告诉数据发给谁
28-31 writerId 00 00 02 00 4字节 写入者ID。标识是哪个发布者在发数据,DDS发现阶段自动分配
32-39 writerSN 00 00 00 00 00 00 00 01 8字节 写入者序列号。这是该Writer发送的第1个样本,自动递增
40-41 inlineQoS 00 00 2字节 内联QoS(本例中无额外QoS)
42-43 表示标识符 00 00 2字节 PL_CDR_BE,表示后面是CDR序列化数据,大端字节序
44-45 表示选项 00 00 2字节 选项标志,0x0000表示无额外选项
46-52 serializedPayload 01 F4 01 65 5B 5B 00 7字节 序列化后的刹车指令数据
├── 01 F4 2字节 pressure=500(50%刹车)
├── 01 1字节 is_emergency=1(紧急刹车)
└── 65 5B 5B 00 4字节 timestamp=1700000000
53-56 填充 00 00 00 00 4字节 对齐填充,保证下一个子消息从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规范:序列化的"交通规则"

郭靖问:"那序列化有没有统一的规则?万一发布者用大端,订阅者用小端,不就乱套了?"

黄蓉:"这就是CDR(Common Data Representation)规范的作用。"

CDR规则 说明
字节序 可配置(大端/小端),在RTPS头部标志位标明
基本类型长度 uint16=2字节,uint32=4字节,uint64=8字节
字符串编码 先4字节长度,后面跟UTF-8字符
数组对齐 基本类型按自身长度对齐,结构体按最大成员对齐

八、工程师真的需要关心这些吗

郭靖问出了最关键的问题:"蓉儿,我实际写代码的时候,需要自己写序列化代码吗?需要自己定义writerId吗?"

黄蓉摇头:

不需要。DDS代码生成器会帮你生成序列化和反序列化代码。writerId和writerSN由DDS协议栈自动分配和维护。

工程师做 工具/DDS做
定义Topic和数据类型(IDL或设计文档) 生成序列化/反序列化代码
填业务逻辑 生成发布/订阅框架
调用publish() 负责把结构体变成字节流
实现回调函数 负责把字节流还原成结构体
--- 自动分配writerId
--- 自动维护writerSN

郭靖松了口气:"那就好!我还以为要自己算每个字段占几个字节、大端小端、还要自己维护序列号......"

黄蓉笑了:"那是DDS协议栈的事,不是你的事。你只管填数据,DDS帮你打包。writerId和writerSN是系统自动管的,你抓包能看到,但写代码时不用操心。"

九、黄蓉的小本本

郭靖翻开她的笔记本,上面写着:

数据放入数据帧的完整流程:

1. 定义数据结构 (架构师在设计文档里定好)

└── 刹车指令:pressure + is_emergency + timestamp

2. 工具生成序列化代码

└── 把结构体按CDR规范转成字节流

3. 发布者填数据

└── cmd.pressure = 500; cmd.is_emergency = 1;

4. DDS协议栈序列化

└── 5000x01F410x01,时间戳 → 0x655B5B00

5. 装进RTPS DATA子消息

└── 加上writerId(自动分配)、writerSN(自动递增)等,塞进serializedPayload

6. 接收方反序列化

└── 字节流 → 结构体,回调函数收到数据

writerId和writerSN

  • writerId:DDS发现阶段自动分配,标识数据来源

  • writerSN:Writer自己维护,每发一个+1,用于丢包检测

  • 工程师不用管,DDS自动搞定

一句话:工程师填结构体,DDS帮你序列化。你不用操心字节怎么排,也不用管writerId/writerSN。

写在最后

郭靖合上笔记本:"原来数据放入数据帧,中间有序列化这一步。writerId是DDS自动分配的,writerSN是自动递增的,工程师都不用管。结构体里的字段按顺序排成字节流,装进RTPS DATA子消息的serializedPayload里。接收方再反序列化还原。"

黄蓉咬了口糖葫芦:"全明白了?"

郭靖点头:"明白了。我不需要关心字节怎么排,也不需要关心writerId/writerSN,只需要关心数据本身。"

黄蓉眨眨眼:"那你知道谁负责发布、谁负责订阅吗?怎么让DDS知道'这个Topic我要发'、'那个Topic我要收'?"

郭靖摇头。

"下篇预告:一收多发轻松办,发布订阅各司职------Publisher和Subscriber。"

打完收工,886。

相关推荐
波特率1152009 天前
在ROS2当中两种rmw比较(CycloneDDS和FastDDS)
ros·ros2·dds
kyle~10 天前
CDR--- 数据序列化格式(DDS的底层数据支持)
机器人·信息与通信·ros2·dds
想成为优秀工程师的爸爸10 天前
车载以太网之要火系列 - 第48篇:郭大侠学SOME/IP (Subscribe订阅):想收通知要订阅,订阅之后随心阅
车载以太网·some/ip·自学笔记
想成为优秀工程师的爸爸11 天前
车载以太网之要火系列 - 第47篇:郭大侠学SOME/IP (Find Service):主动通知未收好,自己寻问自己找
车载以太网·some/ip·自学笔记
想成为优秀工程师的爸爸11 天前
车载以太网之要火系列 - 第46篇:郭大侠学SOME/IP (offer Service):启动时快稍后慢,断断续续哥还在
车载以太网·some/ip·自学笔记
想成为优秀工程师的爸爸12 天前
车载以太网之要火系列 - 第45篇:郭大侠学SOME/IP (Offer Service):上电主动会喊话,Offer告知我会啥
车载以太网·some/ip·自学笔记
虹科汽车电子15 天前
自动驾驶域控开发与测试实践:虹科车载以太网方案赋能L3量产落地
人工智能·自动驾驶·车载以太网·车辆网络通讯测试·自动驾驶域控开发
想成为优秀工程师的爸爸15 天前
车载以太网之要火系列 - 第40篇:郭大侠学SOME/IP - Method vs Event:一个一问一答,一个自己说话
车载以太网·some/ip·自学笔记
想成为优秀工程师的爸爸17 天前
车载以太网之要火系列 - 第37篇:郭大侠学SOME/IP - 玄之又玄谓之道,报文头中藏玄妙
车载以太网·some/ip·自学笔记