RTPS message module

引子:从 Wireshark 的 RTPS 解析树说起

RTPS 是 DDS 的互操作线协议(wire protocol),不是 DDS API 本身。DDS 规定了发布/订阅的模型和 QoS,RTPS 规定了这些语义如何落到 UDP/TCP 的字节流上。也正因为如此,ROS 2 里不同 DDS 实现(Fast DDS、Cyclone DDS、Connext 等)才能在同一网段里互相发现、互相收发。

在 Wireshark 里抓一条 ROS 2 或 DDS 的流量,你大概率会看到这样一棵解析树:

RTPS
  INFO_TS
  DATA

展开后是一串十六进制字节。它们看起来像天书,但结构其实非常规整:一条 RTPS 消息就是 一个固定 20 字节的消息头,后面跟着若干条子消息。子消息各管一件事:有的承载用户数据,有的确认收包,有的请求重传,有的只是给后续子消息设置时间戳或目标地址。

本文按 Header、SubmessageHeader、13 种子消息的顺序逐个拆解报文结构。读完你应该能独立看懂 Wireshark 里一条 RTPS 报文的字段布局。

一条 RTPS 消息长什么样

RTPS 消息的结构可以用一句话概括:

Message = Header(20 B)+ Submessage × N

每个 Submessage 又由 4 字节的 SubmessageHeader + 可变长度的 body 组成。所有子消息都相对消息起点按 32 位(4 字节)边界对齐,必要时用 PAD 子消息填充。RTPS 协议本身不携带整条消息的总长度:走 UDP 时,UDP payload 的长度就是消息长度;走 TCP 时,需要额外的长度前缀来界定消息边界。

图 1:RTPS 消息由 20 字节 Header 和若干子消息组成

解析一条 RTPS 消息的过程,本质上是一个循环:

  1. 先读固定的 20 字节 Header;
  2. 读 4 字节 SubmessageHeader;
  3. 从 octetsToNextHeader 得到这条子消息内容的长度;
  4. 解完当前子消息,跳到下一条的头部,直到消息结束。

octetsToNextHeader 是这套格式的关键。它让接收者可以跳过不认识的子消息,也让 RTPS 能在不破坏兼容性的前提下扩展新类型。32 位对齐也不是形式主义:它让多字节字段落在自然的边界上,接收者可以用固定步长读取,实现也更容易把字节流映射成结构体(端序一致时)。

Header(20 字节)

Header 固定 20 字节,字段很少,但信息量不小。

图 2:Header 字段布局

偏移 长度 字段 含义
0x00 4 B protocol 固定 ASCII "RTPS",即 52 54 50 53
0x04 2 B version 协议版本,major/minor 各 1 字节,如 02 03 表示 2.3
0x06 2 B vendorId 中间件厂商 ID,如 RTI 01 01、eProsima 01 0F
0x08 12 B guidPrefix 该 Participant 的 GUID 前缀

guidPrefix 是 Header 里最值得展开的字段。RTPS 里的全局唯一标识叫 GUID,由两部分组成:

GUID (16 B) = guidPrefix (12 B) + EntityId (4 B)
EntityId (4 B) = entityKey (3 B) + entityKind (1 B)

guidPrefix 标识一个 DomainParticipant,同一个 Participant 内的所有 Writer、Reader 共享它;EntityId 则区分 Participant 内部的具体实体。entityKind 的高 2 位表示实体类别(11 内置、00 用户自定义、01 厂商自定义),低 6 位表示实体类型。几个常见的内置 EntityId:

  • Participant:00 00 01 C1
  • SPDP writer:00 01 00 C2
  • SEDP publications writer:00 00 03 C2
  • 用户自定义、带 key 的 writer:kind 为 0x02,例如 00 00 01 02

规范还要求 guidPrefix 的前 2 字节等于 vendorId,这样即使同一个 DDS Domain 里混用多家实现,GUID 也不会冲突。

