4. ROS2 通信机制实战:发布者与订阅者、服务端与客户端、动作服务器与客户端

ROS2 的核心,说白了就是节点之间的通信。你想想看,一个机器人上有激光雷达、摄像头、电机驱动、导航算法……这么多模块,总得有个办法让它们互相说话吧?

ROS2 提供了三种通信方式:话题(Topic)、服务(Service)和动作(Action)。我个人习惯把它们比作三种不同的对话场景:

  • 话题:像广播电台。发布者一直往外发,订阅者想听就听。适合传感器数据这种持续更新的信息。
  • 服务:像打电话。你问一句,对方答一句,一次性的。适合开关、查询这种短平快的操作。
  • 动作:像派任务。你下达指令后,对方会持续汇报进度,最后告诉你结果。适合导航、机械臂运动这种耗时较长的任务。

这一章,我们就手写这三种通信方式的代码。我会用 C++ 和 Python 各写一遍,你挑自己顺手的学就行。

本章核心目标:掌握 ROS2 三种通信机制的编程范式,能独立编写发布者-订阅者、服务端-客户端、动作服务器-客户端代码。

ROS2 三大通信机制 话题 (Topic) 发布者 (Publisher) 订阅者 (Subscriber) 服务 (Service) 服务端 (Server) 客户端 (Client) 动作 (Action) 动作服务器 动作客户端 三种通信方式对比 话题:持续广播,一对多,无反馈 服务:一问一答,同步调用,有反馈 动作:任务下发,持续反馈,可取消

4.1 编写发布者与订阅者

先从最基础的话题通信开始。我习惯把发布者想象成「说话的人」,订阅者就是「听的人」。它们之间通过话题名来匹配,消息类型必须一致。

4.1.1 创建功能包

我们先创建一个专门用来练习的功能包:

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake learning_communication
cd learning_communication
mkdir -p src

如果你用 Python,可以这样:

ros2 pkg create --build-type ament_python learning_communication_py

4.1.2 编写发布者(C++ 版)

在 src/ 目录下创建 talker.cpp:

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

class Talker : public rclcpp::Node
{
public:
  Talker() : Node("talker_node")
  {
    publisher_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);
    timer_ = this->create_wall_timer(
      std::chrono::seconds(1),
      std::bind(&Talker::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(), "发布: '%s'", message.data.c_str());
    publisher_->publish(message);
  }

  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
  rclcpp::TimerBase::SharedPtr timer_;
  int count_ = 0;
};

int main(int argc, char *argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<Talker>());
  rclcpp::shutdown();
  return 0;
}

嗯,这里要注意:create_publisher 的第二个参数是队列大小。我在项目中遇到过队列满了导致丢消息的情况,后来养成了根据数据频率合理设置队列大小的习惯。高频数据(比如激光雷达)建议设大一点,低频数据(比如状态信息)设小一点就行。

4.1.3 编写订阅者(C++ 版)

在 src/ 目录下创建 listener.cpp:

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

