4. ROS2 通信机制实战:发布者与订阅者、服务端与客户端、动作服务器与客户端
ROS2 的核心,说白了就是节点之间的通信。你想想看,一个机器人上有激光雷达、摄像头、电机驱动、导航算法……这么多模块,总得有个办法让它们互相说话吧?
ROS2 提供了三种通信方式:话题(Topic)、服务(Service)和动作(Action)。我个人习惯把它们比作三种不同的对话场景:
- 话题:像广播电台。发布者一直往外发,订阅者想听就听。适合传感器数据这种持续更新的信息。
- 服务:像打电话。你问一句,对方答一句,一次性的。适合开关、查询这种短平快的操作。
- 动作:像派任务。你下达指令后,对方会持续汇报进度,最后告诉你结果。适合导航、机械臂运动这种耗时较长的任务。
这一章,我们就手写这三种通信方式的代码。我会用 C++ 和 Python 各写一遍,你挑自己顺手的学就行。
本章核心目标:掌握 ROS2 三种通信机制的编程范式,能独立编写发布者-订阅者、服务端-客户端、动作服务器-客户端代码。
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 仓库找到完整示例代码,建议自己动手敲一遍——只有亲手写过,才能真正理解这些通信机制的精髓。