把 GUID 拆成”Participant 前缀 + 实体内标识”的好处是省带宽:一条消息的 Header 只需要带一次 12 字节的 guidPrefix,后续子消息里的 readerId/writerId 都只写 4 字节 EntityId。接收者用 Header(或 INFO_SRC)里的前缀把 EntityId 还原成完整 GUID。

Header 里的 guidPrefix 只是”默认源”。后续子消息可以用 INFO_SRC 改写逻辑源,用 INFO_DST 改写目标 Participant——这也是 RTPS 子消息具有上下文依赖的原因。

SubmessageHeader(4 字节)

每条子消息都以 4 字节开头:

图 3:SubmessageHeader 与 flags 位结构

偏移 长度 字段 含义
+0 1 B submessageId 子消息类型
+1 1 B flags bit0 是端序位 E,其余位按类型解释
+2 2 B octetsToNextHeader 从本子消息内容开始(4 字节头之后)到下一子消息头部的字节数

ID 分区:0x00–0x7F 是协议定义范围;其中 0x30–0x3F 被 DDS-Security 保留;0x80–0xFF 是厂商自定义范围。协议范围之外、或者来自未知厂商的 ID,接收者必须用 octetsToNextHeader 跳过,并继续解析后面的子消息。

这种”ID + 长度”的自描述设计,是 RTPS 可扩展性的基础:新版本增加子消息类型时,旧实现只要按长度跳过即可,不需要重新协商协议版本;厂商也可以在自己的 ID 区间里做私有扩展,而不会和标准子消息冲突。

flags:bit0 永远是 E(EndiannessFlag)。E=0 表示该子消息用大端编码,E=1 表示小端。注意这是”每条子消息各自决定”的,不是整条消息统一。其余位按类型解释:

  • DATA:Q(inline QoS)、D(data)、K(key)、N(非标准 payload)
  • DATA_FRAG:Q、K、N——注意 K/N 的位置和 DATA 不同
  • HEARTBEAT:F(final)、L(liveliness)
  • ACKNACK:F
  • INFO_TS:I(invalidate)
  • INFO_REPLY / INFO_REPLY_IP4:M(multicast)

发送者应把未定义位置 0,接收者应忽略不认识的位——这是 RTPS 向前兼容的一部分。

octetsToNextHeader 有三种情况:

  1. 大于 0:从本子消息内容开始,到下一子消息头部开始的字节数;
  2. 等于 0:本子消息是消息的最后一条,内容一直延伸到消息末尾。这让单条子消息可以超过 64 KB;
  3. 等于 0 且类型是 PAD 或 INFO_TS:下一条子消息头紧跟在当前 4 字节头之后,或者当前子消息就是最后一条。

标准子消息总览

ID 子消息 方向 用途
0x01 PAD 任意 对齐填充
0x06 ACKNACK Reader → Writer 确认收到 / 请求重传
0x07 HEARTBEAT Writer → Reader 宣告可用序号范围
0x08 GAP Writer → Reader 宣告某段序号不可用
0x09 INFO_TS 任意 为后续子消息设置时间戳
0x0C INFO_SRC 任意 修改后续子消息的逻辑源
0x0D INFO_REPLY_IP4 Reader → Writer 紧凑的 IPv4 回复地址
0x0E INFO_DST 任意 修改后续子消息的目标 Participant
0x0F INFO_REPLY Reader → Writer 回复地址列表
0x12 NACK_FRAG Reader → Writer 请求分片重传
0x13 HEARTBEAT_FRAG Writer → Reader 分片可用范围
0x15 DATA Writer → Reader 数据或生命周期变更
0x16 DATA_FRAG Writer → Reader 分片数据

按职责可以分成四类:

  • 数据面:DATA、DATA_FRAG、GAP
  • 可靠性:HEARTBEAT、ACKNACK、HEARTBEAT_FRAG、NACK_FRAG
  • 上下文:INFO_TS、INFO_SRC、INFO_DST、INFO_REPLY、INFO_REPLY_IP4
  • 辅助:PAD

Header 初始化接收者上下文,INFO_* 修改上下文;其余子消息只影响对应的端点状态,不改变后续子消息的解释方式。

