17 KiB
20 · Topic 深度:pub/sub(完全指南)
目标:吃透 ROS2 pub/sub,涵盖消息定义、QoS、跨语言互通、常见坑,学完直接写工业级代码。
目录
- 1. 通信模型
- 2. 消息类型
- 3. Publisher API(Python / C++)
- 4. Subscriber API(Python / C++)
- 5. QoS 详解(必备)
- 6. 跨语言互通(核心特性)
- 7. 完整实战:写一个传感器数据流
- 8. 调试命令大全
- 9. 常见坑 + 解决方案
- 10. 在本仓库里跑
- 11. 进阶:可靠通信 / 录制 / 跨机
1. 通信模型
1.1 一句话
Topic 是 异步、多对多、单向的发布订阅通道。
Publisher ──publish()──> Topic (chatter) ──callback(msg)──> Subscriber
────────────────────────────────────────────────────
异步,非阻塞 多对多,单向 事件循环触发
1.2 三种通信对比
| 维度 | Topic | Service | Action |
|---|---|---|---|
| 同步 | 异步 | 同步(阻塞) | 异步(long-running) |
| 方向 | 单向 pub→sub | 双向 req/resp | 双向 goal/fb/result |
| 一对多 | ✅ | ❌ 一对一 | ❌ 一对一 |
| 取消 | N/A | ❌ | ✅ |
| 进度反馈 | N/A | ❌ | ✅ |
| 适合 | 传感器流、状态 | 短查询 | 长任务 |
1.3 何时用 Topic
✅ 适合:
- 周期性传感器数据(相机、IMU、激光雷达、关节状态)
- 状态发布(机器人位置、电池电量)
- 持续监控数据(诊断、log)
❌ 不适合:
- 一次性 req/resp(用 Service)
- 长任务(用 Action)
- 需要反馈进度(用 Action)
2. 消息类型
2.1 标准消息(ROS2 自带)
| 包 | 常用消息 |
|---|---|
std_msgs |
String, Bool, Int32, Float64, Header |
geometry_msgs |
Point, Quaternion, Pose, Twist, Transform, Vector3 |
sensor_msgs |
Image, JointState, Imu, PointCloud2, CameraInfo, LaserScan |
nav_msgs |
Odometry, Path, OccupancyGrid |
trajectory_msgs |
JointTrajectory, JointTrajectoryPoint |
2.2 消息结构示例
# sensor_msgs/msg/Image
msg = sensor_msgs.msg.Image()
msg.header.stamp = node.get_clock().now().to_msg()
msg.header.frame_id = "camera_optical_frame"
msg.height = 480
msg.width = 640
msg.encoding = "bgr8" # OpenCV 默认
msg.is_bigendian = 0
msg.step = 640 * 3 # width * bytes_per_pixel
msg.data = bgr_array.tobytes() # numpy → bytes
2.3 自定义消息(本仓库不用)
如果需要自定义消息:
- 在包内
msg/MyMsg.msg定义字段 package.xml加<build_depend>rosidl_default_generators</build_depend>+<exec_depend>rosidl_default_runtime</exec_depend>CMakeLists.txt调rosidl_generate_interfaces(...)colcon build后 Python/C++ 自动生成类
本仓库只使用标准消息,简化学习曲线。
3. Publisher API(Python / C++)
3.1 Python
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Talker(Node):
def __init__(self):
super().__init__('talker_py')
# 1) 声明参数
self.declare_parameter('period_ms', 500)
self.declare_parameter('topic', 'chatter')
# 2) 取参数
period = self.get_parameter('period_ms').value
topic = self.get_parameter('topic').value
# 3) 创建 Publisher
# create_publisher(msg_type, topic, qos_depth)
self.publisher_ = self.create_publisher(String, topic, 10)
# 4) 创建定时器,周期性 publish
self.timer_ = self.create_timer(period / 1000.0, self.timer_callback)
def timer_callback(self):
msg = String()
msg.data = f'Hello, count={self.count}'
self.publisher_.publish(msg) # 异步,不等
self.count += 1
def main():
rclpy.init()
node = Talker()
try:
rclpy.spin(node) # 进入事件循环
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
3.2 C++
#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class Talker : public rclcpp::Node {
public:
Talker() : rclcpp::Node("talker_cpp"), count_(0) {
// 1) 声明 + 读参数
this->declare_parameter<int>("period_ms", 500);
this->declare_parameter<std::string>("topic", "chatter");
int period = this->get_parameter("period_ms").as_int();
std::string topic = this->get_parameter("topic").as_string();
// 2) 创建 Publisher
publisher_ = this->create_publisher<std_msgs::msg::String>(topic, 10);
// 3) 定时器
timer_ = this->create_wall_timer(
std::chrono::milliseconds(period),
std::bind(&Talker::timer_callback, this));
}
private:
void timer_callback() {
auto msg = std_msgs::msg::String();
msg.data = "Hello from C++, seq=" + std::to_string(count_++);
publisher_->publish(msg);
}
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
size_t count_;
};
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<Talker>());
rclcpp::shutdown();
return 0;
}
3.3 关键 API 速查
| Python | C++ | 用途 |
|---|---|---|
create_publisher(MsgType, name, depth) |
create_publisher<MsgType>(name, depth) |
创建 publisher |
publisher.publish(msg) |
publisher->publish(msg) |
异步发消息 |
publisher.get_subscription_count() |
同 | 看有几个订阅者 |
destroy_publisher() |
同 | 销毁 |
4. Subscriber API(Python / C++)
4.1 Python
class Listener(Node):
def __init__(self):
super().__init__('listener_py')
self.declare_parameter('topic', 'chatter')
topic = self.get_parameter('topic').value
# create_subscription(msg_type, topic, callback, qos_depth)
# callback 签名: callback(msg)
self.subscription = self.create_subscription(
String, topic, self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info(f'recv: "{msg.data}"')
# 这里做处理:解析、入队、下发指令、可视化等
4.2 C++
class Listener : public rclcpp::Node {
public:
Listener() : rclcpp::Node("listener_cpp") {
this->declare_parameter<std::string>("topic", "chatter");
std::string topic = this->get_parameter("topic").as_string();
// create_subscription<T>(topic, depth, callback)
// callback 签名: [](const T::SharedPtr msg) { ... }
subscription_ = this->create_subscription<std_msgs::msg::String>(
topic, 10,
[this](const std_msgs::msg::String::SharedPtr msg) {
RCLCPP_INFO(this->get_logger(), "recv: \"%s\"", msg->data.c_str());
});
}
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
4.3 关键 API 速查
| Python | C++ | 用途 |
|---|---|---|
create_subscription(MsgType, name, cb, depth) |
create_subscription<T>(name, depth, cb) |
创建订阅者 |
callback 签名: cb(msg) |
[](const T::SharedPtr msg) { ... } |
消息回调 |
4.4 重要原则
- 回调里别阻塞:回调在主线程里跑,长时间阻塞会让 timer / 其他 callback 卡死
- 回调里别抛异常:rclcpp 会捕获但仍可能崩
- 复杂处理入队:把消息放到 queue,另起线程消费
5. QoS 详解(必备)
5.1 五个维度
| 维度 | 取值 | 默认 | 含义 |
|---|---|---|---|
| Reliability | RELIABLE / BEST_EFFORT | RELIABLE | 必须投递 / 丢一帧无所谓 |
| History | KEEP_LAST(N) / KEEP_ALL | KEEP_LAST(10) | 队列策略 |
| Durability | VOLATILE / TRANSIENT_LOCAL | VOLATILE | 晚订阅者是否收旧数据 |
| Deadline | Duration | ∞ | 最长多久发一次 |
| Lifespan | Duration | ∞ | 多旧的消息失效 |
5.2 常用组合
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
# 视频流:丢一帧没关系,要最新
sensor_qos = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=1
)
# 控制指令:必须到达
control_qos = QoSProfile(
reliability=ReliabilityPolicy.RELIABLE,
history=HistoryPolicy.KEEP_LAST,
depth=10
)
5.3 兼容规则(关键!)
| Publisher | Subscriber | 结果 |
|---|---|---|
| RELIABLE | RELIABLE | ✅ |
| BEST_EFFORT | BEST_EFFORT | ✅ |
| RELIABLE | BEST_EFFORT | ❌ (sub 不发 ACK,pub 报 QoS incompatible) |
| BEST_EFFORT | RELIABLE | ✅ (sub 容忍丢) |
报错样例:
[WARN] ... New subscription discovered on this topic with incompatible QoS ...
怎么查: ros2 topic info /topic -v 看 pub/sub 各自 QoS。
5.4 本仓库 QoS 策略
所有 demo 都用默认 QoS(RELIABLE + KEEP_LAST(10))。
- 优点:跨语言互通零障碍
- 缺点:高频场景需调优
实战: 视频流改 BEST_EFFORT + depth=1;关节控制用 RELIABLE + depth=1。
6. 跨语言互通(核心特性)
6.1 为什么能互通
ROS2 用 DDS 做底层,Python / C++ / 其他语言只是同一消息的不同"视图"。 消息定义在 .msg/.srv/.action 里,所有语言按这个定义自动生成代码。
6.2 互通条件
- ✅ 消息类型一致(
std_msgs/String的 Python/C++ 字段名都是data) - ✅ Topic 名一致
- ✅ QoS 兼容
- ✅ ROS_DOMAIN_ID 一致(
ROS_DOMAIN_ID环境变量)
6.3 验证互通
# 启 4 节点(pubsub_launch.py)
ros2 launch bringup pubsub_launch.py
# 看 /chatter 的 pub/sub 列表
ros2 topic info /chatter -v
预期:
Publication count: 2
Subscription count: 2
Node name: listener_py Node namespace: /
Publisher count: 0
Node name: talker_py Node namespace: /
Publisher count: 1
Node name: listener_cpp Node namespace: /
Publisher count: 0
Node name: talker_cpp Node namespace: /
Publisher count: 1
看到 talker_py + talker_cpp 两个 publisher, listener_py + listener_cpp 两个 subscriber。
6.4 看跨语言消息流
# 终端 1
ros2 launch bringup pubsub_launch.py
# 终端 2
ros2 topic echo /chatter
# 预期:
# data: 'Hello from PY, seq=42'
# data: 'Hello from C++, seq=12'
# data: 'Hello from PY, seq=43'
# data: 'Hello from C++, seq=13'
Python talker 发 → C++ listener 收到 ✅ C++ talker 发 → Python listener 收到 ✅
端到端日志: docker/bringup_e2e.log
7. 完整实战:写一个传感器数据流
假设你做一个激光雷达节点:
7.1 Publisher(传感器端)
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
import random
class FakeLidar(Node):
def __init__(self):
super().__init__('fake_lidar')
self.declare_parameter('rate_hz', 10)
rate = self.get_parameter('rate_hz').value
self.pub_ = self.create_publisher(
LaserScan, '/scan', 10)
self.timer_ = self.create_timer(1.0/rate, self.tick)
def tick(self):
scan = LaserScan()
scan.header.stamp = self.get_clock().now().to_msg()
scan.header.frame_id = 'laser_frame'
scan.angle_min = -3.14159
scan.angle_max = 3.14159
scan.angle_increment = 0.01
scan.time_increment = 0.0
scan.range_min = 0.05
scan.range_max = 30.0
scan.ranges = [random.uniform(0.5, 5.0) for _ in range(629)]
self.pub_.publish(scan)
7.2 Subscriber(消费端)
class LidarProcessor(Node):
def __init__(self):
super().__init__('lidar_processor')
self.sub_ = self.create_subscription(
LaserScan, '/scan', self.cb, 10)
def cb(self, msg):
# 找最近障碍
nearest = min(msg.ranges)
self.get_logger().info(f'nearest obstacle: {nearest:.2f}m')
# 这里可以做:避障规划、点云处理、可视化
7.3 完整 run
# 终端 1:传感器
ros2 run my_pkg fake_lidar
# 终端 2:处理
ros2 run my_pkg lidar_processor
# 终端 3:验证
ros2 topic hz /scan # 10 Hz
ros2 topic echo /scan # 看数据(不要全打印,会很乱)
8. 调试命令大全
# 列所有 topic
ros2 topic list
# 看 topic 元数据(类型 + pub/sub + QoS)
ros2 topic info /chatter -v
# 实时打印消息内容
ros2 topic echo /chatter
# 测频率(平均 / 最小 / 最大 / 标准差)
ros2 topic hz /chatter
# 测带宽(每秒多少 KB)
ros2 topic bw /chatter
# 发一条测试消息
ros2 topic pub /chatter std_msgs/String "{data: 'hello'}" --once
# 录包
ros2 bag record /chatter -o my_bag
# 录包回放
ros2 bag play my_bag
# 看当前所有节点
ros2 node list
# 看节点发布的 topic
ros2 node info /talker_py
9. 常见坑 + 解决方案
9.1 收不到消息(最常见)
症状: ros2 topic echo 没输出,但 ros2 topic list 看得到。
排查步骤:
# 1) 看 pub/sub 列表
ros2 topic info /topic -v
# 2) 看 QoS 是否兼容
# 如果 QoS incompatible,WARN 日志会打印
# 3) 看节点是否活着
ros2 node list
常见原因:
- ❌ 节点名重复:
Node 'x' already exists→ 改名或加 namespace - ❌ Topic 名拼写错误:大小写、
/区分 - ❌ QoS 不兼容:改成 default 或两边匹配
- ❌ 消息类型不匹配:Publisher 是
std_msgs/String,Subscriber 是std_msgs/Int32→ 静默不匹配 - ❌ Publisher 还没起来:等 1-2s DDS discovery
9.2 Docker 内 localhost 不互通
症状: 两个容器里节点互相看不到。
解决: 用 network_mode: host(本仓库已配),或 ROS_STATIC_PEERS 单播。
9.3 DDS QoS 兼容性错
报错:
[WARN] ... Incompatible QoS ... (PolicyKind=RMW_QOS_POLICY_RELIABILITY)
解决: 双方 Reliability 一致。
from rclpy.qos import QoSProfile, ReliabilityPolicy
qos = QoSProfile(reliability=ReliabilityPolicy.BEST_EFFORT, depth=1)
self.pub_ = self.create_publisher(String, 'topic', qos)
9.4 Cyclone DDS 没装导致 build 失败
CMake 错误:
Could not find ROS middleware implementation 'rmw_cyclonedds_cpp'
解决:
- 不要设
RMW_IMPLEMENTATION=rmw_cyclonedds_cpp(本仓库默认 fastdds) - 切 RMW 时务必
rm -rf build/ install/ log/
9.5 Callback 阻塞导致节点"卡死"
症状: 节点启动后什么都不做,其他 timer / callback 也不响应。
原因: callback 里跑同步阻塞代码(long I/O、time.sleep)。
解决: 用 MultiThreadedExecutor,callback 里只入队 + 另起线程处理。
from rclpy.executors import MultiThreadedExecutor
exec_ = MultiThreadedExecutor(num_threads=4)
exec_.add_node(node)
exec_.spin()
10. 在本仓库里跑
10.1 启动 4 节点跨语言 demo
docker exec ros2_dev bash -lc "cd /root/ros2_ws && source install/setup.bash && ros2 launch bringup pubsub_launch.py"
10.2 源码位置
- Python pub:
src/py_pubsub/py_pubsub/publisher_member_function.py - Python sub:
src/py_pubsub/py_pubsub/subscriber_member_function.py - C++ pub:
src/cpp_pubsub/src/publisher_member_function.cpp - C++ sub:
src/cpp_pubsub/src/subscriber_member_function.cpp - launch:
src/bringup/launch/pubsub_launch.py
10.3 端到端日志
docker/bringup_e2e.log — 跨语言互通验证。
11. 进阶:可靠通信 / 录制 / 跨机
11.1 Reliable 通信设置
# 默认就是 RELIABLE,显式写:
from rclpy.qos import QoSProfile, ReliabilityPolicy
qos = QoSProfile(
reliability=ReliabilityPolicy.RELIABLE,
history=HistoryPolicy.KEEP_LAST,
depth=10
)
11.2 录制 + 回放(bag)
ros2 bag record -o my_bag /chatter /tf /joint_states
# 输出: my_bag_0.db3 + metadata.yaml
ros2 bag play my_bag --loop # --loop 循环回放
ros2 bag info my_bag # 看消息数 / 时间 / 类型
注意: 回放时,topic 真实 pub 也要在,否则 recorder 找不到对应 publisher。bag 不会保存节点,只保存消息。
11.3 跨机 DDS
默认走 UDP multicast,同网段自动发现。
跨子网: 用 unicast discovery。
PC:
export ROS_DISCOVERY_SERVER=192.168.1.20:11811
RDK X5: 启 discovery server:
ros2 run discovery_server discovery_server --address 0.0.0.0 --port 11811
跨 LAN + 防火墙: 用 ROS_STATIC_PEERS 静态发现。
详见 doc/100-embedded-deployment.md §5。
接下来读
| 主题 | 文档 |
|---|---|
| Service 深度 | 30-services.md |
| Action 深度 | 40-actions.md |
| TF2 坐标变换 | 50-tf2.md |
| 三机部署 | 100-embedded-deployment.md |
| 具身智能路径 | 99-embodied-ai.md |