DDS 通信中间件剖析:RMW · RTPS · 自动发现
你已经会写 Publisher/Subscriber 了,但 "没写一行连接代码,两个节点为什么就能通?"——答案藏在 ROS2 的底座 DDS 里。本实验拆开这层黑盒:RMW 抽象层如何让 DDS 实现可热插拔,RTPS 协议的 Participant→Endpoint 域模型长什么样,SPDP+SEDP 两阶段自动发现如何让节点"开机即互通"。
目标本课学完,你应该能做到
能阐述 RMW 抽象层的设计原理——上层 rcl API 不改一行就能切换底层 DDS 实现,并说清 RTPS 中 Participant、Endpoint、Writer/Reader 的层次关系。
能正确执行切换 DDS 实现(RMW_IMPLEMENTATION 环境变量)、配置 QoS 策略,观察 DDS 两阶段发现(SPDP + SEDP)。
独立编写使用非默认 QoS 的收发节点,对比 RELIABLE vs BEST_EFFORT、KEEP_LAST(1) vs KEEP_LAST(100) 的行为差异。
能为不同话题选合理 QoS:激光雷达允许丢帧但要低延迟;位姿估计不可丢帧但能忍较高延迟——说出取舍依据。
难点:四层域模型(Domain → Participant → Publisher/Subscriber → DataWriter/DataReader)容易记混,尤其"节点 ≠ Participant"。
01问题引入:没写一行连接代码,两个节点为什么就能通
要认识这套神经网络,先回答两个问题:它是什么?为什么是它?——能实现通信的模型和软件框架非常多,把常用的四种摆在一起看,DDS 的优势就清楚了:
02先看现象:零连接代码的 0→1
两个终端、四步、约 2 分钟:先看"启动即通",再当场拿到 DDS 底座的端口与隔离证据。
不写任何"连接代码",启动即开始发布:
ros2 run demo_nodes_cpp talker
话题还没人订阅,底层却发现端口已开——这就是"没写连接代码也能通"的物理入口:
ss -unap | grep -E "7400|7411"
回到下方 04 节的"互动 12-1"沙盘,连点 4 次推进一步:① 领 GUID → ② SPDP 进花名册 → ③ SEDP 交端点 → ④ 绿线亮起撮合成功——第 ② 步人已认识但无数据,第 ④ 步才开始送达。
域 7 的列表里消失的 /chatter,就是"不同域互相看不见"的 0→1 证据:
ROS_DOMAIN_ID=7 ros2 topic list
✔ 四步走完:启动即通、端口可见、后台四步建链、换域即隔离——接下来拆开这些现象背后的原理。
03原理讲解
先看楼层表再进机房——EXP-01 的五层架构原图,本课聚焦被高亮的第 3 层(RMW 插座)与第 4 层(DDS 实现):
RMW——DDS 的"万能适配器"
对照上图:你的代码只碰上面两层,真正收发字节的是第 4 层——夹在中间的第 3 层 RMW 就是"插座":一份接口说明书,各家 DDS 按它做成共享库,运行时一个环境变量插拔。
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp 一行切换底层实现,代码零改动、无需重编译。课堂若离线,可课前用 apt-get download ros-humble-rmw-cyclonedds-cpp ros-humble-cyclonedds 取包,课上 sudo dpkg -i ./*.deb 安装。ROS_DOMAIN_ID 下必须使用同一种 RMW 实现——不同实现说的话彼此听不懂。export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp(只需设在启动节点的环境里);验证读法 echo $RMW_IMPLEMENTATION 为空即默认 Fast-DDS,ros2 doctor --report | grep -i middleware 可确认当前生效实现。注意:A、B 两组实操不切换实现、不需要联网——全部用默认 Fast-DDS + ros2cli 自带命令即可完成。切换 RMW 属于"知道有这回事"的扩展认知,不作为本课动手项。
RTPS 协议的四层嵌套
RTPS 是 DDS 在网线上跑的"方言"。下图四层套娃:EXP-06 配的 QoS 就贴在最里层"快递员"身上:
ROS_DOMAIN_ID 就是楼号——只有同楼的人才能通信,避免无用数据占带宽。
KEEP_LAST(N) 只留最近 N 件;KEEP_ALL 全留。晚来的订阅者能不能拿到"历史包裹",就看这张货架。
ros2 node list 数出来的是节点——同一进程里的多个节点通常共用一个 Participant(一家公司可以开好几个部门)。所以:节点数 ≠ 公司数。真正带工号的是
ros2 topic info --verbose 打出的 "GID",那是 DataWriter 的 GUID(快递员工号)。你在命令行看到的一切,都能在这套"楼→公司→部门→快递员"里找到位置。一条消息如何"穿过四层"到达对端
你只写了 publish(msg) 一行,剩下全靠下图八步自动跑完:
为什么不用写"连接代码"
答案全在下图两阶段协议里——零配置自动跑,怎么设:什么都不用设:
239.255.0.1 周期发送"我来了"(带上自己的 GUID 和楼号 Domain)。收到的人把彼此记进花名册。这一步用 Best Effort——丢一两条没关系,下个周期还会再喊。| 阶段 | 解决什么 | 生活类比 |
|---|---|---|
| SPDP | 网络里有谁、在哪个 Domain(楼) | 进工作群先看"群成员列表" |
| SEDP | 每个人发/收什么话题、QoS 是否匹配 | 群里发公告对暗号,对上了才私下加好友 |
QoS 决策表:按数据特性选策略
QoS 是收发双方的"合约"——先看三推子档位图,再对照下表按数据特性直接选,场景列即取舍依据:
BEST_EFFORT:丢了不补,换取更低延迟——激光雷达点云、图像流常用。
TRANSIENT_LOCAL:为话题保留"最后一条",晚到的订阅者一上线就补发。
KEEP_ALL:一条不留地全排队——可能撑爆内存,慎用。
| QoS 策略 | 可选值 | 语义 | 典型场景 |
|---|---|---|---|
| Deadline | DEADLINE(时长) | 每次通信必须期限内完成 | 200Hz IMU → 设 5ms:超期即报"断流",第一时间发现节点卡死 |
| History | KEEP_LAST(n) / KEEP_ALL | 历史数据的缓存大小 | 状态量 → KEEP_LAST(1);录制回放 → KEEP_LAST(1000) |
| Reliability | RELIABLE / BEST_EFFORT | 可信赖模式 vs 尽力传输 | RELIABLE 尽量保证完整性(重传)→ 控制指令;BEST_EFFORT 网络差也保流畅、可能丢 → 激光点云/图像 |
| Durability | VOLATILE / TRANSIENT_LOCAL | 晚加入的节点收不收历史 | 实时流 → VOLATILE;地图、机器人状态 → TRANSIENT_LOCAL(新节点快速跟上系统) |
| Liveliness | AUTOMATIC / MANUAL_BY_TOPIC | 谁负责证明"我还活着" | 安全关键节点 → MANUAL(代码显式心跳) |
# 查看任一话题当前生效的完整 QoS 档案 ros2 topic info /chatter --verbose # 输出含:Reliability: RELIABLE / Durability: VOLATILE / GID: 01.0f... # 你的 QoS 代码里对应的声明(EXP-06 复习) qos = QoSProfile(history=HistoryPolicy.KEEP_LAST, depth=10, reliability=ReliabilityPolicy.RELIABLE)
04实验步骤
工程化顺序:先看全景(图 12-3)→ 再进 A/B 两组实操 → 最后读真实 QoS 清单。
亲手推进两阶段发现
点"推进一步",按真实时序看 DDS 怎么在后台把三个节点连起来——你一行连接代码都不用写:
组播地址
互动 12-1|亲手推进 SPDP(找人)→ SEDP(找端点)→ 撮合(建链):注意第 ② 步只是"人被发现",还没有数据流动
ros2 topic list 秒级可见,但 echo 出数据还要再等 SEDP 撮合完成。只有走到 ④,绿色连线亮起,消息才真正开始送达。域隔离——看得见的功能 + 摸得着的底层证据
本节全部命令零安装、纯 ros2cli,学生全程能自己敲出效果。先建立"域隔离"的直觉,再从 ss 端口号和共享内存文件名拿到底层证据链。
A-1 · 域号 = UDP 端口偏移(最推荐,证据最硬)
ROS_DOMAIN_ID 直接映射到 DDS 发现端口 7400 + 250 × 域号——域号不是"逻辑标签",它物理上决定了节点监听哪个端口。这里只用前缀式临时给域号做对照实验;域号的三种给法、组网与防火墙排查的完整主场在 EXP-13 分布式通讯,本课只取它的"端口证据"。
不设任何环境变量 → 落在默认域 0:
ros2 run demo_nodes_cpp talker
域 0 的发现端口应为 7400(组播发现)与 7411(单播发现):
ss -unap | grep -E "7400|7411"
域 7 的预期端口 = 7400 + 250×7 = 9150;同时 ros2 topic list 看不到域 0 的 /chatter:
ROS_DOMAIN_ID=7 ros2 run demo_nodes_cpp talker ss -unap | grep -E "9150|9161" ROS_DOMAIN_ID=7 ros2 topic list
✔ 功能现象(不同域节点绝对互不可见)+ 底层证据(端口号恰好 7400 + 250×7 = 9150)同时到手,这条最有教学价值。
A-2 · 组播发现通道本身(ros2 multicast)
发现报文独立于话题数据,走的是 UDP 组播。ros2 multicast 是 ros2cli 自带命令,可直接抓到这个通道:
ros2 multicast receive
ros2 multicast send
排查顺序:先
ping 通(IP 可达)→ 再 ros2 multicast send/receive(组播可达)→ 都通了才轮到查域号与 QoS。A-3 · 共享内存传输:同机通信不走网卡
Fast-DDS 同机通信默认走共享内存(SHM),段文件立刻出现在 /dev/shm:
ros2 run demo_nodes_cpp talker & ls -l /dev/shm | grep fast df -h /dev/shm
✔ 亮点是 fastrtps_port7411 这个文件名——学生能直接读出"同机通信走共享内存,不走网卡",端口号与 A-1 的 7411 严丝合缝对上。
kill -9 强杀后,/dev/shm 里的段文件可能残留,导致下次启动报错或行为异常。清理命令:fastdds shm clean(或手动删掉残留的 fastrtps_* 文件)。优雅退出(Ctrl+C)不会留残留——这也是为什么"杀进程"和"正常停"要用不同手段。A-4 · 实操验收
| # | 证据项 | 记录方式 | 合格标准 |
|---|---|---|---|
| 1 | 域号 → 端口映射 | ss -unap 截图 | 域 0 = 7400/7411,域 7 = 9150/9161,能说出 +250×7 的规律 |
| 2 | 域隔离功能现象 | ROS_DOMAIN_ID=7 ros2 topic list 截图 | 看不到域 0 的 /chatter |
| 3 | 组播通道存活 | ros2 multicast 收发截图 | 能收到对端 Hello World 与来源 IP:端口 |
| 4 | 共享内存证据 | ls -l /dev/shm 截图 | 指出 fastrtps_port7411 与发现端口的对应关系 |
QoS 实验——DDS 最直观的两组反差
QoS 最好的教学方式是让现象自己说话。下面两组实验都只用 CLI,反差强烈、结论确定,建议作为本节主力。动手前先看三门判定——改错任何一门都会"连不上":
B-1 · Durability:后加入的订阅者能拿到历史数据
这就是 ROS 1 时代的 latching,在 DDS 里叫 TRANSIENT_LOCAL durability。发布者先跑,订阅者后启动,照样立刻收到积压的消息:
ros2 topic pub -r 2 --qos-durability transient_local --qos-depth 5 \ /qos_tl std_msgs/msg/String "{data: retained}"
ros2 topic echo --qos-durability transient_local /qos_tl
把两边都换成 volatile(默认)再试 → 一条都收不到。两个命令的差别只有 durability 一个参数,结果天差地别。
B-2 · Reliability 不兼容 → 逻辑上根本没连上
ros2 topic pub -r 5 --qos-reliability best_effort /qos_be \ std_msgs/msg/String "{data: be}"
ros2 topic echo --qos-reliability reliable /qos_be ros2 topic info -v /qos_be
B-3 · 顺带读一份真实 QoS 清单
ros2 topic info -v 会打印每个端点的完整 QoS——这份清单本身就是 DDS 的 QoS 策略表:
ros2 topic info -v /qos_tl
ros2 topic echo /scan 大概率什么都收不到——激光驱动用 BEST_EFFORT 发布,而 echo 默认 RELIABLE,QoS 不匹配。必须加 --qos-profile sensor_data(BEST_EFFORT + KEEP_LAST(5))或 --qos-reliability best_effort。RViz2 看 LaserScan 也要设 Best Effort,这是新手最常卡的坑之一。| # | 证据项 | 记录方式 | 合格标准 |
|---|---|---|---|
| 1 | TRANSIENT_LOCAL 收历史 | 后启动 echo 的截图 | 立刻收到多条积压消息 |
| 2 | VOLATILE 对照 | 同实验换 volatile 的截图 | 一条都收不到,能说出原因 |
| 3 | RELIABILITY 不兼容 | WARN + topic info -v 截图 | 指出 Subscription count: 0 的含义 |
| 4 | QoS 清单解读 | topic info -v 输出 | 逐项说出六个策略的实际取值 |
05注意事项与验收清单
| ⚠️ 注意项 | 说明 |
|---|---|
| 全程零安装 | 本节实操全部使用 ros2cli 自带命令(ss / ros2 multicast / ls),不需要联网装任何 DDS 实现 |
| 域号范围 | ROS_DOMAIN_ID 取值 0~232;端口映射公式 7400 + 250 × 域号,域号过大可能超出系统端口范围 |
| 组播依赖 | SPDP 依赖 UDP 组播——虚拟机/容器关闭组播、交换机禁 IGMP 时节点互相看不见;用 ros2 multicast send/receive 先验证通道 |
| 共享内存残留 | 进程被 kill -9 后 /dev/shm 可能留下 fastrtps_* 段文件导致下次启动异常,清理用 fastdds shm clean |
| QoS 不匹配无声 | 不兼容时表现为"没有任何反应、没有报错",只有 topic info -v 的 Subscription count: 0 能佐证 |
| Durability 三态 | VOLATILE / TRANSIENT_LOCAL / TRANSIENT,第三种需 DDS 持久化存储支持,课程默认前两者 |
- ss -unap | grep -E "7400|7411" 能指出这是域 0 的发现端口,并算出域 7 对应 9150/9161
- ROS_DOMAIN_ID=7 ros2 topic list 看不到域 0 的
/chatter——域隔离功能现象成立 - ros2 multicast receive / send 能收到对端消息与来源 IP:端口,说清发现报文走 UDP 组播
- ls -l /dev/shm | grep fast 能认出
fastrtps_port7411,解释同机通信走共享内存 - TRANSIENT_LOCAL 订阅端晚启动仍能收到 5 条历史消息,换 VOLATILE 一条都收不到
- topic info -v 看到 Subscription count: 0 时能解释"不兼容 = 连接未建立"而非"收到空数据"
- 能对照解释 SPDP(找人)与 SEDP(找端点)各自职责
- 能为"激光点云"与"导航指令"分别推荐合适的 QoS 组合并说明理由
06本课总结与验证题
ss 端口号和 /dev/shm 文件名拿到底层证据;跑通 durability 与 reliability 两组 QoS 反差实验。ss 验证端口偏移、ros2 multicast 验证组播通道、ls /dev/shm 验证共享内存;能读懂 topic info -v 的六项 QoS。multicast 通组播 → 同 Domain → QoS 请求 ≤ 提供。最后才怀疑 RMW 实现是否统一。ROS_DOMAIN_ID=12:组播地址、发现端口各变成多少?如果只改了一台,现象是什么?
参考答案与得分点
得分点:①组播地址 = 239.255.0.(N+1) → 239.255.0.13 ②端口 = 7400 + 250×12 → 10400(单播 10411) ③只改一台 → 两机不同域,互相不可见(域 12 的 topic list 无对端话题)。(①②各 40%,③ 20%)--qos-durability 都改回默认 volatile,其它参数不动。晚启动 3 秒的订阅者还能立刻收到积压消息吗?用哪条命令留下证据?
参考答案与得分点
得分点:①收不到——VOLATILE 不为晚到者保留历史 ②证据:ros2 topic info -v /qos_tl 显示 Durability: VOLATILE,且 echo 长时间无输出 ③对照:TRANSIENT_LOCAL 下同条件可收 5 条。(①③各 40%,② 20%)
reliable、订阅端 best_effort。这次能收到消息吗?Subscription count 是多少?
参考答案与得分点
得分点:①能收到——兼容规则是订阅请求 ≤ 发布提供,RELIABLE(提供)≥ BEST_EFFORT(请求)成立 ②count: 1,撮合成功 ③原 B-2 方向相反(请求 RELIABLE > 提供 BEST_EFFORT)才拒绝。(①②各 40%,③ 20%)export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp 再跑本课命令:域号→端口公式还成立吗?A-3 的 /dev/shm 证据还会出现吗?要守住哪条红线?
参考答案与得分点
得分点:①7400 + 250×域号 是 RTPS 标准端口映射,换实现仍成立 ②fastrtps_* 段文件消失——共享内存是 Fast-DDS 专有 ③红线:同域内所有进程必须用同一种 RMW,否则彼此听不懂 ④SPDP/SEDP 与 B-1/B-2 的 QoS 现象不变。(①②各 30%,③④各 20%)