一个常见坑:网上有些旧资料把 ACKNACK 写成 0x02、HEARTBEAT 写成 0x03。在 RTPS 2.x 里它们是 0x06 和 0x07;0x02/0x03 并不对应这两个子消息。以规范枚举为准。

子消息逐类介绍

从这一节开始,每一种标准子消息单独成节。每节的顺序统一为:

  1. 先放报文结构图:每行 4 个字节,图中只保留偏移、字段边界和字段名,不写字段说明;
  2. 再给字段列表:列出每个字段的名称、偏移、长度和简短说明;
  3. 最后解释语义:说明标志位、方向和典型用法。

DATA(0x15)

图 4:报文结构示意图

  • ID(0x00,1 B):子消息类型,DATA 为 0x15。
  • flags(0x01,1 B):bit0 E;bit1 Q;bit2 D;bit3 K;bit4 N。
  • length(0x02,2 B):octetsToNextHeader,即本子消息内容的长度。
  • extraFlags(0x04,2 B):额外标志位,本版本置 0。
  • octetsToInlineQos(0x06,2 B):到 inline QoS 的字节偏移;无 inline QoS 时指向 payload 起点。
  • readerId(0x08,4 B):目标 Reader;全 0 表示所有 Reader。
  • writerId(0x0C,4 B):源 Writer 的 EntityId。
  • writerSN(0x10,8 B):64 位序号,从 1 开始。
  • inlineQoS(0x18,0 B,可选):Q=1 时存在。
  • serializedPayload(0x18,20 B):序列化 payload,前 4 字节是表示头。

DATA 由 Writer 发给 Reader,承载一次数据变更。writerSN 编码时拆成两个 32 位字;readerId=0 表示目标 Participant 内该 Writer 的所有 Reader。flags 的 Q/D/K/N 组合决定 payload 的含义:D=1,K=0 是数据,D=0,K=1 是 key,D=0,K=0 不带 payload,D=1,K=1 在 RTPS 2.3 中是非法组合。

DATA_FRAG(0x16)

图 5:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x16。
  • flags(0x01,1 B):bit0 E;bit1 Q;bit2 K;bit3 N。
  • length(0x02,2 B):octetsToNextHeader,即本子消息内容的长度。
  • extraFlags(0x04,2 B):额外标志位,置 0。
  • octetsToInlineQos(0x06,2 B):到 inline QoS 的字节偏移;无 inline QoS 时指向 payload 起点。
  • readerId(0x08,4 B):目标 Reader。
  • writerId(0x0C,4 B):源 Writer。
  • writerSN(0x10,8 B):所属数据的序号。
  • fragmentStartingNum(0x18,4 B):起始分片号,从 1 开始。
  • fragmentsInSubmessage(0x1C,2 B):本子消息包含的分片数。
  • fragmentSize(0x1E,2 B):每个分片的大小。
  • sampleSize(0x20,4 B):原始数据总大小。
  • serializedPayload(0x24,8 B):本分片数据。

DATA_FRAG 把 DATA 的 payload 拆成分片;sampleSize 是原始数据总长,fragmentsInSubmessage × fragmentSize 应覆盖本次携带的分片数据。注意 DATA_FRAG 的 K/N 位在 bit2/bit3,而 DATA 的 D/K/N 在 bit2/bit3/bit4,解析时不能直接套用。

GAP(0x08)

图 6:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x08。
  • flags(0x01,1 B):E 端序位。
  • length(0x02,2 B):octetsToNextHeader,即本子消息内容的长度。
  • readerId(0x04,4 B):目标 Reader。
  • writerId(0x08,4 B):源 Writer。
  • gapStart(0x0C,8 B):不可用区间的起始序号。
  • bitmapBase(0x14,8 B):gapList 的起始序号。
  • numBits(0x1C,4 B):gapList 的有效位数;0 表示只有连续区间。

GAP 告诉 Reader“某段序号不用再等”:gapStart 到 gapList.base - 1 之间的序号被视为不可用;gapList 可以再列出额外的不可用序号。GAP 不携带用户数据。

