Skip to content

第六章 · ROS 2 体验与系统架构 ​

上一章写的两个 Hello World 还没碰 ROS 2。这一章正式进门:先跑三个自带的小例子,亲眼看两个节点怎么自动找到对方、怎么收发消息;然后把 ROS 2 从应用层到操作系统层一层层拆开,搞清楚 rcl、RMW、DDS 各自负责什么,为什么节点之间要用它来通信。

6.1 你说我听:最简的两个节点 ​

这个“游戏”只做一件事:启动两个节点,一个负责发消息(说),一个负责收消息(听)。

  1. 打开一个终端,快捷键 Ctrl+Alt+T。

  2. 在里面启动倾听者——它是收到消息的一方:

    bash
    ros2 run demo_nodes_py listener
  3. 再打开一个新终端,同样按 Ctrl+Alt+T。

  4. 在新终端里启动说话者——它是发消息的一方:

    bash
    ros2 run demo_nodes_cpp talker

然后观察两个终端的输出:talker 节点每数一个数,listener 节点就收到一个,一个不落。一个 Python 节点和一个 C++ 节点,没有配置过彼此的地址,却自动连上了。终端里的输出大致是这样:

text
[talker]:   Publishing: 'Hello World: 1'
[listener]: I heard: [Hello World: 1]

talker 与 listener 两个节点在终端中收发消息

图:你说我听

先记住这个现象

两个节点互不认识,也没告诉对方自己在哪里,却能自动匹配。这说明 ROS 2 在背后有一层机制替它们做了“牵线”——这就是下面要拆的架构。

6.2 涂鸦乌龟 ​

打开终端(Ctrl+Alt+T),输入下面的指令启动海龟画图:

bash
ros2 run turtlesim turtlesim_node

运行后会弹出一个画着海龟的窗口:

turtlesim 海龟模拟器窗口

图:海龟模拟器

点一下原来的终端,按 Ctrl+Shift+T 在同一个窗口里开一个新的标签页,输入:

bash
ros2 run turtlesim turtle_teleop_key

这个节点负责接收键盘按键。按方向键,海龟就会在窗口里移动并留下轨迹,这就是“涂鸦乌龟”。

Ctrl+Shift+T 和 Ctrl+Alt+T 的区别

Ctrl+Alt+T 是新开一个终端窗口,Ctrl+Shift+T 是在当前终端窗口里新开一个标签页。第二个节点刻意开在标签页里,就是为了方便和第一个节点的输出对照着看。

6.3 rqt 可视化 ​

保持前面两个游戏都在运行,再打开一个终端,输入:

bash
rqt

打开之后选择插件 Introspection / Node Graph,刷新一下,就能看见节点之间的通信关系:

rqt 的 Node Graph 插件展示节点之间的连线

图:RQT Node Graph 可视化

rqt 把“节点之间谁发给谁”画成了图:椭圆里是节点名,箭头旁边标着话题名,箭头指向就是消息的流向。刚才“你说我听”里两个节点自动匹配的秘密,到这里已经能看见一半了——图里 /talker 指向 /listener 的那根箭头,旁边写的就是它们通信用的话题。

6.4 分层架构总览 ​

前面两个小游戏跑通了,rqt 里也能看到连线,但这些都还停留在“会用”的层面。这一节把 ROS 2 拆开,从整体框架上看它由哪几层构成,以及为什么要这样分层。

ROS 2 的整体结构可以描述成一张“千层饼”式的图,从上到下依次是:应用层 → ROS 2 客户端库 → 客户端库核心 rcl → 中间件抽象层 RMW → DDS 实现层 → 操作系统层。相邻两层之间是双向调用关系:上层向下层提出请求,下层把结果和事件回传给上层。

层次(自上而下)这一层有什么这一层的作用
应用层用户节点、rqt、命令行工具我们写的所有节点代码都住在这里
ROS 2 客户端库rclpy、rclcpp、rcljava、rclnodejs……提供给各语言的 API(跨语言绑定)
客户端库核心 rclC 语言实现的统一核心ROS 2 自身的实现,各语言库共用
中间件抽象层 RMWROS Middleware 抽象接口给上层统一接口,屏蔽各家差异
DDS 实现层Fast DDS、Cyclone DDS、Connext……第三方中间件,可自由替换
操作系统层Linux、Windows、macOS、驱动与网络协议栈提供硬件、进程、网络等基础能力