class Listener : public rclcpp::Node
{
public:
  Listener() : Node("listener_node")
  {
    subscription_ = this->create_subscription<std_msgs::msg::String>(
      "chatter", 10,
      std::bind(&Listener::topic_callback, this, std::placeholders::_1));
  }

private:
  void topic_callback(const std_msgs::msg::String::SharedPtr msg)
  {
    RCLCPP_INFO(this->get_logger(), "收到: '%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<Listener>());
  rclcpp::shutdown();
  return 0;
}

4.1.4 配置 CMakeLists.txt

别忘了在 CMakeLists.txt 中添加编译规则:

add_executable(talker src/talker.cpp)
ament_target_dependencies(talker rclcpp std_msgs)

add_executable(listener src/listener.cpp)
ament_target_dependencies(listener rclcpp std_msgs)

install(TARGETS
  talker
  listener
  DESTINATION lib/${PROJECT_NAME})

4.1.5 编译与测试

cd ~/ros2_ws
colcon build --packages-select learning_communication
source install/setup.bash

# 终端1:运行发布者
ros2 run learning_communication talker

# 终端2:运行订阅者
ros2 run learning_communication listener

你会看到发布者每秒发一条消息,订阅者实时收到。这就是最基础的话题通信。

小技巧:用 ros2 topic echo /chatter 也能直接看话题内容,适合调试时快速验证。

4.2 编写服务端与客户端

话题是「广播」,服务就是「点对点」。我举个例子:你让机器人打开灯,它打开后告诉你「好了」。这就是一次服务调用。

4.2.1 创建服务接口

先定义一个服务接口。在功能包中创建 srv/AddTwoInts.srv:

int64 a
int64 b
---
int64 sum

然后在 CMakeLists.txt 中添加:

rosidl_generate_interfaces(${PROJECT_NAME}
  "srv/AddTwoInts.srv"
)

4.2.2 编写服务端(C++ 版)

#include "rclcpp/rclcpp.hpp"
#include "learning_communication/srv/add_two_ints.hpp"

class ServerNode : public rclcpp::Node
{
public:
  ServerNode() : Node("server_node")
  {
    service_ = this->create_service<learning_communication::srv::AddTwoInts>(
      "add_two_ints",
      std::bind(&ServerNode::handle_service, this,
                std::placeholders::_1, std::placeholders::_2));
  }

private:
  void handle_service(
    const std::shared_ptr<learning_communication::srv::AddTwoInts::Request> request,
    std::shared_ptr<learning_communication::srv::AddTwoInts::Response> response)
  {
    response->sum = request->a + request->b;
    RCLCPP_INFO(this->get_logger(), "收到请求: %ld + %ld = %ld",
                request->a, request->b, response->sum);
  }

  rclcpp::Service<learning_communication::srv::AddTwoInts>::SharedPtr service_;
};

4.2.3 编写客户端(C++ 版)

#include "rclcpp/rclcpp.hpp"
#include "learning_communication/srv/add_two_ints.hpp"

class ClientNode : public rclcpp::Node
{
public:
  ClientNode() : Node("client_node")
  {
    client_ = this->create_client<learning_communication::srv::AddTwoInts>("add_two_ints");
  }

  void send_request(int64_t a, int64_t b)
  {
    while (!client_->wait_for_service(std::chrono::seconds(1)))
    {
      RCLCPP_WARN(this->get_logger(), "等待服务端上线...");
    }

    auto request = std::make_shared<learning_communication::srv::AddTwoInts::Request>();
    request->a = a;
    request->b = b;

    auto future = client_->async_send_request(request);
    // 这里可以用 spin_until_future_complete 等待结果
  }
};

避坑指南:我曾经在客户端调用服务时忘记检查服务是否在线,结果程序直接崩溃。记住:调用服务前一定要用 wait_for_service 确认服务端已就绪。

4.3 编写动作服务器与客户端

动作是 ROS2 中最复杂的通信方式,但也是最强大的。它适合「派任务」的场景:你让机器人导航到某个点,它一路上给你汇报进度,到了告诉你「完成」,中途你还可以取消。

4.3.1 创建动作接口

创建 action/Fibonacci.action:

int32 order
---
int32[] sequence
---
int32[] partial_sequence

在 CMakeLists.txt 中添加:

rosidl_generate_interfaces(${PROJECT_NAME}
  "srv/AddTwoInts.srv"
  "action/Fibonacci.action"
)

4.3.2 编写动作服务器

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "learning_communication/action/fibonacci.hpp"

class ActionServer : public rclcpp::Node
{
public:
  using Fibonacci = learning_communication::action::Fibonacci;
  using GoalHandle = rclcpp_action::ServerGoalHandle<Fibonacci>;

  ActionServer() : Node("action_server")
  {
    action_server_ = rclcpp_action::create_server<Fibonacci>(
      this,
      "fibonacci",
      std::bind(&ActionServer::handle_goal, this, std::placeholders::_1, std::placeholders::_2),
      std::bind(&ActionServer::handle_cancel, this, std::placeholders::_1),
      std::bind(&ActionServer::handle_accepted, this, std::placeholders::_1));
  }

private:
  rclcpp_action::Server<Fibonacci>::SharedPtr action_server_;
};

动作服务器的回调有三个:接收目标、处理取消、接受目标后执行。我个人觉得这是 ROS2 通信中最优雅的设计——把任务的生命周期管理得清清楚楚。

4.3.3 编写动作客户端

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "learning_communication/action/fibonacci.hpp"

class ActionClient : public rclcpp::Node
{
public:
  using Fibonacci = learning_communication::action::Fibonacci;
  using GoalHandle = rclcpp_action::ClientGoalHandle<Fibonacci>;

  ActionClient() : Node("action_client")
  {
    client_ = rclcpp_action::create_client<Fibonacci>(this, "fibonacci");
  }

  void send_goal(int order)
  {
    while (!client_->wait_for_action_server(std::chrono::seconds(1)))
    {
      RCLCPP_WARN(this->get_logger(), "等待动作服务器...");
    }

    auto goal = Fibonacci::Goal();
    goal.order = order;

    auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions();
    send_goal_options.goal_response_callback =
      std::bind(&ActionClient::goal_response_callback, this, std::placeholders::_1);
    send_goal_options.feedback_callback =
      std::bind(&ActionClient::feedback_callback, this,
                std::placeholders::_1, std::placeholders::_2);
    send_goal_options.result_callback =
      std::bind(&ActionClient::result_callback, this, std::placeholders::_1);

    client_->async_send_goal(goal, send_goal_options);
  }

private:
  rclcpp_action::Client<Fibonacci>::SharedPtr client_;
};

关键点:动作客户端有三个回调——goal_response(服务端是否接受任务)、feedback(进度反馈)、result(最终结果)。这三个回调覆盖了任务从下发到完成的完整生命周期。

4.4 三种通信方式对比总结

特性 话题(Topic) 服务(Service) 动作(Action)
通信模式 发布-订阅 请求-响应 目标-反馈-结果
是否持续 持续广播 一次性 持续到完成
是否有反馈 无 只有最终响应 有中间进度反馈
是否可取消 不适用 不适用 支持取消
典型场景 传感器数据、状态信息 开关控制、查询参数 导航、机械臂运动

写到这里,我想起刚学 ROS2 时的一个感悟:别急着写代码,先想清楚你的场景适合哪种通信方式。选对了,后面写代码就是水到渠成的事。

这一章的代码量不小,但都是基本功。你可以在我的 GitHub 仓库找到完整示例代码,建议自己动手敲一遍——只有亲手写过,才能真正理解这些通信机制的精髓。