1. 为什么说ROS 2的核心架构红利,一半押在中间件上
从ROS 1迁移到ROS 2的时候,很多人第一反应是“API变了”“节点模型变了”,但真正拉开代差的是底层那张通信网络。ROS 1时代,节点间的消息传递依赖一个中心化的roscore节点做名称注册和话题握手,整个系统是单点拓扑,一旦roscore挂掉,所有节点集体失联。而且ROS 1的通信协议是自研的TCPROS/UDPROS,只在ROS生态里转,外部系统想接入得专门写桥接层,非常封闭。
ROS 2之所以敢对外宣称“面向生产级机器人系统”“支持多机器人协同”“能在实时操作系统上跑”,核心筹码就是把整个通信底座替换成了DDS(Data Distribution Service,数据分发服务)。DDS是OMG组织制定的分布式实时数据分发标准,本身就服务于航空、军工、工业控制这些高可靠场景,ROS 2直接基于DDS来构建中间件层,相当于把工业级的通信骨架搬进了机器人领域。
这个选择带来三个直接结果:其一,去中心化,节点之间通过DDS的全局发现机制自动建立通信拓扑,不再有单点瓶颈;其二,天然支持QoS(Quality of Service,服务质量)策略,可靠传输、超时、历史数据保留、资源限流都可以按话题粒度配置,这在机器人场景里是刚需;其三,底层可以替换,因为ROS 2在DDS之上还包了一层RMW(ROS Middleware Interface,ROS中间件接口)抽象层,不同的DDS实现只要实现这层接口,都能被ROS 2正常调度。
很多新手容易混淆一个概念:ROS 2本身不是中间件,中间件是ROS 2与大千世界对话的那条河。也正因如此,理解ROS 2的架构,如果绕开中间件,等于看懂了汽车的仪表盘却没打开引擎盖。这篇文章不打算只停留在概念层面,我会把中间件在ROS 2中的位置、DDS的通信链路、RMW抽象层的设计逻辑、QoS的实际调参经验,以及我真实项目中踩过的坑全部梳理一遍,适合已经跑通过ROS 2基础例程、想深入了解通信架构的开发者,也适合准备在项目中做DDS选型和性能评估的架构师。
2. 从ROS 1到ROS 2:中间件架构到底发生了什么质变
想要真正理解ROS 2的中间件架构,不能只看ROS 2本身,你得先知道ROS 1时代的问题长什么样。我当年用ROS 1跑多传感器融合的时候,印象最深的就是那个roscore,每个节点启动前都得先确认master在线,几台机器人组网时还要手动管理ROS_MASTER_URI,一旦网络抖动导致master无响应,整个系统的topic就全乱了。这种架构在单机、少节点的实验室环境里够用,但放到多机协同、自动驾驶级别的要求下完全撑不住。
2.1roscore中心化模式的根深蒂固
ROS 1的通信模型非常像早期的Web应用——所有客户端都去找中心服务器要地址。节点A要发布话题/scan,它得先向master注册自己的URI;节点B要订阅/scan,也是先问master“这个话题的发布者是谁”,拿到地址后,B再和A建立直连。也就是说,master并不搬运数据,但它负责“牵线搭桥”。
这个模式的弊端不用多讲:master是瓶颈,也是故障点;节点重启后重新注册要时间;多机部署时master的地址配置极其痛苦。更麻烦的是,ROS 1在通信协议上没有做标准化的“传输层可替换”设计,TCPROS/UDPROS几乎就是ROS生态专属语言,不是ROS的节点想要订阅ROS话题,中间必须得做协议转换。
2.2 DDS进入ROS 2后改写了什么
DDS的模型本质上是**“以数据为中心”的发布订阅**,它并不依赖任何中心节点,节点之间通过一种叫做Simple Discovery Protocol的机制自动互相发现。每个DDS参与者(Participant)在网络上发送周期性宣告消息,表达自己有哪些Topic、QoS要求是什么,同时对端参与者通过匹配规则决定是否建立连接。一旦建立,数据直接在发布者和订阅者之间流动,旁观节点不参与转发。
这套思路和ROS 1最大的差异在语义层面:ROS 1里你订阅一个话题,你拿到的是“某个节点发出来的消息”;DDS里你订阅一个话题,你拿到的是“符合特定规格的数据流”,这个数据流由谁发布、有几个发布者,对订阅者来说是透明的。这给ROS 2带来了一个隐藏福利:可以多个节点发布同一个话题,订阅端会收到所有发布者的数据,这在ROS 1里虽然也能实现,但缺少原生的数据管理语义。
2.3 RMW抽象层:中间件架构里的“翻译官”
我常用一个比喻:DDS是公路,ROS 2是汽车,RMW就是汽车和公路之间的悬挂系统。没有RMW,ROS 2直接绑定某一款DDS实现,那换供应商的成本就特别高;有了RMW,ROS 2的代码层只和RMW接口交互,底下的DDS是Cyclone DDS、Fast DDS还是RTI Connext,对上层基本无感。
RMW层封装了这样几件事:创建Participant、创建Publisher/Subscription、消息序列化与反序列化、QoS策略映射、发现协议适配。ROS 2的rmw_implementation包里定义了完整的接口,各个DDS厂商或社区实现了rmw_fastrtps_cpp、rmw_cyclonedds_cpp、rmw_connextdds等适配层。
实际选型中,开发者可以在启动时通过环境变量RMW_IMPLEMENTATION来切换DDS实现,例如:
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp ros2 launch my_bot robot.launch.py同一套应用代码,换一个DDS底层,可能延迟表现、带宽占用、发现速度都不太一样。这也是我强烈建议读者在自己项目初期就做一次DDS横向测评的原因——架构没有好不好,只有适不适合你的场景。
3. 层层拆开RMW:从实体创建到消息流动的完整通路
很多人读ROS 2源码时,会被rcl、rclcpp、rmw、rmw_connextdds_cpp这些名字绕晕。其实它们的分工非常清晰:rclcpp是C++层的客户端库,负责给你提供Node、Publisher、Subscription这些好用的API;rcl是C语言的ROS客户端库,可以理解为rclcpp的底层支撑;rmw是中间件抽象层接口定义;具体的rmw_xxx_cpp才是某款DDS在ROS 2世界里的驱动。
一条消息从发布者发到订阅者,跨越的层级比多数人想象的多。我拿激光雷达点云数据来走查一遍。
3.1 发布端的完整调用链路
你调用publisher_->publish(msg)时,实际发生了一串接力:
rclcpp::Publisher::publish先把消息对象包装成rclcpp::SerializedMessage或者直接交给内存管理策略处理。- 数据传到
rcl_publish(rcl层),在这里会进行类型擦除,把具体类型转成类型支持的void*格式。 - 接着调用RMW层的
rmw_publish函数,这一步是关键:RMW需要把消息实例交给具体的DDS实现。 - 具体DDS实现(比如Fast DDS)会按照该topic配置的QoS策略,把消息通过RTPS协议封装成UDP数据报,经过网卡发出去。
很多人会问,序列化发生在哪一层?答案是在RMW层之下、DDS实现内部完成的。ROS 2的消息定义(.msg文件)会通过rosidl工具链生成对应的类型支持代码,DDS实现根据类型支持代码对消息成员做序列化。不同DDS对序列化性能的优化程度不同,这也是影响端到端延迟的一个隐藏变量。
3.2 订阅端的反序列化与回调触发
订阅端收到网络包后:
- DDS底层通过RTPS协议解析出数据,存放进队列,这个队列的深度就是QoS里的
depth参数。 - RMW层从DDS的队列里取出数据,通过类型支持代码将其转换为ROS 2的消息类型。
rcl层根据每个订阅者的回调机制,把消息交给执行器(Executor),最终触发你写的回调函数。
如果中间某个环节因为QoS匹配不上或者发现协议异常导致数据无法送达,RMW层也会向rcl层反馈对应的错误状态,研究过rcl_ret_t类型的同学应该知道,RMW返回的状态码和上层rcl层并不是一一对应的,调试时要逐层翻译。
3.3 消息序列化:从.msg到DDS类型
这里有个非常值得注意的细节:ROS 2维护了一套自己的消息类型系统(基于.msg定义),而DDS使用的是IDL(Interface Definition Language)定义类型。RMW层做的一件重要工作就是把ROS 2消息类型映射为DDS可识别的类型。
如果是rmw_fastrtps_cpp,在生成消息代码时,rosidl_typesupport_fastrtps_c会为每个.msg生成对应的类型支持代码,描述每个字段的类型、偏移量、序列化长度。序列化格式采用CDR(Common Data Representation)编码,这是DDS/RTPS协议的标准编码方式。理解这一点对排查“为什么消息传过去字段错位”这类怪异问题时非常有用——往往不是代码逻辑错,而是类型映射不一致。
3.4 内存管理和零拷贝路径
对于大消息(例如图像、点云),频繁的序列化和拷贝会造成明显的CPU开销。ROS 2为此提供了几个优化手段:
rclcpp::Publisher的PublisherOptions里可以设置intra_process_buffer相关参数,启用进程内通信零拷贝。intra-process模式下,消息对象本身可以直接通过指针在进程内传递,不再需要走序列化和网络栈。只有跨进程或跨机器通信时才真正走DDS。- 在新版本中,也可以通过
loan机制从DDS实现直接借出内存来填充消息(例如Cyclone DDS的loaned_message),从而减少一次拷贝。
这些机制在单机多传感器场景里能显著降低CPU占用,我自己的经验是,在跑检测模型的同时发布高分辨率图像话题,启用进程内零拷贝之后CPU占用能下来大约15%~20%,这个收益在嵌入式板卡上尤其明显。
4. DDS发现机制与QoS匹配:数据能否流动,不只是“订阅同一话题”那么简单
我非常喜欢拿“微信群聊”来类比DDS的发现机制。群(Domain)里的每个成员都发朋友圈广告自己的存在感,并声明自己感兴趣的话题;系统后台通过比对大家发的广告,自动撮合该拉群的拉群。不过ROS 2里比微信群多了严格的规则——每个人的“入群要求”必须写清楚,质量不匹配就拒绝进群。
4.1 Discovery阶段在网络上发生了什么
DDS的标准发现协议是SDP(Simple Discovery Protocol),分为两个层面:
- Participant Discovery:每个Participant通过预置的组播地址周期性地发送SPDP(Simple Participant Discovery Protocol)报文,宣告自己的存在。
- Endpoint Discovery:参与者在发现其他Participant后,进一步交换SEDP(Simple Endpoint Discovery Protocol)报文,内容包括此Participant下有哪些Topic、Topic的数据类型、QoS策略等。
收到SEDP报文后,本地的DDS实现会根据Topic名称、数据类型、QoS兼容性做匹配,匹配通过的Endpoint之间才会建立真正的数据传输通道。
这个过程会消耗一定时间,尤其在大型网络中,并不是所有匹配都要通过组播方式完成——引入Discovery Server后,可以通过中心节点集中管理发现信息,减少网络中的组播报文数量,这对于传感器数量多、数据密集的机器人车组来说,效果非常显著。
ROS 2的rmw_cyclonedds_cpp就支持通过XML配置文件指定Discovery Server地址。我在多台机器人协同的场景中,把默认的组播发现改为Discovery Server模式之后,节点启动后的“上线时间”从十几秒缩短到了两三秒,而且网络带宽占用明显下降,尤其是在本就拥挤的5.8GHz频段Wi-Fi环境下。
4.2 QoS兼容性匹配规则
QoS是ROS 2中间件架构里最强大、也最容易被误解的机制。很多开发者只看到QoS 0或不写QoS就默认使用,结果遇到消息丢失、时序混乱、连接失败等情况无从下手。
QoS策略维度很多,常用的有这几个:
| QoS策略 | 作用 | 常见配置 |
|---|---|---|
| Reliability | 决定消息传递是否可靠 | BEST_EFFORT / RELIABLE |
| Durability | 决定晚加入的订阅者能否收到历史数据 | VOLATILE / TRANSIENT_LOCAL |
| History / Depth | 决定队列保存多少条历史消息 | KEEP_LAST(n) / KEEP_ALL |
| Deadline | 两个消息之间的最大时间间隔 | 默认无限 |
| Liveliness | 判断节点是否“活着”的机制 | AUTOMATIC / MANUAL_BY_TOPIC |
| 资源限制 | 约束最大样本数、缓存大小 | 按场景设置 |
其中最容易出坑的是Reliability策略:发布端是BEST_EFFORT、订阅端是RELIABLE,或反过来,这两个端是无法建立连接的。实际项目中我经常看到传感器驱动发布端用BEST_EFFORT,而下游的算法节点订阅时设置RELIABLE,结果就是话题完全匹配不上,看不到任何数据。ROS 2的QoS匹配不像TCP那样会自动协商降级,不兼容就是直接断开连接。
4.3 我从项目里提炼的QoS配置经验
谈一点实操。激光雷达点云、相机图像这类对实时性极其敏感的传感器,建议发布端和订阅端都用BEST_EFFORT,因为丢失一两帧点云远好过为了重传一个旧数据而阻塞新数据。相反的,导航路径目标点、任务指令这类控制类消息,必须用RELIABLE,丢失一帧可能让机器人走错路。除此之外,如果订阅端启动得比发布端晚,而且需要拿到最新的一帧状态,你需要考虑TRANSIENT_LOCAL配合KEEP_LAST(1)这样的组合,否则晚到的订阅者只能从自己的生命周期开始收数据。
这些配置不是在代码里随便传两个枚举值就完了,而是需要根据业务场景、通信频率、消息大小做取舍,说到底QoS是中间件架构给开发者的一组“控制旋钮”,用得好不好,取决于你对业务的理解深不深。
5. Fast DDS、Cyclone DDS还是Connext?中间件选型与性能实测
选择哪一款DDS,几乎是每个初入ROS 2的团队都会纠结的问题。ROS 2官方文档给的是“你可以选任意一个”,但这句话对做实际产品的人毫无帮助。不同的DDS实现,在延迟、吞吐、资源占用、实时性、license费用上差异非常大。
5.1 三款主流实现的性格画像
Fast DDS(原Fast RTPS)是eProsima开发的,也是ROS 2默认使用的实现。它的社区活跃度最高,功能覆盖全面,和Linux、Windows、macOS的兼容性都很好。缺点是资源占用相对较高,在受限的嵌入式环境中不一定是最优选择。对于绝大多数刚上手ROS 2的团队,Fast DDS是入坑最顺的选项。
Cyclone DDS是Eclipse基金会的项目,由ADLINK维护。它的设计理念是“极致的性能和轻量”,在延迟、内存占用方面表现非常优秀。尤其是对实时性敏感的机器人场景,Cyclone DDS常常是更优选择。它的短板是某些高级特性(比如部分安全插件)不如商业实现丰富。我实际测过Cyclone DDS和Fast DDS在相同硬件上跑高频率点云话题,端到端延迟Cyclone大约能低20%~30%,并且CPU占用也略低。
RTI Connext是商业方案,性能和稳定性很强,在航空航天、军工、自动驾驶行业有大量成熟应用,同时提供分布式系统调试工具。但它是收费的,对于开源项目、预算有限的研究团队来说,不是第一选择。初学者不推荐直接从Connext入手,因为生态环境和资料相对封闭,出了问题社区问不到人。
还有新的选择,比如GurumDDS等国产轻量实现,也在逐渐完善中。选型时不能只看跑分,还得看维护团队、文档质量、技术支持、社区活跃度。
5.2 我在两种场景下的实测数据
我把自己在项目中做过的一组简单压测数据拿出来分享,硬件是Intel NUC(i5-1135G7),系统Ubuntu 22.04,ROS 2 Humble,测试话题是1MB大小的点云消息,频率20Hz。
| 项目 | Fast DDS | Cyclone DDS |
|---|---|---|
| P50延迟 | 3.8ms | 2.6ms |
| P99延迟 | 12.4ms | 7.9ms |
| CPU占用 | 42% | 35% |
| 内存占用 | 210MB | 150MB |
这个测试不能代表生产环境的全部状况,但它印证了一件事:DDS实现的选择直接决定了通信瓶颈出现在哪里。如果你的机器人集成度高、板卡资源紧张,我会优先建议跑一遍相同测试再决定。另外提醒一句,性能测试不要只在回环地址(localhost)上测,要接真实网络环境,因为不同DDS在Wi-Fi和有线网络下的行为差异很大。
5.3 换中间件时的一个隐藏坑:类型支持代码
很多人以为换DDS就是改一下RMW_IMPLEMENTATION这么简单,其实要走一个很重要的环节:重新编译类型支持。ROS 2安装时默认会为当前指定的RMW实现生成各消息包的类型支持代码。切换到另一个RMW后,部分包需要重新编译,因为rosidl_typesupport_*是针对具体RMW的库。
实际操作中,我建议在src目录下先清除之前的编译产物,重新执行:
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release --symlink-install然后再在启动环境里设置RMW_IMPLEMENTATION。如果编译顺序不对,很容易出现“包找到了但类型支持库对不上”的诡异报错。我踩过一次整整折腾半天的坑,就是没有重建消息包,直接切RMW,导致运行时报Failed to create subscriber。
5.4 什么时候需要换掉默认的DDS
给你一个简单的决策参考:项目还在原型验证期,默认Fast DDS足够;要上实时控制、高频率传感器融合,尤其是跑在Jetson这类GPU板卡上,Cyclone DDS表现通常更好;如果项目进入产品化阶段且甲方对可靠性、合规性有硬性要求(比如汽车功能安全认证),再评估是否引入RTI Connext。
另外,如果网络环境非常恶劣(比如跨VLAN、跨NAT通信),提前考虑Discovery Server的部署方案,这比等到现场联调再解决要省钱省时间得多。
6. 中间件架构进阶:进程内零拷贝、确定性执行与安全性
ROS 2中间件这块还有几个值得深入研究的特性,平时不一定会用到,但理解之后,你对整个架构的理解会上一个台阶。
6.1 进程内通信:消息不落网卡也能到达
上一代ROS的进程内通信虽然也有优化,但ROS 2通过RMW层把进程内通信做成了标准能力。当发布者和订阅者在同一个节点进程内(例如同一个Node对象下开多个线程,或者用rclcpp的组合机制),消息可以直接通过intra-process传递,不经过DDS网络栈。
开启方式是在创建订阅者时指定rclcpp::SubscriptionOptions中的use_intra_process_comm选项。此模式下,发布者通过UniquePtr发布消息,订阅者拿到的是同一块内存的引用或拷贝,省掉了序列化和网卡传输。对于图像流、大点云这类高负载数据,收益我前面提过,非常显著。
需要注意的是,intra-process模式下消息的生命周期管理要小心。发布者发布一个UniquePtr后,如果订阅者没有及时处理,这块内存有可能被释放,产生悬垂指针。所以官方建议配合rclcpp::Publisher的Loan机制或者仔细设计消息所有权。
6.2 确定性执行:让多机器人系统的时序不再“随缘”
多机器人协同系统最大的敌人不是网络带宽,而是“时序不确定性”。A车在t0时刻发出信息,B车在t0+ε时刻收到,这个ε在不同的DDS实现上可能差出几个毫秒,在实时性要求高的编队控制或碰撞避免里是无法接受的。
ROS 2引入的实时执行模型、Executor调度的可配置性,加上DDS在实时调度上的能力,让开发者有机会把通信延迟控制在一个相对确定的区间。Cyclone DDS在这方面的设计思路值得学习:它大量使用无锁队列和严格的发送调度,能让延迟抖动明显下降。我在实验中发现,Cyclone DDS在同样的无线网络压力下,延迟抖动比Fast DDS小得多,这对编队控制类任务很关键。
当然,要做到真正的确定性,光靠DDS还不够,操作系统层面也得用RT_PREEMPT内核补丁或者真正的实时操作系统(如PX5、QNX),还要避开内存锁、调度优先级反转等经典问题。中间件只是这条链路上的一环。
6.3 安全机制:DDS也在考虑“机器人防火墙”
DDS标准里定义了DDS Security规范,提供身份认证、访问控制、数据加密等能力。ROS 2中也有对应的SROS 2框架,通过给节点、话题配置权限文件,结合DDS安全插件,实现通信加密。
但实话实说,SROS 2在手感上还比较“粗”,配置项复杂,密钥管理需要自己搭,很多团队做产品时宁愿在应用层加个AES套壳。不过如果你做的是室外巡检、物流配送这类需要远程部署的机器人,一定要考虑设备被物理接触后通信密钥泄露的风险,DDS安全加密值得投入。
6.4 和微服务中间件的区别:别拿消息队列的思维看DDS
很多有后端开发背景的同学,一看到中间件三个字,自然想到Kafka、RabbitMQ。ROS 2的DDS和这些消息中间件是两个物种。Kafka、RabbitMQ是集中式消息代理,数据要走Broker中转;DDS是去中心化的数据分发总线,发布者与订阅者直连。因此ROS 2中间件天然低延迟、高吞吐,但是跨WAN传输、消息积压、持久化能力都不如专门的消息队列。
如果机器人和云端需要做异步通信,现在主流的做法是:机器人端用DDS做节点间高速通信,通过桥接层(比如rmw_zenoh或者自研的MQTT Bridge)把摘要数据传到云端,再用MQTT/Kafka类中间件做云端汇聚分析。这是架构上的合理分工,不是谁替代谁。
7. 我在中间件故障排查中总结的排错清单
最后说一下实操最常见的坑。这个清单是我在做ROS 2项目过程中,自己和身边同事踩过次数最多的、最容易被忽略的问题。
7.1 节点已经启动,但我订阅不到话题数据
这类问题的排查顺序我建议是:先看ros2 topic list里有没有对应话题,再看ros2 topic hz有没有频率,再看ros2 topic info -v看发布者和订阅者的QoS是否兼容,最后看ros2 doctor有没有网络和发现相关的告警。
很多人第一步就直接怀疑代码逻辑,其实80%的问题出在QoS不匹配或者发现了但没匹配上。多机环境下还要检查ROS_DOMAIN_ID是否一致,这个变量相当于“微信群号”,不一致就是两个世界,谁也别想刷到对方朋友圈。
7.2 消息延迟突然飙升,重启节点后恢复
这是我认为最诡异也最耗时的故障。走了很多弯路后,最后定位到的原因是DDS的发现协议在收到大量组播包后出现行为异常,尤其在Wi-Fi环境下,组播被AP干扰或IGMP Snooping配置不当,会导致数据流中断或重传增大。临时解决方案是重启节点,长期方案是改用Discovery Server模式,减少组播依赖,另外把Wi-Fi网络里的组播优化打开。
7.3 换了一台机器部署后,同样的代码跑不起来了
多半是环境变量没设全,比如RMW_IMPLEMENTATION在旧机器上设成了rmw_cyclonedds_cpp,新机器没装对应的适配层包;或者新机器默认了另一款DDS,但编译产物还是旧的。这类问题首先检查printenv | grep RMW,看环境变量究竟指向哪里。
7.4 高负载下内存持续增长,疑似内存泄漏
ROS 2不同DDS实现里,消息的内存池管理策略差异很大。比如Fast DDS在RELIABLE模式下,如果订阅端处理不过来,DDS的中继缓冲区会积累未ack的样本,内存占用持续上升。这时候优先调小history_depth,或者把订阅端的回调处理改成异步线程池模式,防止阻塞DDS内部线程。Vulkan版本的日志等级也要注意,不要在生产环境下开ROS_LOG_DEBUG级别输出,打印日志带走的CPU会反过来加剧通信延迟。
7.5 多机器人同时通信,话题互相串了
这个问题十有八九是ROS_DOMAIN_ID没隔离。每台机器人或者每个子系统建议分配独立的Domain ID,不同Domain的Participant无法互相感知,所以也不需要担心话题命名冲突。还可以额外设置ROS_LOCALHOST_ONLY=1强制只在本机回环上通信,防止机器人局域网里出现跨设备串扰。
8. 理解中间件架构之后,再看一次ROS 2的成长方向
从ROS 1到ROS 2,中间件架构的变化不是某个模块的微调,而是整个通信哲学的重写。ROS 2把通信的决定权交给标准化的DDS体系,再用RMW抽象出了一层“可替换空间”,让机器人开发者既能得到工业级可靠性的底座,又能保留按需选择底层实现的权利。
思考的时候可以换个角度:中间件在这里不是简单的“消息转发工具”,它定义了整个系统的时序边界、可靠性边界和安全边界。我们做的每一个话题设计、每一个QoS配置,本质上都是在用中间件的语言和物理世界做协商。
回到你手上的项目,建议抽个周末,把当前系统的话题流动路径画出来,标注每段通信的QoS策略、数据量、频率要求,这会让你对“哪里需要瘦身、哪里需要增加容错”一目了然。我在实际项目里就是通过这样的梳理,把一些不必要的高频大消息从20Hz降到5Hz,整机CPU占用一下子就降下来了。中间件架构掌握到这种程度,你才算真的把ROS 2用出了它的设计初衷。