写在开篇·蓉儿继续挖坑
上回说到,郭靖搞清楚了DDS的核心思想——发布-订阅,以数据为中心。发布者只管发,订阅者只管收,中间由DDS管理。
郭靖合上笔记本,信心满满:“蓉儿,DDS的Topic我懂了!就是数据的‘名字’,用字符串标识,一看就懂。”
黄蓉咬了口糖葫芦:“那好,我问你——DDS的报文长啥样?它跟SOME/IP报文有什么区别?你怎么一眼认出它是DDS报文?”
郭靖一愣:“这……”
“DDS底层跑的是RTPS协议。 今天就把RTPS报文格式讲透。读完这篇,你抓包看到RTP S这几个字节,就知道‘哦,这是DDS在说话’。”
附:RTPS协议速查表
💡 RTPS的背景:DDS是一个API标准,定义了“怎么用”。但不同的DDS厂家实现不同,互相之间不能通信。于是OMG组织定义了RTPS(Real-Time Publish-Subscribe Protocol),作为DDS的“有线协议”,让不同厂家的DDS产品能够互操作。
| 全称 | Real-Time Publish-Subscribe Protocol |
| 作用 | DDS的底层有线协议,定义报文格式和交互行为 |
| 传输层 | UDP/IP(通常),也支持TCP |
| 端口 | 动态分配,通过发现机制协商 |
| 与SOME/IP的区别 | SOME/IP头部12字节有Service ID/Method ID;RTPS更复杂,支持发现、可靠传输、分片等 |
一、RTPS是什么?为什么DDS需要它?
郭靖问:“SOME/IP直接跑在UDP上,报文头就12个字节。DDS为什么要搞个RTPS?”
黄蓉画了一张对比图:
text
┌─────────────────────────────────────────────────────────────────────┐
│ SOME/IP vs DDS/RTPS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ SOME/IP: │
│ ┌─────────────┐ │
│ │ SOME/IP报文 │ ──→ UDP ──→ IP ──→ 以太网 │
│ │ (12字节头) │ │
│ └─────────────┘ │
│ │
│ DDS/RTPS: │
│ ┌─────────────────────────────────────────────┐ │
│ │ RTPS消息(Header + 多个Submessage) │ │
│ │ ├── 发现子消息(PDP/EDP) │ │
│ │ ├── 心跳子消息(Heartbeat) │ │
│ │ ├── 确认子消息(AckNack) │ │
│ │ └── 数据子消息(Data) │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ UDP ──→ IP ──→ 以太网 │
│ │
└─────────────────────────────────────────────────────────────────────┘
“SOME/IP是‘轻量级’——头部简单,适合控制类通信。DDS/RTPS是‘重量级’——头部复杂,但支持自动发现、可靠传输、分片等高级功能。”
RTPS的核心价值:
| 性能与QoS | 在标准IP网络上实现最大努力和可靠的发布-订阅通信 |
| 容错性 | 允许创建无单点故障的网络 |
| 即插即用 | 自动发现新的应用程序和服务,无需重新配置 |
| 可配置性 | 允许平衡每次数据交付的可靠性和及时性要求 |
| 类型安全 | 防止应用程序编程错误影响远程节点的操作 |
二、RTPS消息的整体结构
黄蓉在白板上画了RTPS消息的结构:
┌─────────────────────────────────────────────────────────────────────┐
│ RTPS消息(Message) │
├─────────────────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 消息头(Header) │ │
│ │ 'R' 'T' 'P' 'S' │ 协议版本 │ 供应商ID │ 参与者前缀(12字节) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 子消息1(Submessage) │ │
│ │ ┌─────────────┬─────────────┬─────────────────────────────┐ │ │
│ │ │ 子消息ID │ 标志(flags) │ 子消息长度 │ │ │
│ │ └─────────────┴─────────────┴─────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ 子消息元素(SubmessageElement) │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 子消息2(Submessage) │ │
│ │ … │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
郭靖头又大了:“SOME/IP就12个字节头,这个RTPS怎么这么复杂?”
黄蓉:“SOME/IP是‘轻骑兵’,RTPS是‘重装部队’。 复杂是因为要支持自动发现、可靠传输、分片重传这些高级功能。”
三、消息头(Header)——RTPS的“身份证”
黄蓉拆开RTPS消息头:
| 0 | 'R' | 1字节 | RTPS标识 | 0x52 |
| 1 | 'T' | 1字节 | RTPS标识 | 0x54 |
| 2 | 'P' | 1字节 | RTPS标识 | 0x50 |
| 3 | 'S' | 1字节 | RTPS标识 | 0x53 |
| 4 | Protocol Version (major) | 1字节 | 主版本号 | 0x02 |
| 5 | Protocol Version (minor) | 1字节 | 次版本号 | 0x02 |
| 6-7 | Vendor ID | 2字节 | 供应商标识 | 如0x01 0x00 |
| 8-19 | GUID Prefix | 12字节 | 全局唯一参与者标识 | — |
“抓包时看到52 54 50 53(RTP S),就知道这是RTPS消息,底层是DDS在通信。”
郭靖对比:“SOME/IP的‘身份证’是03 FC 80 01,DDS/RTPS的‘身份证’是52 54 50 53。各念各的经!”
四、子消息(Submessage)——RTPS的“五脏六腑”
黄蓉继续说:“RTPS消息可以有多个子消息。根据功能,子消息分为三大类:”
4.1 子消息分类
| 数据型 | DATA、DATA_FRAG | 携带用户数据 | “信纸” |
| 可靠型 | HEARTBEAT、ACKNACK、GAP | 可靠传输、重传机制 | “回执” |
| 信息型 | INFO_TS、INFO_DST、INFO_SRC | 提供时间戳、目标地址等上下文 | “信封备注” |
4.2 子消息头结构
每个子消息都有固定的头结构:
| 0 | Submessage ID | 1字节 | 标识子消息类型(如DATA=0x15) |
| 1 | Flags | 1字节 | 标志位(字节序、是否内联QoS等) |
| 2-3 | Submessage Length | 2字节 | 子消息长度(不含头部) |
“关键点:submessageLength是2字节,最大64KB。如果要发大于64KB的数据,需要拆成多个DATA_FRAG子消息。”
五、数据子消息(DATA)——用户数据怎么装
郭靖问:“最重要的数据子消息长啥样?”
黄蓉画了DATA子消息的结构:
┌─────────────────────────────────────────────────────────────────────┐
│ DATA子消息结构 │
├─────────────────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 子消息头(SubmessageHeader) │ │
│ │ ID=0x15 │ Flags │ Length │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 读取者实体ID(readerId,4字节) │ │
│ │ 写入者实体ID(writerId,4字节) │ │
│ │ 写入者序列号(writerSN,8字节) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 内联QoS(可选,如果Flags中Q=1) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 序列化负载(serializedPayload) │ │
│ │ ├── 表示标识符(2字节) │ │
│ │ ├── 表示选项(2字节) │ │
│ │ └── 数据内容 │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
关键字段说明:
| readerId/writerId | 标识具体的读者和写者实体 | “收件人地址” |
| writerSN | 写入者赋予这个样本的序列号 | “快递单号”,用于可靠传输 |
| inlineQoS | 内联的QoS参数(如可靠性、持久性) | “加急件标记” |
| serializedPayload | 序列化后的用户数据 | “信纸” |
六、可靠传输子消息——Heartbeat和AckNack
郭靖问:“SOME/IP如果丢包了怎么办?UDP不保证可靠。”
黄蓉:“SOME/IP要么靠TCP,要么应用层自己处理。DDS/RTPS在UDP之上实现了可靠传输机制。”
6.1 Heartbeat(心跳)——告诉对方“我有这些数据”
┌─────────────────────────────────────────────────────────────────────┐
│ Heartbeat子消息结构 │
├─────────────────────────────────────────────────────────────────────┤
│ 子消息头 │ 读者实体ID │ 写者实体ID │ 起始序列号 │ 结束序列号 │ 计数 │
└─────────────────────────────────────────────────────────────────────┘
作用:写入者告诉读取者:“我这里有序列号从firstSN到lastSN的数据,你需要哪些?”
6.2 AckNack(确认/否定确认)——告诉对方“我缺哪些”
┌─────────────────────────────────────────────────────────────────────┐
│ AckNack子消息结构 │
├─────────────────────────────────────────────────────────────────────┤
│ 子消息头 │ 读者实体ID │ 写者实体ID │ 读者SN状态 │ 计数 │ 标志 │
└─────────────────────────────────────────────────────────────────────┘
作用:读取者告诉写入者:“我已经收到了X、Y、Z,还缺A、B、C,请重发。”
6.3 可靠传输流程
写入者(Writer) 读取者(Reader)
│ │
│ ① Heartbeat(我有[1-10]这些数据) │
│─────────────────────────────────────────>│
│ │
│ │ 检查本地已收到[1,2,3,5,7]
│ │ 发现缺[4,6,8,9,10]
│ │
│ ② AckNack(缺[4,6,8,9,10]) │
│<─────────────────────────────────────────│
│ │
│ ③ Data(重发缺失的数据) │
│─────────────────────────────────────────>│
│ │
│ ④ Heartbeat(我再发一遍状态) │
│─────────────────────────────────────────>│
│ │
│ ⑤ AckNack(全收到了!) │
│<─────────────────────────────────────────│
郭靖恍然大悟:“哦~~这就像寄快递——我先告诉你‘我有10个包裹’,你检查收到几个,告诉我还缺哪几个,我再补发。”
七、DDS/RTPS vs SOME/IP 报文对比
黄蓉画了一张完整的对比表:
| 报文标识 | 03 FC 80 01(Service ID=0xFFFF时是SD) | 52 54 50 53(RTP S) |
| 头部长度 | 12字节固定 | 消息头+子消息头,可变 |
| 服务发现 | SD(Offer/Find/Subscribe) | 内置PDP/EDP自动发现 |
| 可靠传输 | 依赖UDP(不保证)或TCP | 内置Heartbeat/AckNack机制 |
| 大数据传输 | SOME/IP-TP(需要额外协议) | 内置DATA_FRAG分片 |
| QoS支持 | 无内置 | 23种QoS策略 |
| 适用场景 | 车身控制、RPC、诊断 | 自动驾驶、大数据分发 |
八、为什么先讲RTPS报文格式?
郭靖问出了关键问题:“蓉儿,为什么DDS要从RTPS报文讲起?不能先讲Topic、Publisher这些上层概念吗?”
黄蓉解释道:
“DDS和SOME/IP的学习路径不一样。SOME/IP是先有概念(Service ID、Method ID),后有报文。DDS是先有报文(RTPS),上层概念是对报文的封装。”
她画了一个学习路径对比:
SOME/IP学习路径:
概念(Service ID/Method ID)→ 报文(03 FC 80 01)→ 抓包验证
DDS学习路径:
报文(RTPS)→ 理解底层机制(发现、可靠传输、分片)→ 概念(Topic、Publisher、Subscriber)→ QoS配置
“理解了RTPS报文,你就明白了DDS为什么能自动发现、为什么能可靠传输、为什么能支持大数据分片。”
九、黄蓉的小本本
郭靖翻开她的笔记本,上面写着:
RTPS核心要点:
1. RTPS是什么:DDS的“有线协议”,让不同厂家DDS产品互操作
2. RTPS消息结构:
-
消息头:52 54 50 53(RTP S)+ 版本 + 供应商ID + 参与者前缀
-
子消息:DATA(数据)、HEARTBEAT(心跳)、ACKNACK(确认)、GAP(间隙)等
3. 与SOME/IP的识别方式:
-
SOME/IP:看03 FC 80 01(DoIP头部)
-
DDS/RTPS:看52 54 50 53(RTPS标识)
4. 可靠传输机制:Heartbeat告知数据范围,AckNack告知缺失,Writer补发
5. 一句口诀:RTP S开头是DDS,03FC是SOME/IP,抓包先看头字节,谁家孩子谁来认
写在最后
郭靖合上笔记本:“RTPS是DDS的底层有线协议,消息头以52 54 50 53(RTP S)开头。里面可以包含DATA(数据)、HEARTBEAT(心跳)、ACKNACK(确认)等子消息。可靠传输靠Heartbeat和AckNack配合完成。”
黄蓉咬了口糖葫芦:“报文格式讲完了。那Topic、Publisher、Subscriber这些上层概念,你现在有基础了,能理解它们是怎么在RTPS报文里体现的吗?”
郭靖若有所思:“DATA子消息里装的serializedPayload,就是Topic的数据?”
黄蓉点头:“对!下篇就讲——Topic怎么定义、用IDL描述、和RTPS报文怎么对应。”
打完收工,886。