这篇博客是 B 站《古月·ROS2入门21讲》的第十三个视频的图文记录,主要介绍了 ROS2 的分布式通讯如何使用,此外还补充了一点是在同一个设备下 Docker 容器和宿主机之间的通讯如何配置。原始视频链接如下:
- B 站视频链接:13. 分布式通讯:多计算平台的任务分配
这一节的内容是 ROS2 和 ROS1 之间差异最大的体现,虽然前面的博客在具体实现甚至命令上已经和 ROS1 存在不同了,但其核心设计理念依旧一致。然而,这一节的分布式概念在 ROS1 中是完全不存在的。
【Note】:作者视频中使用了一个单独的树莓派硬件作为第二主机,但我手边没有该设备,只能用另一台笔记本进行演示。
由于在机器人开发过程中 Docker 是很常用的工具,在一台设备中同时运行多个 Docker 容器是常规操作,每个容器中运行若干节点也是很常见的情况,因此尽管作者没有在视频中涉及到 Docker 相关的设置,我这里也进行了一段补充。
1. 多台硬件通讯
另一台笔记本硬件我使用的是 Intel x86 芯片的 ThinkPad X1,安装了 Ubuntu 22.04 系统与 ROS2 Humble:
首先需要将两台设备放在 同一个局域网内,即确保 IP 地址在一个网段下,并保证两台设备能相互 Ping 通,如果 Ping 不通可能是防火墙的问题,晚上有很多解决方案故此处不进行展开。
使用下面的命令查看设备的 IP 地址:
bash
$ ifconfig | grep inet

然后 不用做任何配置,因为 ROS2 本身是自发现机制。
- 在设备1上启动一个 talker:
bash
$ ros2 run demo_nodes_cpp talker

- 在设备2上启动一个 listener:
bash
$ ros2 run demo_nodes_cpp listener
2. 分组通讯
如果在一个网段中所有设备都互通互联,那么依旧会对网络造成巨大的负担,况且有时候我们只是将设备放在了一个空间中,并不想让所有设备都相互通讯。因此 ROS2 引入了 分组 概念用来将不同设备以组的形式进行隔离。
在你想要划分同一个组设备的 ~/.bashrc 末尾添加相同的 ID 号:
bash
$ export ROS_DOMAIN_ID=1

[可选] 3. Docker 宿主机与容器
3.1 拉取 ROS Humble 镜像
我将实验设备 Jetson Orin 作为宿主机,并在上面拉取了一个 ROS2 Humble 的镜像,该镜像是 Nvidia 在其 NGC 平台上发布的官方镜像,该平台上有很多现成的 Docker 可以拉,并且经过了严格的测试,能极大减轻你的配置压力。当然你也可以自己拉一个空白镜像然后手动安装 ROS。
- Nvidia NGC 官网:https://catalog.ngc.nvidia.com/

这里拉取的是 nvcr.io/nvidia/isaac/ros:aarch64-ros2_humble_adc428c7077de4984a00b63c55903b0a 镜像,该镜像总大小 11.71 GB,等待拉取完成即可:
bash
$ docker pull nvcr.io/nvidia/isaac/ros:aarch64-ros2_humble_adc428c7077de4984a00b63c55903b0a

然后你就可以查看现有的容器:
bash
$ docker images

3.2 运行镜像
【Note】:由于 ROS2 本身的架构设计,当节点在 不同设备上走的是 DDS 通讯,当节点都在 同一个设备上则走的是共享内存;然而 Docker 默认是不开启共享内存的,这样会造成以下两个现象:
- 宿主机和容器都可以看到对方节点与话题;
- 当 echo 话题时输出均为空;
这里提供两种最常见的解决方案:
[推荐] 开启共享内存
Docker 共享内存的开启需要用参数 -v /dev/shm:/dev/shm\ 进行指定,本质上就是让宿舍机和容器共用 /dev/shm 的目录,该目录是 ROS 共享内存数据存放的地方。
之所以推荐使用该方法是因为:这种操作是从本质上解决数据访问的问题,同时 几乎不损耗通讯性能,ROS2 最大的特点就是在使用 DDS 的同时保证了通讯性能。
那么我最常用的 Docker 启动命令如下,参数非常多但都是必要的,这种配置 几乎可以无缝访问宿主机的硬件、网络和图形界面。
bash
$ docker run -dit \
--net=host \
--ipc=host \
--pid=host \
--privileged \
-v /dev:/dev \
-v /dev/bus/usb:/dev/bus/usb \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-e DISPLAY=$DISPLAY \
-v /etc/localtime:/etc/localtime:ro \
-v $(pwd):/workspace \
-v /dev/shm:/dev/shm\
--name demo \
nvcr.io/nvidia/isaac/ros:aarch64-ros2_humble_adc428c7077de4984a00b63c55903b0a \
/bin/bash
--net=host:容器内使用宿主机的网络,而不是 Docker 自己的虚拟网络。这对于 ROS 2 的节点发现至关重要;--ipc=host:共享宿主机的进程间通信资源(如共享内存);--privileged:容器内可以访问宿主机的所有设备;-v /dev:/dev:共享宿主机的设备文件,允许容器直接与摄像头、GPU、USB 设备等硬件通信;-v /tmp/.X11-unix、-e DISPLAY:共享宿主机的 X11 图形界面,允许你在容器内运行带 GUI 的程序(如 Rviz)并显示;-v $(pwd):/workspace:挂载了当前目录,方便你在容器内访问和修改宿主机上的代码;-v /dev/shm:/dev/shm:挂载共享内存目录,确保进程间通信顺畅;
[临时] 修改通讯方式
除了上面在运行容器的时候就指明共享内存的卷,还可以在正在运行的容器中以修改通讯方式的方式形式让宿舍机和容器正常传输数据,但这样做 会有一定传输损耗,如果你的设备不多的话这些损耗可以忽略不计。
修改宿主机和容器的 ~/.bashrc 文件,在文件末尾添加以下环境变量,该环境变量的含义是在使用 FastDDS (ROS2默认通讯协议)时只以 UDPv4 形式运行,那么如果你的所有节点都在同一个硬件上,相当于 所有数据又走了一圈网卡,而共享内存则直接访问文件。
bash
export FASTDDS_BUILTIN_TRANSPORTS=UDPv4
【Note】:宿主机和容器内都要修改,否则依旧无法获取话题中的数据;

启动 Talker 和 Listener
在容器中启动一个 talker:
bash
$ docker exec -it demo /bin/bash
root@ubuntu:/# ros2 run demo_nodes_cpp talker

在宿主机上启动一个 listener:
bash
$ ros2 run demo_nodes_cpp listener
