3. ROS2 基础实战:节点、话题、服务、动作的创建与通信

好,咱们直接进入正题。这一章是 ROS2 的“内功心法”。你想想看,机器人系统里那么多模块——传感器、控制器、规划器——它们怎么互相说话?靠的就是节点、话题、服务、动作这四样东西。

我个人习惯把这四样比作一个团队协作的场景:节点是干活的人,话题是广播喇叭(一个人说,多人听),服务是打电话问答案(一问一答),动作是派发长期任务(边干边汇报进度)。

下面咱们就动手,把这四个家伙都跑一遍。

ROS2 通信中间件 节点 (Node) 话题 (Topic) 服务 (Service) 动作 (Action) 发布/订阅 | 请求/响应 | 目标/反馈/结果

3.1 节点:ROS2 的最小执行单元

节点说白了就是一个独立的可执行程序。每个节点只干一件事,比如读取激光雷达数据,或者控制电机。我刚开始学的时候,总想把所有功能塞进一个节点里——后来发现调试起来简直要命,一个地方崩了全完蛋。

创建节点的标准姿势:

// 文件名: my_first_node.cpp
#include "rclcpp/rclcpp.hpp"

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);
  // 创建节点,名字叫 "minimal_node"
  auto node = std::make_shared<rclcpp::Node>("minimal_node");
  RCLCPP_INFO(node->get_logger(), "节点已启动: %s", node->get_name());
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}
💡 我的习惯:节点命名用蛇形命名法,比如 laser_scan_node。别用驼峰,ROS2 社区约定俗成的规矩。

3.2 话题:一对多的异步通信

话题是最常用的通信方式。发布者只管往外扔数据,订阅者只管收。双方不需要知道对方的存在——这就是解耦。

我曾经在一个项目中,需要把摄像头数据从相机节点传给视觉处理节点。用话题就特别合适,因为图像数据是持续产生的,订阅者随时加入都能收到最新帧。

发布者示例:

// 发布 "chatter" 话题,类型为 std_msgs::msg::String
auto publisher = node->create_publisher<std_msgs::msg::String>("chatter", 10);
std_msgs::msg::String message;
message.data = "Hello ROS2!";
publisher->publish(message);

订阅者示例:

auto subscription = node->create_subscription<std_msgs::msg::String>(
  "chatter", 10,
  [](const std_msgs::msg::String::SharedPtr msg) {
    RCLCPP_INFO(rclcpp::get_logger("subscriber"), "收到: '%s'", msg->data.c_str());
  });
⚠️ 避坑指南:我曾经把队列大小设成 1,结果在高频率发布时丢包严重。一般建议设成 10 或 100,具体看你的数据频率和实时性要求。

3.3 服务:同步的请求-响应模式

服务适合那种“你问我答”的场景。比如你让机器人抓取一个物体,你发出请求,它返回成功或失败。注意,服务是同步的——客户端发完请求后会阻塞等待响应。

服务端定义:

// 服务类型: example_interfaces::srv::AddTwoInts
auto service = node->create_service<example_interfaces::srv::AddTwoInts>(
  "add_two_ints",
  [](const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> req,
     std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> res) {
    res->sum = req->a + req->b;
    RCLCPP_INFO(rclcpp::get_logger("service"), "返回: %ld + %ld = %ld", req->a, req->b, res->sum);
  });

客户端调用:

auto client = node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = 3;
request->b = 5;
auto future = client->async_send_request(request);
// 等待结果
if (rclcpp::spin_until_future_complete(node, future) == rclcpp::FutureReturnCode::SUCCESS)
{
  RCLCPP_INFO(node->get_logger(), "结果: %ld", future.get()->sum);
}
🔑 关键点:服务调用是同步的,如果服务端处理时间较长,客户端会卡住。这时候就要考虑用动作了。

3.4 动作:带反馈的异步任务

动作是 ROS2 里最强大的通信机制。它结合了话题和服务的特点:你给机器人下达一个目标(比如“移动到坐标 (1,2)”),它一边执行一边给你反馈进度(“已走 30%”),最后告诉你结果(“到达目标”)。

我记得第一次用动作写导航程序时,发现它天然支持取消任务——用户随时可以喊停。这在真实机器人上太重要了,万一撞墙呢?

动作服务端骨架:

// 动作类型: nav2_msgs::action::NavigateToPose
class MyActionServer : public rclcpp::Node
{
public:
  MyActionServer() : Node("action_server_node")
  {
    using namespace std::placeholders;
    action_server_ = rclcpp_action::create_server<nav2_msgs::action::NavigateToPose>(
      this, "navigate",
      std::bind(&MyActionServer::handle_goal, this, _1, _2),
      std::bind(&MyActionServer::handle_cancel, this, _1),
      std::bind(&MyActionServer::handle_accepted, this, _1));
  }

private:
  rclcpp_action::Server<nav2_msgs::action::NavigateToPose>::SharedPtr action_server_;
  // 处理目标、取消、接受的回调函数...
};

动作客户端调用:

auto action_client = rclcpp_action::create_client<nav2_msgs::action::NavigateToPose>(node, "navigate");
auto goal_msg = nav2_msgs::action::NavigateToPose::Goal();
goal_msg.pose.pose.position.x = 1.0;
goal_msg.pose.pose.position.y = 2.0;

auto send_goal_options = rclcpp_action::Client<nav2_msgs::action::NavigateToPose>::SendGoalOptions();
send_goal_options.feedback_callback = [](auto, auto feedback) {
  RCLCPP_INFO(rclcpp::get_logger("client"), "进度: %.2f%%", feedback->distance_remaining);
};
action_client->async_send_goal(goal_msg, send_goal_options);
💡 实战建议:动作的反馈频率不要太高。我曾经设成每 10 毫秒反馈一次,结果控制台刷屏,CPU 占用飙升。一般 100-200 毫秒反馈一次就够了。

3.5 四种通信方式的对比

通信方式 模式 典型场景 是否阻塞 是否支持取消
话题 (Topic) 发布/订阅 传感器数据流、状态广播 否 不适用
服务 (Service) 请求/响应 查询参数、触发一次性操作 是 否
动作 (Action) 目标/反馈/结果 导航、机械臂控制、长时间任务 否(异步) 是

嗯,到这里四种通信方式就讲完了。你想想看,其实它们各有各的脾气:话题适合“一直说”,服务适合“问一句答一句”,动作适合“派个活慢慢干”。

我个人建议,刚开始写代码时先用话题把数据流打通,等需要“请求-响应”时再引入服务,最后遇到长时间任务再上动作。别一上来就全用动作,杀鸡焉用牛刀嘛。

下一节咱们会把这些知识点串起来,写一个完整的机器人控制节点。到时候你就知道,这些通信方式是怎么配合工作的了。