HEARTBEAT(0x07)

图 7:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x07。
  • flags(0x01,1 B):bit0 E;bit1 F;bit2 L。
  • length(0x02,2 B):octetsToNextHeader,即本子消息内容的长度。
  • readerId(0x04,4 B):目标 Reader。
  • writerId(0x08,4 B):源 Writer。
  • firstSN(0x0C,8 B):可用范围的起始序号。
  • lastSN(0x14,8 B):可用范围的结束序号。
  • count(0x1C,4 B):发送计数器,用于识别重复。

HEARTBEAT 由 Writer 发出,宣告 firstSN..lastSN 范围内的序号可用。F=0 表示 Reader 必须回一个 ACKNACK,F=1 表示不需要;L=1 用于刷新手动 liveliness;count 每发一次递增。

ACKNACK(0x06)

图 8:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x06。
  • flags(0x01,1 B):bit0 E;bit1 F。
  • length(0x02,2 B):octetsToNextHeader,即本子消息内容的长度。
  • readerId(0x04,4 B):发起确认的 Reader。
  • writerId(0x08,4 B):被确认的 Writer。
  • bitmapBase(0x0C,8 B):位图的起始序号。
  • numBits(0x14,4 B):有效位数。
  • bitmap[0](0x18,4 B):第一个位图字,MSB 对应 bitmapBase。
  • count(0x1C,4 B):发送计数器。

ACKNACK 由 Reader 发出,用 SequenceNumberSet 告诉 Writer 缺哪些序号。F=0 表示 Writer 必须回一个 HEARTBEAT;count 用于识别重复。位图是 MSB-first:最高位对应 bitmapBase,某位为 1 表示该序号缺失。

HEARTBEAT_FRAG(0x13)

图 9:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x13。
  • flags(0x01,1 B):E 端序位。
  • length(0x02,2 B):octetsToNextHeader,即本子消息内容的长度。
  • readerId(0x04,4 B):目标 Reader。
  • writerId(0x08,4 B):源 Writer。
  • writerSN(0x0C,8 B):所属数据的序号。
  • lastFragmentNum(0x14,4 B):可用分片的上界。
  • count(0x18,4 B):发送计数器。

HEARTBEAT_FRAG 针对单个 writerSN 通告分片可用范围:lastFragmentNum 表示 1..N 号分片可用。Reader 如果缺分片,用 NACK_FRAG 请求重传。

NACK_FRAG(0x12)

图 10:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x12。
  • flags(0x01,1 B):E 端序位。
  • length(0x02,2 B):octetsToNextHeader,即本子消息内容的长度。
  • readerId(0x04,4 B):发起请求的 Reader。
  • writerId(0x08,4 B):被请求的 Writer。
  • writerSN(0x0C,8 B):所属数据的序号。
  • bitmapBase(0x14,4 B):分片位图的起始分片号。
  • numBits(0x18,4 B):有效位数。
  • bitmap[0](0x1C,4 B):第一个位图字。
  • count(0x20,4 B):发送计数器。

NACK_FRAG 是分片级的 ACKNACK:FragmentNumberSet 由 bitmapBase(4) + numBits(4) + bitmap(4×M) 组成,位图含义与序号位图相同,只是对象换成分片号。

INFO_TS(0x09)

图 11:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x09。
  • flags(0x01,1 B):bit0 E;bit1 I。
  • length(0x02,2 B):带 timestamp 时为 8;I=1 时为 0。
  • seconds(0x04,4 B):秒数,int32。
  • fraction(0x08,4 B):秒的小数部分,单位 2^-32 秒。

INFO_TS 给后续子消息设置时间戳。I=1 时 timestamp 被省略,子消息只有 4 字节头,表示后续子消息没有有效时间戳。

INFO_DST(0x0E)

图 12:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x0E。
  • flags(0x01,1 B):E 端序位。
  • length(0x02,2 B):octetsToNextHeader,即本子消息内容的长度。
  • guidPrefix(0x04,12 B):新的目标 Participant 前缀。

