ROS2入门到精通:节点通信、话题订阅与SLAM项目实战

从2023年开始,ROS2已经全面取代ROS1成为机器人开发的主流框架。无论是工业机器人、服务机器人还是自动驾驶领域,ROS2的分布式架构、实时性保障和跨平台支持都展现出了巨大的优势。然而,很多初学者在学习ROS2时都会遇到同样的问题:官方文档过于理论化,网上的教程大多零散不成体系,代码过时无法运行,缺少从基础到项目的完整学习路径。
我在过去两年里带领团队完成了多个基于ROS2的机器人项目,从最初的简单小车到复杂的工业巡检机器人,踩过的坑不计其数。本文将从最基础的节点通信讲起,一步步带你掌握ROS2的核心技术,并通过一个完整的SLAM建图项目,让你真正具备独立开发ROS2机器人的能力。所有代码都基于ROS2 Humble版本编写,经过实际测试,确保你能直接复制运行。
一、ROS2核心架构与基础概念
1.1 ROS2系统架构
ROS2采用了基于DDS(Data Distribution Service)的分布式通信架构,彻底解决了ROS1中存在的单点故障问题。与ROS1的主从架构不同,ROS2中的所有节点都是对等的,它们通过DDS中间件直接进行通信,不需要中央节点的协调。
这种架构带来了几个显著的优势:
- 高可靠性:没有中央节点,单个节点故障不会影响整个系统
- 实时性:DDS提供了多种QoS策略,可以满足不同实时性要求
- 分布式:节点可以运行在不同的计算机上,自动发现和通信
- 跨平台:支持Linux、Windows、macOS和嵌入式系统
1.2 核心概念详解
节点(Node)
节点是ROS2系统中最基本的执行单元,每个节点负责一个特定的功能。一个完整的机器人系统通常由多个节点组成,例如:
- 激光雷达驱动节点
- 相机驱动节点
- 电机控制节点
- SLAM建图节点
- 导航规划节点
每个节点都有一个唯一的名称,用于在系统中标识自己。节点可以通过发布话题、提供服务或执行动作来与其他节点进行通信。
话题(Topic)
话题是ROS2中最常用的通信方式,采用发布-订阅模式。一个节点可以向某个话题发布消息,其他节点可以订阅这个话题来接收消息。发布者和订阅者之间是解耦的,它们不需要知道对方的存在。
服务(Service)
服务是一种请求-响应模式的通信方式,适用于需要立即得到结果的场景。一个节点提供服务,其他节点向该服务发送请求并等待响应。
动作(Action)
动作是一种更复杂的通信方式,适用于需要长时间执行的任务。动作包含三个部分:目标、反馈和结果。客户端向服务器发送目标,服务器在执行过程中不断向客户端发送反馈,执行完成后返回最终结果。
QoS(服务质量)
QoS是ROS2的一个重要特性,它允许你为通信配置不同的服务质量策略,包括:
- 可靠性(Reliability):可靠传输或尽力而为传输
- 耐久性(Durability):是否保留历史消息
- 历史(History):保留多少条历史消息
- 期限(Deadline):消息的有效期限
二、环境搭建与工作空间管理
2.1 系统要求与ROS2安装
重要提醒:强烈推荐使用Ubuntu 22.04 LTS 64位系统! 这是ROS2 Humble的官方支持系统,兼容性最好,问题最少。
- 设置系统语言环境
sudo apt update && sudo apt install locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
- 添加ROS2软件源
sudo apt install curl gnupg2 lsb-release
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] https://mirrors.tuna.tsinghua.edu.cn/ros2/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
- 安装ROS2 Humble桌面版
sudo apt update
sudo apt install ros-humble-desktop
- 配置环境变量
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
echo "source /usr/share/colcon_argcomplete/hook/colcon-argcomplete.bash" >> ~/.bashrc
source ~/.bashrc
- 验证安装
ros2 version
# 输出应该是:ros-humble 0.14.0
2.2 工作空间创建与使用
ROS2使用colcon作为构建系统,工作空间是存放ROS2项目的目录。
- 创建工作空间
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
- 构建工作空间
colcon build
- 配置工作空间环境变量
echo "source ~/ros2_ws/install/setup.bash" >> ~/.bashrc
source ~/.bashrc
- 创建功能包
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake my_robot_cpp
ros2 pkg create --build-type ament_python my_robot_py
三、节点通信与话题订阅实战
3.1 编写C++发布者节点
创建~/ros2_ws/src/my_robot_cpp/src/publisher_node.cpp:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, ROS2! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
3.2 编写C++订阅者节点
创建~/ros2_ws/src/my_robot_cpp/src/subscriber_node.cpp:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
void topic_callback(const std_msgs::msg::String & msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
3.3 配置CMakeLists.txt
编辑~/ros2_ws/src/my_robot_cpp/CMakeLists.txt:
cmake_minimum_required(VERSION 3.8)
project(my_robot_cpp)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(publisher_node src/publisher_node.cpp)
ament_target_dependencies(publisher_node rclcpp std_msgs)
add_executable(subscriber_node src/subscriber_node.cpp)
ament_target_dependencies(subscriber_node rclcpp std_msgs)
install(TARGETS
publisher_node
subscriber_node
DESTINATION lib/${PROJECT_NAME})
ament_package()
3.4 编译并运行
cd ~/ros2_ws
colcon build --packages-select my_robot_cpp
source install/setup.bash
# 终端1:运行发布者
ros2 run my_robot_cpp publisher_node
# 终端2:运行订阅者
ros2 run my_robot_cpp subscriber_node
3.5 QoS配置详解
QoS是ROS2通信中最容易被忽略但又非常重要的部分。很多初学者遇到的"节点运行正常但收不到消息"的问题,都是因为QoS配置不兼容导致的。
// 创建自定义QoS配置
rclcpp::QoS qos_profile(10);
qos_profile.reliability(rclcpp::ReliabilityPolicy::RELIABLE);
qos_profile.durability(rclcpp::DurabilityPolicy::VOLATILE);
qos_profile.history(rclcpp::HistoryPolicy::KEEP_LAST);
// 使用自定义QoS创建发布者
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", qos_profile);
// 使用相同的QoS创建订阅者
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", qos_profile, std::bind(&MinimalSubscriber::topic_callback, this, _1));
常用QoS策略对比:
| 策略 | 适用场景 | 特点 |
|---|---|---|
| RELIABLE | 重要数据传输 | 确保消息可靠到达,重传丢失的消息 |
| BEST_EFFORT | 传感器数据 | 尽力而为传输,不重传丢失的消息,延迟低 |
| TRANSIENT_LOCAL | 地图、配置信息 | 发布者保留历史消息,新订阅者可以收到 |
| VOLATILE | 实时数据 | 不保留历史消息,新订阅者只能收到之后的消息 |
四、服务与动作通信
4.1 服务通信示例
创建服务定义文件~/ros2_ws/src/my_robot_cpp/srv/AddTwoInts.srv:
int64 a
int64 b
---
int64 sum
编写服务端节点:
#include "rclcpp/rclcpp.hpp"
#include "my_robot_cpp/srv/add_two_ints.hpp"
using namespace std::placeholders;
class AddTwoIntsServer : public rclcpp::Node
{
public:
AddTwoIntsServer()
: Node("add_two_ints_server")
{
service_ = this->create_service<my_robot_cpp::srv::AddTwoInts>(
"add_two_ints", std::bind(&AddTwoIntsServer::handle_service, this, _1, _2));
RCLCPP_INFO(this->get_logger(), "AddTwoInts server ready");
}
private:
void handle_service(
const std::shared_ptr<my_robot_cpp::srv::AddTwoInts::Request> request,
std::shared_ptr<my_robot_cpp::srv::AddTwoInts::Response> response)
{
response->sum = request->a + request->b;
RCLCPP_INFO(this->get_logger(), "Incoming request: a=%ld, b=%ld", request->a, request->b);
RCLCPP_INFO(this->get_logger(), "Sending response: sum=%ld", response->sum);
}
rclcpp::Service<my_robot_cpp::srv::AddTwoInts>::SharedPtr service_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<AddTwoIntsServer>());
rclcpp::shutdown();
return 0;
}
4.2 动作通信示例
动作通信适用于需要长时间执行的任务,比如机器人移动到目标位置。它允许客户端在执行过程中取消任务,并接收实时反馈。
五、SLAM项目实战:从仿真到建图
5.1 安装依赖包
sudo apt install -y ros-humble-gazebo-ros \
ros-humble-gazebo-ros-pkgs \
ros-humble-robot-state-publisher \
ros-humble-joint-state-publisher \
ros-humble-joint-state-publisher-gui \
ros-humble-rviz2 \
ros-humble-slam-toolbox \
ros-humble-navigation2 \
ros-humble-nav2-bringup \
ros-humble-turtlebot3-gazebo
5.2 机器人URDF建模
创建~/ros2_ws/src/my_robot/urdf/my_robot.urdf:
<?xml version="1.0"?>
<robot name="my_robot">
<!-- 底盘 -->
<link name="base_link">
<visual>
<geometry>
<box size="0.2 0.15 0.05"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
<collision>
<geometry>
<box size="0.2 0.15 0.05"/>
</geometry>
</collision>
</link>
<!-- 左轮 -->
<link name="left_wheel">
<visual>
<geometry>
<cylinder length="0.02" radius="0.035"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
<collision>
<geometry>
<cylinder length="0.02" radius="0.035"/>
</geometry>
</collision>
</link>
<!-- 右轮 -->
<link name="right_wheel">
<visual>
<geometry>
<cylinder length="0.02" radius="0.035"/>
</geometry>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
</visual>
<collision>
<geometry>
<cylinder length="0.02" radius="0.035"/>
</geometry>
</collision>
</link>
<!-- 激光雷达 -->
<link name="laser_link">
<visual>
<geometry>
<cylinder length="0.05" radius="0.05"/>
</geometry>
<material name="red">
<color rgba="1 0 0 1"/>
</material>
</visual>
</link>
<!-- 关节定义 -->
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="0 0.075 0" rpy="1.5707 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel"/>
<origin xyz="0 -0.075 0" rpy="1.5707 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<joint name="laser_joint" type="fixed">
<parent link="base_link"/>
<child link="laser_link"/>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
</joint>
</robot>
5.3 Gazebo仿真环境搭建
创建启动文件~/ros2_ws/src/my_robot/launch/simulation.launch.py:
import os
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
def generate_launch_description():
urdf_file = os.path.join(
get_package_share_directory('my_robot'),
'urdf',
'my_robot.urdf'
)
with open(urdf_file, 'r') as infp:
robot_desc = infp.read()
return LaunchDescription([
# 启动Gazebo
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(get_package_share_directory('gazebo_ros'), 'launch', 'gazebo.launch.py')
]),
launch_arguments={'world': os.path.join(
get_package_share_directory('turtlebot3_gazebo'),
'worlds',
'turtlebot3_world.world'
)}.items()
),
# 发布机器人状态
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
name='robot_state_publisher',
output='screen',
parameters=[{'robot_description': robot_desc, 'use_sim_time': True}]
),
# 在Gazebo中生成机器人
Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=['-topic', 'robot_description', '-entity', 'my_robot'],
output='screen'
),
# 启动关节状态发布器
Node(
package='joint_state_publisher',
executable='joint_state_publisher',
name='joint_state_publisher',
output='screen'
)
])
5.4 SLAM建图实战
创建SLAM启动文件~/ros2_ws/src/my_robot/launch/slam.launch.py:
import os
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
def generate_launch_description():
slam_params_file = os.path.join(
get_package_share_directory('my_robot'),
'config',
'slam_params.yaml'
)
return LaunchDescription([
# 启动仿真环境
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(get_package_share_directory('my_robot'), 'launch', 'simulation.launch.py')
])
),
# 启动SLAM Toolbox
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(get_package_share_directory('slam_toolbox'), 'launch', 'online_async_launch.py')
]),
launch_arguments={'params_file': slam_params_file, 'use_sim_time': 'True'}.items()
),
# 启动RViz
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(get_package_share_directory('nav2_bringup'), 'launch', 'rviz_launch.py')
]),
launch_arguments={'rviz_config': os.path.join(
get_package_share_directory('nav2_bringup'),
'rviz',
'nav2_default_view.rviz'
)}.items()
)
])
创建SLAM参数文件~/ros2_ws/src/my_robot/config/slam_params.yaml:
slam_toolbox:
ros__parameters:
solver_plugin: solver_plugins::CeresSolver
ceres_linear_solver: SPARSE_NORMAL_CHOLESKY
ceres_preconditioner: SCHUR_JACOBI
ceres_trust_strategy: LEVENBERG_MARQUARDT
ceres_dogleg_type: TRADITIONAL_DOGLEG
ceres_loss_function: None
odom_frame: odom
map_frame: map
base_frame: base_link
scan_topic: /scan
use_map_saver: true
mode: mapping
resolution: 0.05
max_laser_range: 3.5
minimum_travel_distance: 0.1
minimum_travel_heading: 0.1
scan_buffer_size: 10
scan_buffer_maximum_scan_distance: 3.0
link_match_minimum_response_fine: 0.1
link_scan_maximum_distance: 1.5
loop_search_maximum_distance: 3.0
do_loop_closing: true
loop_match_minimum_chain_size: 10
loop_match_maximum_variance_coarse: 3.0
loop_match_minimum_response_coarse: 0.35
loop_match_minimum_response_fine: 0.45
5.5 启动建图流程
# 编译工作空间
cd ~/ros2_ws
colcon build
source install/setup.bash
# 启动SLAM建图
ros2 launch my_robot slam.launch.py
# 新终端:启动键盘控制
ros2 run teleop_twist_keyboard teleop_twist_keyboard
使用键盘控制机器人在环境中缓慢移动,确保覆盖所有区域。建图完成后,保存地图:
ros2 run nav2_map_server map_saver_cli -f ~/my_map
六、常见问题与踩坑总结
6.1 环境配置问题
问题1:rosdep init失败
解决方案:
sudo rosdep init
rosdep update
如果还是失败,可以使用国内镜像源:
sudo sed -i 's|https://raw.githubusercontent.com/ros/rosdistro/master|https://mirrors.tuna.tsinghua.edu.cn/github-raw/ros/rosdistro/master|g' /etc/ros/rosdep/sources.list.d/20-default.list
rosdep update
问题2:colcon build时内存不足
解决方案:
MAKEFLAGS=-j1 colcon build
6.2 节点通信问题
问题1:节点运行正常但收不到消息
解决方案:
- 检查话题名称是否一致
- 检查消息类型是否一致
- 检查QoS配置是否兼容
- 检查ROS_DOMAIN_ID是否相同
问题2:多机通信失败
解决方案:
- 确保两台计算机在同一网络
- 确保防火墙允许UDP通信
- 设置相同的ROS_DOMAIN_ID
- 检查/etc/hosts文件配置
6.3 SLAM建图问题
问题1:建图漂移严重
解决方案:
- 降低机器人移动速度,建议不超过0.2m/s
- 增加激光雷达扫描频率
- 校准里程计参数
- 启用回环检测
问题2:地图中有很多噪点
解决方案:
- 调整激光雷达最小和最大测距范围
- 增加scan_buffer_size参数
- 启用激光雷达角度补偿
七、总结与展望
本文从ROS2的基础架构讲起,详细介绍了节点、话题、服务、动作等核心概念,并通过完整的代码示例演示了如何实现节点通信。然后,我们通过一个SLAM建图项目,从URDF建模、Gazebo仿真到最终的地图保存,一步步带你掌握了ROS2机器人开发的完整流程。
通过本文的学习,你应该已经具备了以下能力:
- 独立搭建ROS2开发环境
- 编写发布者和订阅者节点
- 配置QoS策略
- 进行机器人URDF建模
- 在Gazebo中进行仿真
- 使用SLAM Toolbox进行建图
下一步学习建议:
- 深入学习Nav2导航系统,实现自主避障和路径规划
- 学习ROS2参数服务器和动态参数配置
- 学习TF坐标变换系统
- 尝试在实际硬件上部署ROS2系统
- 学习ROS2的高级特性,如组件、生命周期节点等
ROS2是一个非常庞大和复杂的系统,本文只是一个入门指南。要真正精通ROS2,还需要在实际项目中不断学习和积累经验。希望本文能为你的ROS2学习之路提供一些帮助。
更多推荐


所有评论(0)