第1章 节点与通信基础

码枢沄社 · 嵌入式体系化教程平台
入门 👤 刚接触ROS2的机器人开发者 约15分钟 🔧 解决多传感器数据不同步问题
本章你将学到
  • 理解ROS2节点是什么,为什么需要它
  • 掌握三种核心通信方式及其适用场景
  • 编写并运行你的第一个发布-订阅程序
核心概念 节点 话题 发布/订阅 DDS

调试一台差速轮机器人时,里程计数据和IMU数据总是对不上——里程计显示走了1米,IMU却认为只移动了0.8米。翻看日志,两个话题的时间戳差了整整300毫秒。不是传感器坏了,而是两个数据处理节点各自为政,没有同步机制。你需要的不是更好的传感器,而是一个让节点之间有序沟通的框架。

1.1 从故障现象理解节点

里程计节点
ROS2中间件(DDS)
IMU节点
/odom话题
/imu话题
融合节点
时间戳对齐

上面这张图暴露了问题根源:里程计节点和IMU节点是独立进程,各自按照自己的节奏发布数据。如果没有统一的同步机制,融合节点拿到两个话题的数据时,时间戳可能已经错位。ROS2的解决方案是把每个功能模块封装成一个节点(Node),通过中间件让节点之间有序通信。

节点在ROS2里就是一个可执行进程,完成一项特定的工作——比如读取激光雷达数据、控制电机、发布里程计等。每个节点拥有唯一的名称,在系统里是全局可识别的。

常见误区:很多初学者以为一个机器人程序里只能有一个节点。实际项目中,一个机器人通常包含十几个甚至上百个节点——每个传感器驱动一个节点,每个算法模块一个节点,独立开发、独立调试、独立崩溃。节点之间互相不影响,一个节点挂了,其他节点还能继续工作。

1.2 通信方式:不只是“发消息”那么简单

通信方式模型方向性同步/异步典型场景
话题(Topic)发布/订阅单向流异步传感器数据流、状态广播
服务(Service)请求/响应双向同步查询参数、触发动作
动作(Action)目标/结果+反馈双向+持续混合导航、机械臂运动

ROS2提供了三种通信原语,分别解决不同场景的需求。上面这张表是你在项目里做技术选型时的速查手册。本章重点讲话题,因为它是使用频率最高、最基础的一种——机器人里90%的数据交换都靠话题完成。

编辑注:话题的异步特性既是优势也是陷阱。优势在于发送者不需要等待接收者,可以按自己的节奏发数据;陷阱在于如果某个订阅者处理太慢,数据会堆积在DDS队列里,最终导致内存溢出或数据延迟。我在一个项目中遇到过激光雷达节点发布频率是40Hz,但建图节点处理不过来,队列占满了内存直接OOM。解决方案是:要么降低发布频率,要么在创建订阅者时设置合理的队列深度(qos),而不是用默认值。

1.3 话题通信:发布/订阅模型拆解

发布者节点
发布消息到话题
DDS中间件分发
订阅者节点接收

话题通信的核心是发布/订阅(Pub/Sub)模型:

这种解耦设计的最大好处是:新增一个订阅者不需要修改发布者的代码。你在调试时临时加一个 ros2 topic echo /odom 命令,就能看到数据流,而发布者节点根本感知不到你的存在。

关键概念:消息类型 话题上传输的数据是有结构的,这个结构由消息定义(.msg文件)指定。比如 std_msgs/msg/String 只包含一个字符串字段,而 sensor_msgs/msg/LaserScan 包含角度范围、距离数组等复杂结构。发布者和订阅者必须使用相同的消息类型才能通信。

这里有一个容易被忽略的细节:话题名 + 消息类型必须完全匹配,订阅者才能收到数据。很多教程不会告诉你的是,如果消息类型不匹配,ROS2不会报错,只会静默地不传输数据。我曾经排查了一整个下午,才发现订阅者用的消息类型是 std_msgs::msg::String,而发布者发的是 std_msgs::msg::UInt8——两个类型都对,但就是收不到数据。

1.4 亲手搭建第一个节点

我们用C++写一个最简单的发布者节点,每隔1秒发布一条 "Hello ROS2" 消息。这里使用ROS2 Humble版本,rclcpp库。

// talker_node.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

int main(int argc, char **argv) {
    rclcpp::init(argc, argv);                              // 1. 初始化ROS2
    auto node = std::make_shared<rclcpp::Node>("talker"); // 2. 创建节点
    auto publisher = node->create_publisher<std_msgs::msg::String>("chatter", 10);
                                                              // 3. 创建发布者
    rclcpp::spin(node);                                     // 4. 保持节点运行
    rclcpp::shutdown();
    return 0;
}

对应的订阅者节点:

// listener_node.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

void topic_callback(const std_msgs::msg::String::SharedPtr msg) {
    RCLCPP_INFO(rclcpp::get_logger("listener"), "收到: '%s'", msg->data.c_str());
}