各层的归属并不相同:从应用层到客户端库核心 rcl,这三层是 ROS 2 官方自己实现的;中间的 RMW 是一层统一接口;最下面的 DDS 实现层则来自第三方。接下来自下而上,一层一层看。

6.5 操作系统层 ​

这一层最好理解。ROS 2 自己不是一个操作系统,它只是跑在 Linux、Windows 或 macOS 之上的一个中间件框架。真正去驱动硬件、管理进程、收发底层网络数据包这些脏活累活,全部交给操作系统完成。

也正因为如此,ROS 2 才能跨这么多平台:只要那个平台提供的系统调用和网络协议栈足够完整,上面的各层就可以原封不动地搬过去。

顺带说一句,前面折腾 WSL 里的 Ubuntu,其实就是在给上面几层准备一块合适的“地基”。

6.6 DDS 实现层 ​

想弄懂这一层,得先回答两个问题:DDS 到底是什么?为什么 ROS 2 里会同时存在好几套 DDS 实现?

6.6.1 DDS 是什么 ​

DDS 全称 Data Distribution Service,中文叫“数据分发服务”,是由对象管理组 OMG(Object Management Group)在 2003 年发布、2007 年修订的一套分布式系统通信标准。

它的通信方式和 ROS 里的话题发布订阅几乎一模一样:一方把数据“发布”到某个主题上,关心这个主题的另一方“订阅”后就能收到,双方不需要事先知道对方是谁。除此之外,DDS 还提供了相当丰富的服务质量(QoS)策略,用来规定通信的可靠性、持久性、传输方式等要求。

正因为设计理念如此契合,ROS 2 干脆不再自己另造一套通信协议,而是直接把 DDS 拿来当作底层的通信支柱。

6.6.2 为什么会有多套 DDS 实现 ​

DDS 只是一份标准文档,标准本身不能运行,得有人照着它写出真正的程序,这些程序就叫“DDS 实现”。目前常见的实现有:

DDS 实现出品方
Fast DDSeProsima
Cyclone DDSEclipse
Connext DDSRTI

它们遵守同一份标准,但侧重点各不相同:有的偏重性能,有的体积小巧,有的面向商用场景提供技术支持。既然标准是统一的,多准备几套实现就不会给上层添麻烦,想换随时换。至于这些差异是怎么被“抹平”的,就要看下面这一层。

6.7 中间件抽象层 RMW ​

RMW 是 ROS Middleware 的缩写,也就是中间件抽象层。它的作用可以用一句话概括:给上面一个统一的接口,把下面各家的差异藏起来。

为什么不直接把 DDS 暴露给上层?因为直接用 DDS 其实挺麻烦:创建一次通信要设置分区、主题名称、发现模式、消息类型等一大堆参数,还要记得在不用的时候逐项清理。这些琐碎而重复的配置如果让每个写节点的同学自己处理,代码会变得又臭又长。

于是 ROS 2 在中间加了一层 RMW:它定义好一套固定的接口,无论底下换成 Fast DDS 还是 Cyclone DDS,上层看到的 API 都完全一样;那些繁琐的配置也就顺理成章地在 RMW 和 RCL 里统一完成了。

bash
# 查看当前使用的 DDS 实现
ros2 doctor --report | grep middleware

# 通过环境变量切换(需要系统里已安装对应的 DDS)
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp

6.8 客户端库 RCL 与语言绑定 ​

RCL 是 ROS Client Library 的缩写,也就是 ROS 2 客户端库。它建立在 RMW 之上,把 ROS 里的节点、话题、服务、参数、Action 这些概念包装成一套好用的 API。

这里有个细节值得留意:RCL 的底层实现是用 C 语言写的。因为 C 是众多高级语言的“共同祖先”,基于它再去封装其它语言会轻松很多。所以官方的 rclpy(Python)和 rclcpp(C++)都直接建立在这套 C 实现之上——这也是后面用两种语言写节点时,代码结构会如此相似的原因。

社区还用同样的方式把 ROS 2 搬到了更多语言和平台上,比如在 Android 应用或网页里也能和 ROS 2 通信:

语言客户端库项目地址
Pythonrclpyhttps://github.com/ros2/rclpy
C++rclcpphttps://github.com/ros2/rclcpp
Javarcljavahttps://github.com/esteve/ros2_java
Rustrclrusthttps://github.com/ros2-rust/ros2_rust
Node.jsrclnodejshttps://github.com/RobotWebTools/rclnodejs
Gorclgohttps://github.com/juaruipav/rclgo
Luarclluahttps://github.com/jbbjarnason/rcllua
Kotlinrclkinhttps://github.com/ros2java-alfred/ros2_kotlin
Swiftrclswifthttps://github.com/atyshka/ros2_swift
C#rclcshttps://github.com/RobotecAI/ros2cs

表:ROS 2 的常见多语言客户端库

6.9 应用层 ​

最上面就是我们自己写代码,以及 rqt、命令行工具这些开发工具所在的层。后面写的所有节点代码,其实都住在这里。

它们不需要关心底下的通信是怎么实现的,只要调用客户端库提供的 API 就好——这也正是分层带来的好处。

6.10 中间件与 DDS 架构 ​

6.10.1 什么是中间件 ​

中间件是一种独立的系统软件或服务程序,分布式应用软件借助它在不同技术之间共享资源。中间件位于客户机/服务器的操作系统之上,管理计算机资源和网络通讯,是连接两个独立应用程序或独立系统的软件。相连的系统即使接口不同,通过中间件也能交换信息;执行中间件的一个关键途径是信息传递。借助中间件,应用程序可以工作在多平台或不同 OS 环境。

一张熊猫表情:这是在说什么,怎么一个字都听不懂

图:第一次读上面这段定义时的真实感受(笑)

6.10.2 DDS 接口层与抽象层 ​

ROS 2 为每家 DDS 供应商都开发了对应的 DDS_Interface(即 DDS 接口层),然后通过 DDS Abstract 抽象层来统一 DDS 的 API:

从应用代码、ROS 客户端库 API、抽象 DDS API 到各家 DDS 实现的分层关系

图:ROS2 DDS 架构

6.11 DDS 通信模型:DCPS ​

DDS 的核心模型是 DCPS——以数据为中心的发布订阅(Data-Centric Publish-Subscribe)。

DCPS 模型通过主题实现发布者与订阅者的解耦,核心构成包括:

  • 域(Domain):逻辑隔离的通信平面。只有相同域 ID 下的节点才能互相发现和通信,用来解决多系统同网络下的隔离问题。
  • 主题(Topic):连接发布者与订阅者的逻辑纽带,对数据做分类标记,是发布和订阅匹配的唯一依据。发布者只需要发布对应主题的数据,所有订阅该主题的节点都会自动收到。
  • 发布者/数据写入器(Publisher/DataWriter):由发布者创建 DataWriter,实际负责数据的发送。
  • 订阅者/数据读取器(Subscriber/DataReader):由订阅者创建 DataReader,实际负责接收对应主题的数据。

这种模式下,发布者和订阅者不需要知道对方的地址信息,仅通过主题完成匹配,能够轻松构建复杂的多对多通信关系,实现系统的高度解耦。6.1 节里 talker 和 listener 能自动接上,靠的就是这套模型。

6.12 ROS 2 的节点是怎么编译出来的 ​

节点说到底是 C++ 程序,而 C/C++ 程序从源码到能执行,一般要经过预处理、编译、汇编、链接几个步骤:

text
源文件(.h、.cpp 等)
   │ 预处理
   ▼
   │ 编译
   ▼
   │ 汇编
   ▼
   │ 链接          ←── 静态库 / 动态库
   ▼
可执行文件

实际项目里,有些公共代码需要反复使用,就会先编译成“库”文件,在链接阶段由链接器从库文件里取出所需代码:

  • 静态(链接)库:可执行文件中包含库代码的一份完整拷贝,缺点是被多次使用就会有多份冗余拷贝。
  • 动态(链接)库:程序开始运行后调用库函数时才被载入。这种库独立于现有的程序,本身不可执行,但包含着程序需要调用的一些函数。

两种库在不同平台上的后缀也不一样:

平台静态链接库动态链接库
Windows.lib.dll
Linux.a.so

静态库、动态库的区别来自链接阶段如何处理库;链接成可执行程序的方式,分别称为静态链接和动态链接。

那么,一个 ROS 2 节点该怎么正确编译?直接用 g++ 单文件编译是走不通的——正确做法是构建一个 ament 包,这正是下一章的内容。

下一篇:第七章 · 用 CMake + ament 编译节点。

用 VitePress 搭建 · 托管在 GitHub Pages