INFO_DST 只带一个 12 字节 guidPrefix,用于修改后续子消息的目标 Participant。它不承载用户数据,属于上下文类子消息。

INFO_SRC(0x0C)

图 13:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x0C。
  • flags(0x01,1 B):E 端序位。
  • length(0x02,2 B):octetsToNextHeader,即本子消息内容的长度。
  • unused(0x04,4 B):保留字段,必须为 0。
  • version(0x08,2 B):协议版本。
  • vendorId(0x0A,2 B):厂商 ID。
  • guidPrefix(0x0C,12 B):新的源 Participant 前缀。

INFO_SRC 修改后续子消息的逻辑源;收到后,接收者用新的 guidPrefix 还原后续子消息里的 EntityId。

INFO_REPLY(0x0F)

图 14:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x0F。
  • flags(0x01,1 B):bit0 E;bit1 M。
  • length(0x02,2 B):octetsToNextHeader,即本子消息内容的长度。
  • numLocators(0x04,4 B):单播 locator 数量。
  • kind(0x08,4 B):locator 类型,UDPv4 为 1。
  • port(0x0C,4 B):端口。
  • address(0x10,16 B):地址,IPv4 只占前 4 字节。
  • multicast LocatorList:可选,M=1 时追加。

INFO_REPLY 告诉 Writer 回复时应该发到哪里。LocatorList = numLocators(4) + N × Locator_t,每个 Locator_t = kind(4) + port(4) + address(16)。

INFO_REPLY_IP4(0x0D)

图 15:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x0D。
  • flags(0x01,1 B):bit0 E;bit1 M。
  • length(0x02,2 B):octetsToNextHeader,即本子消息内容的长度。
  • address(0x04,4 B):IPv4 地址。
  • port(0x08,4 B):端口。
  • multicast address/port:可选,M=1 时追加。

INFO_REPLY_IP4 是 INFO_REPLY 的 UDP/IPv4 紧凑版本,语义与 INFO_REPLY 中的单个 UDPv4 locator 等价。

PAD(0x01)

图 16:报文结构示意图

  • ID(0x00,1 B):子消息类型,0x01。
  • flags(0x01,1 B):E 端序位。
  • length(0x02,2 B):填充字节数。
  • padding(0x04,N B):填充内容,接收者忽略。

PAD 用于让下一条子消息从 32 位边界开始。octetsToNextHeader=0 时有一个特殊规则:下一条子消息头紧跟在当前 4 字节头之后,或者当前 PAD 就是最后一条。

Qos

user_data,字符串类型,用于传递一些信息(比如网络绑定的 service name 等),用于 domain,participant,datareader 和 datawriter

topic_data,字符串类型,用于传递一些信息,用于 topic

group_data,字符串类型,用于传递一些信息,用于 publisher 和 subscriber

durability,枚举类型,表示是否需要对收到的 change 进行 cache,可选值有 volatile(不 cache),transient_local,transient(在内存 cache) 和 persistent(持久化存储),用于 topic,datareader 和 datawriter

Deadline,数值类型,表示 endpoint 预期在一定的时间内需要收到对端的消息,如果没有收到,就会调用 onrequesteddeadlinemissed(周期性)

latency_budget,数值类型,表示 writer 的消息,到 reader,允许的最大时延(如果超过了,会怎样,规范没有说)

Ownership,表示是否允许多个 writer 写入同一个 topic,同时多个 writer 有优先级的区分

liveliness,保活机制,表示是在哪些范畴之间的

参考资料


 上一篇
FastDds 库使用及详解 FastDds 库使用及详解
Recv message消息流 类之间的交互
2026-01-06
下一篇 
Spdlog 库使用及详解 Spdlog 库使用及详解
前置知识 函数 fwrite 是 glibc 封装的一层有关 write 的接口,会在应用层再创建一个缓冲的 buffer,当调用的 fwrite 到一定的数量时,才会调用一次操作系统原生的 write。也因此有了函数 fflush,将这个
2025-08-21
  目录