int main(int argc, char **argv) {
    rclcpp::init(argc, argv);
    auto node = std::make_shared<rclcpp::Node>("listener");
    auto subscription = node->create_subscription<std_msgs::msg::String>(
        "chatter", 10, topic_callback);
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}
代码要点说明
rclcpp::init()初始化ROS2客户端库,必须在所有API调用之前执行
create_publisher<T>(topic, qos)创建发布者,T是消息类型,topic是话题名,qos是队列深度
create_subscription<T>(topic, qos, callback)创建订阅者,注册回调函数,有消息到达时自动调用
rclcpp::spin()阻塞当前线程,不断处理回调,相当于让节点"活"起来

上面这段代码里,spin()是最容易被误解的一行。很多入门教程说它"让节点持续运行",但实际它做的事情是:不断检查是否有新消息到达、是否有定时器触发、是否有服务请求,然后调用对应的回调函数。如果你在 spin() 之后写了代码,那些代码永远不会被执行——因为 spin() 不返回。

1.5 编译运行与排查

把两个节点文件放到ROS2包的 src/ 目录下,修改 CMakeLists.txt 添加编译规则,然后:

# 编译
colcon build --packages-select 你的包名
# 运行发布者
ros2 run 你的包名 talker
# 新终端运行订阅者
ros2 run 你的包名 listener

如果一切正常,订阅者终端会每秒打印一条 "收到: 'Hello ROS2'"。

踩坑实录:我第一次运行时,订阅者终端什么也没打印。排查步骤:
  1. ros2 topic list 检查话题是否存在——看到了 /chatter,说明发布者在工作。
  2. ros2 topic echo /chatter 直接查看话题数据——能看到数据,说明网络没问题。
  3. 检查订阅者代码——发现消息类型写成了 std_msgs::msg::UInt8,而发布者用的是 std_msgs::msg::String。类型不匹配,导致订阅者收不到数据。
如果你看到 ros2 topic list 里有话题但 ros2 topic echo 没输出,大概率是权限问题或DDS配置不对——检查防火墙和网络接口。

1.6 容易被忽略的细节

节点名称的全局唯一性 ROS2的节点名称在系统中是全局唯一的。如果你启动两个同名节点(比如两个 talker),后启动的节点会强制关闭前一个节点,导致原节点消失。这在launch文件中特别容易踩坑——不小心写了两个同名节点,结果只有一个在工作。

还有一点:话题名称的层级结构。ROS2的话题名支持分层命名,比如 /sensor/imu/data/sensor/lidar/scan。这种命名方式不仅是组织代码的习惯,还能利用命名空间实现节点分组和隔离——这在多机器人系统中非常有用。

编辑注:关于DDS的隐藏规则。ROS2底层使用DDS(Data Distribution Service)作为通信中间件,默认实现是Fast DDS或Cyclone DDS。DDS的发现协议(Discovery)依赖UDP组播。如果你在容器里运行ROS2节点,或者跨网段通信,组播可能被防火墙拦截,导致节点之间互相看不见。解决办法是在启动节点前设置环境变量 ROS_DOMAIN_ID,确保所有节点使用相同的domain ID。实际项目里,我建议显式设置ROS_DOMAIN_ID,而不是用默认值0——否则同一网络里不同项目的ROS2节点会互相干扰。

很多教程不会告诉你的是,ROS_DOMAIN_ID 的取值范围是0到101(包括101),超出这个范围DDS会拒绝启动。我曾经设置了 ROS_DOMAIN_ID=200,节点启动时直接报 Domain ID out of range 错误,排查了半天才找到原因。

1.7 实践:自己动手验证

动手练习:
  1. 修改发布者代码,让它在 chatter 话题上发布 std_msgs::msg::Int32 类型的数据,数值递增。
  2. 修改订阅者代码,接收这个Int32消息并打印出来。
  3. ros2 topic info /chatter 查看话题详情,确认类型是否正确。

检验你的理解

判断题:

  1. ROS2的节点名称在系统中可以重复,不会影响系统运行。 (对 / 错)
  2. 话题通信中,发布者和订阅者使用不同的消息类型也能正常通信。 (对 / 错)
  3. 使用 rclcpp::spin() 后,节点会持续处理回调,不会自动退出。 (对 / 错)
本章要点:
  • 节点是ROS2的基本功能单元,每个节点完成一项独立工作,节点之间通过中间件通信。
  • 话题、服务、动作是三种核心通信方式,话题用于持续数据流,服务用于同步请求/响应,动作用于长时间任务。
  • 编写一个ROS2节点只需要四步:初始化、创建节点、创建发布者/订阅者、调用spin()。
  • 节点名称必须全局唯一,话题名+消息类型必须完全匹配才能通信。
  • 设置 ROS_DOMAIN_ID 可以隔离不同项目的ROS2节点,避免干扰。
返回目录 下一章 →