3、ROS2通信机制(一):话题通信:Publisher与Subscriber编程实战(C++/Python)
聊到ROS2,绕不开的就是通信机制。而话题通信,说白了就是ROS2的“血管”——节点之间靠它来输送数据。我刚开始接触ROS2时,第一个实战就是写一个Publisher和Subscriber。那时候觉得,不就是发个消息、收个消息嘛,能有多难?结果一上手,各种坑就来了。今天咱们就把这块彻底捋清楚。
3.1 话题通信的核心逻辑
先看一张图,帮你快速建立整体认知。
这张图其实就说明白了:发布者往话题里扔数据,订阅者从话题里取数据。双方不需要知道对方是谁,只需要约定好话题名和消息类型就行。这种解耦设计,我在做多传感器融合项目时深有体会——加一个新传感器,完全不影响已有的控制节点。
3.2 准备工作:创建功能包
动手之前,先把环境搭好。我习惯用C++和Python各写一份,方便对比。
# 进入你的工作空间
cd ~/ros2_ws/src
# 创建C++功能包
ros2 pkg create --build-type ament_cmake cpp_topic_demo \
--dependencies rclcpp std_msgs
# 创建Python功能包
ros2 pkg create --build-type ament_python py_topic_demo \
--dependencies rclpy std_msgs
这里有个小细节:--dependencies参数会自动帮你把依赖写进package.xml和CMakeLists.txt里。我刚开始学的时候总忘了加,结果编译报错才回头补,浪费时间。
3.3 C++实现:Publisher与Subscriber
3.3.1 编写发布者节点
在cpp_topic_demo/src下新建talker.cpp:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<rclcpp::Node>("talker");
auto publisher = node->create_publisher<std_msgs::msg::String>("chatter", 10);
auto message = std_msgs::msg::String();
rclcpp::WallRate loop_rate(2); // 2Hz
while (rclcpp::ok())
{
message.data = "Hello ROS2: " + std::to_string(count);
RCLCPP_INFO(node->get_logger(), "发布: '%s'", message.data.c_str());
publisher->publish(message);
rclcpp::spin_some(node);
loop_rate.sleep();
}
rclcpp::shutdown();
return 0;
}
嗯,这里要注意create_publisher的第二个参数——队列大小。我见过有人设成1,结果消息稍微一多就丢包。设成10或100比较稳妥,具体看你的实时性要求。
3.3.2 编写订阅者节点
在cpp_topic_demo/src下新建listener.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;
}
回调函数是订阅者的灵魂。数据一来,它就被触发。我曾经在回调里做复杂计算,结果把整个节点卡死了。记住:回调里别干重活,收到消息后扔到队列里处理才是正解。
3.3.3 修改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}
)
3.4 Python实现:Publisher与Subscriber
3.4.1 编写发布者节点
在py_topic_demo/py_topic_demo下新建talker.py:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Talker(Node):
def __init__(self):
super().__init__('talker')
self.publisher_ = self.create_publisher(String, 'chatter', 10)
self.timer = self.create_timer(0.5, self.timer_callback)
self.count = 0
def timer_callback(self):
msg = String()
msg.data = 'Hello ROS2: %d' % self.count
self.get_logger().info('发布: "%s"' % msg.data)
self.publisher_.publish(msg)
self.count += 1
def main(args=None):
rclpy.init(args=args)
node = Talker()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
Python版用了定时器来循环发布,比C++的while循环更优雅。我个人习惯用Python做快速原型验证,C++做最终部署。
3.4.2 编写订阅者节点
在py_topic_demo/py_topic_demo下新建listener.py:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Listener(Node):
def __init__(self):
super().__init__('listener')
self.subscription = self.create_subscription(
String, 'chatter', self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info('收到: "%s"' % msg.data)
def main(args=None):
rclpy.init(args=args)
node = Listener()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
3.4.3 配置setup.py
entry_points={
'console_scripts': [
'talker = py_topic_demo.talker:main',
'listener = py_topic_demo.listener:main',
],
},
3.5 编译与运行
回到工作空间根目录:
cd ~/ros2_ws
colcon build
source install/setup.bash
然后开两个终端:
# 终端1:运行发布者
ros2 run cpp_topic_demo talker
# 或者Python版
ros2 run py_topic_demo talker
# 终端2:运行订阅者
ros2 run cpp_topic_demo listener
# 或者Python版
ros2 run py_topic_demo listener
看到消息在来回传递,说明你成功了。你想想看,C++写的发布者可以和Python写的订阅者通信,这就是ROS2的跨语言能力。
3.6 常用调试命令
| 命令 | 作用 | 示例 |
|---|---|---|
ros2 topic list |
列出所有活跃话题 | 查看当前有哪些话题 |
ros2 topic info /chatter |
查看话题详情 | 显示发布者/订阅者数量 |
ros2 topic echo /chatter |
实时打印话题数据 | 调试时看消息内容 |
ros2 topic hz /chatter |
查看发布频率 | 检查是否达到预期频率 |
ros2 topic bw /chatter |
查看带宽占用 | 排查网络瓶颈 |
ros2 topic echo确认数据格式对不对,再用ros2 topic hz检查频率。这两个命令能解决80%的通信问题。
3.7 避坑指南
- 话题名拼写错误:发布者和订阅者的话题名必须完全一致,大小写敏感。我有一回把
chatter写成了chater,排查了半小时。 - 消息类型不匹配:发布者用
String,订阅者用Int32,肯定连不上。用ros2 topic info可以快速检查。 - 忘记source环境:每次新开终端都要
source install/setup.bash,否则找不到你的节点。 - 回调函数阻塞:回调里别做耗时操作,否则会拖垮整个节点。实在需要,就用多线程或异步处理。
3.8 总结
话题通信是ROS2的基石。说白了,就是发布者往话题里写数据,订阅者从话题里读数据。C++性能好,适合底层驱动;Python开发快,适合原型验证。两者可以混用,这是ROS2的跨语言优势。
我个人建议:先跑通Python版,理解流程后再看C++版。遇到问题多用ros2 topic命令调试,别闷头改代码。嗯,今天就到这里,动手试试吧。
可可精品资料库