
简介一份面向机器人操作系统初学者的ROS话题Topic入门代码示例包帮助读者理解节点间通过话题进行消息传递的核心机制。压缩包内共十一个文件包含四个C源文件与四个Python脚本并附带CMakeLists.txt构建文件、package.xml包描述以及自定义Person.msg消息定义整体仅为10KB轻量易读。示例完整覆盖了发布者与订阅者的创建、话题消息的周期发布与回调订阅以及自定义消息类型的使用内容涉及速度发布、位姿订阅、人物消息传递等多个典型场景代码内附有详细注释便于循序渐进地动手验证。目前已有360人学习下载借助这些带注释的代码可快速掌握ROS话题通信的开发流程从最简单的节点通信延伸到自定义消息与多节点协作为后续学习服务、参数服务器等高级特性打下扎实基础。1. 一份 ROS Topic 示例包能拆出多少通信细节刚接触 ROS 的人通常会被rostopic list、rqt_graph这些命令带着走能跑起来、能看到数据在流动就以为学会了话题通信。但真到自己写包的时候卡住的往往是queue_size填多少、自定义消息为什么编不过、Python 版订阅器为什么收不到数据这类具体问题。learning_ros_topic.zip这个包把 C 和 Python 的发布订阅组件放在一起还附带一份Person.msg自定义消息正好用来把话题通信从「能跑通」推进到「明白为什么这么写」。本文按消息定义、C 实现、Python 实现、调试验证这条线拆开讲新手能跟着目录一步步复现写过几年 ROS 的人也能从中对队列背压和消息生成机制做一次校准。2. 发布订阅模型与 queue_size 背压从 velocity_publisher 和 pose_subscriber 说起2.1 节点、话题与消息一组最小拓扑的组成要素话题通信的核心是三个要素节点、话题、消息。发布者节点把数据写到某个话题上订阅者节点从同一个话题上读双方通过话题名和消息类型完成匹配不需要知道对方节点的地址和实现方式。这种解耦正是 ROS 节点可以独立启动、独立崩溃、独立重启的原因。示例包里的velocity_publisher.cpp和pose_subscriber.cpp构成了一组最小拓扑前者以固定频率发布geometry_msgs/Twist速度指令后者订阅同一个话题并解析线速度和角速度。这是典型的「控制指令下行」模型理解它之后传感器数据上行、状态上报等场景只是消息类型不同拓扑结构完全一致。rostopic list显示的是当前 ROS 环境中所有已注册的话题名rostopic info /turtle1/cmd_vel则能查看话题的消息类型、发布者数量和订阅者数量。注意这里存在一个新手常见的误解ros master 并不是消息数据的转发中心它只负责节点之间的注册发现真正的数据是发布者与订阅者之间点对点传输的。所以即使 roscore 中途重启已经建立连接的两个节点仍然可能继续通信只有新节点加入时才需要重新查询。2.2 C 发布者advertise 的参数与发布循环velocity_publisher.cpp给出了一个标准的发布者写法核心只有三步初始化节点、advertise注册发布者、循环publish。#include ros/ros.h #include geometry_msgs/Twist.h int main(int argc, char **argv) { ros::init(argc, argv, velocity_publisher); ros::NodeHandle n; ros::Publisher vel_pub n.advertisegeometry_msgs::Twist(/turtle1/cmd_vel, 10); ros::Rate loop_rate(10); while (ros::ok()) { geometry_msgs::Twist vel_msg; vel_msg.linear.x 0.5; vel_msg.linear.y 0.0; vel_msg.linear.z 0.0; vel_msg.angular.z 0.2; vel_pub.publish(vel_msg); ROS_INFO(Publish turtle velocity command: linear.x %f, angular.z %f, vel_msg.linear.x, vel_msg.angular.z); loop_rate.sleep(); } return 0; }advertise消息类型(话题名, 队列大小)返回一个Publisher对象之后对它的每次publish都会把消息交给 roscpp 内部的发送队列。这里最容易被忽略的参数是队列大小10当对端订阅者处理不过来时消息不会立刻丢弃而是先在发送队列里排队队列满了才开始丢。这不是一个「设大就稳」的参数设太大意味着内存中积压的消息越多实时性越差对机器人速度指令这种对时效敏感的数据积压反而比丢弃危害更大所以10这种量级是合理的默认选择。ros::Rate loop_rate(10)让循环以 10Hz 的频率运行loop_rate.sleep()会根据每轮循环实际消耗的时间自动补齐剩余等待保证发布频率稳定。需要注意ros::ok()的判断当收到 SIGINT 或 ros master 关闭时它返回 false循环退出前如果希望把队列里残留的消息发完可以调用ros::shutdown()后再 return。2.3 C 订阅者回调线程与 ros::spin 的调度关系pose_subscriber.cpp展示的是接收端如何工作它依赖回调函数而非主动去读消息这是 ROS 事件驱动的核心所在。#include ros/ros.h #include turtlesim/Pose.h void poseCallback(const turtlesim::Pose::ConstPtr msg) { ROS_INFO(Turtle pose: x %f, y %f, theta %f, msg-x, msg-y, msg-theta); } int main(int argc, char **argv) { ros::init(argc, argv, pose_subscriber); ros::NodeHandle n; ros::Subscriber pose_sub n.subscribe(/turtle1/pose, 10, poseCallback); ros::spin(); return 0; }n.subscribe消息类型(话题名, 队列大小, 回调函数)注册订阅者每收到一帧消息roscpp 就把消息对象放入订阅回调队列然后由ros::spin()所在的线程逐个取出并调用poseCallback。所以ros::spin()的执行位置决定了回调在哪里执行单线程写法的回调是在主循环里被调用的回调函数耗时过久就会阻塞后续消息的处理积压到队列上限时roscpp 会选择丢帧。对位姿、点云这类高频消息回调里只做轻量打印或数据拷贝把耗时计算放到其他线程是常见的优化方向也是多线程AsyncSpinner存在的理由。对比发布端的ros::Rate循环订阅端没有显式的频率控制数据到达的节奏完全由发布方决定。poseCallback(const turtlesim::Pose::ConstPtr msg)使用ConstPtr会让 roscpp 在内存中复用同一个消息缓冲区省去每帧消息的拷贝开销这也是官方示例与自定义节点最值得对齐的写法差异之一。3. 自定义 Person.msg从字段定义到 C 头文件生成3.1 Person.msg 字段设计与消息类型映射规则std_msgs和geometry_msgs覆盖了数值、位姿等常见数据但真实项目几乎都会定义业务相关的自定义消息。示例中的Person.msg展示了最典型的做法用几个基础类型组合出一个可复用的结构体供多个节点共享。string name int32 age float32 height float64 weight uint8 gender消息字段每行格式是类型 字段名。类型来源于 ROS 内置的消息类型或者你已经引入的其他包而字段名建议与 Python 属性风格保持一致因为 C 生成的头文件会直接把它作为成员变量名。定义完成后需要知道一个关键映射规则ROS 的 .msg 会被编译成各语言原生的数据类型而不是在运行时解析文本。ROS msg 字段类型C 生成类型头文件成员Python 生成类型int32int32_tintfloat32floatfloatfloat64doublefloatstringstd::stringstruint8uint8_tint这张表解释了为什么 C 侧msg-age可以直接参与算术运算以及 Python 侧from learning_topic.msg import Person得到的类是生成代码的关键——这些头文件和 Python 模块都由catkin_make阶段自动生成而不是手写。3.2 CMakeLists.txt 与 package.xml 的配置顺序很多人在自定义消息这一步编译失败问题往往不在 .msg 文件本身而在于 CMakeLists.txt 的配置顺序和依赖声明不完整。一个最小可用的配置如下find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation ) add_message_files( FILES Person.msg ) generate_messages( DEPENDENCIES std_msgs ) catkin_package( CATKIN_DEPENDS message_runtime )find_package里必须显式包含message_generation它是把 .msg 翻译成目标语言代码的编译器generate_messages的DEPENDENCIES声明本消息引用了哪些其他包的消息类型这里Person.msg只用了原生类型填std_msgs即可catkin_package中的message_runtime则是给下游包用的链接提示让依赖本包的其他节点知道运行时需要消息支持。package.xml 侧对应的改动是声明message_generation作为构建依赖、message_runtime作为运行依赖build_dependmessage_generation/build_depend exec_dependmessage_runtime/exec_depend这两行漏掉任何一个在别的机器上rosdep install或重新构建时都会出现找不到消息定义的情况。构建目录下的devel/include/learning_topic/Person.h就是由add_message_files触发生成的产物看到这个文件存在基本可以确认消息定义环节没有问题。3.3 在 C 节点里使用自定义消息include 路径与消息对象person_publisher.cpp和person_subscriber.cpp的价值在于演示了自定义消息在 C 侧的使用方式它与使用geometry_msgs的差别只集中在 include 和 CMake 依赖两处其余逻辑完全一致。#include ros/ros.h #include learning_topic/Person.h int main(int argc, char **argv) { ros::init(argc, argv, person_publisher); ros::NodeHandle n; ros::Publisher person_pub n.advertiselearning_topic::Person(/person_info, 10); ros::Rate loop_rate(10); while (ros::ok()) { learning_topic::Person person_msg; person_msg.name Tom; person_msg.age 18; person_msg.height 1.75; person_msg.weight 65.0; person_msg.gender 1; person_pub.publish(person_msg); loop_rate.sleep(); } return 0; }#include learning_topic/Person.h采用包名/消息名.h的路径方式对应的生成头文件就在devel/include/learning_topic/下。消息类型写作learning_topic::Person这是命名空间形式与头文件的包名保持一致。若编译时提示找不到头文件优先检查构建产物是否存在其次是 CMakeLists 的add_message_files是否真的执行过大多数情况下是消息生成环节没有生效而不是代码本身的问题。发布自定义消息的节点CMakeLists 需要额外增加生成消息的依赖确保消息头文件先于节点代码编译add_executable(person_publisher src/person_publisher.cpp) target_link_libraries(person_publisher ${catkin_LIBRARIES}) add_dependencies(person_publisher ${PROJECT_NAME}_generate_messages_cpp)add_dependencies控制编译顺序它声明了 person_publisher 这个目标要等 Person.h 生成之后才开始编译。包含自定义消息的节点不写这行在 catkin 并行构建时会出现间歇性「文件不存在」的编译错误且表现极不稳定新增了消息类型后尤其明显。订阅端person_subscriber.cpp的代码结构与 2.3 节完全一致回调签名改为void personCallback(const learning_topic::Person::ConstPtr msg)即可订阅的话题名必须与发布端完全一致话题名拼写错误是运行时最常见的静默故障之一——发布端在发布没人订阅的数据订阅端在等待永远不会到达的数据。4. Python 端实现与逐层验证rospy、rosrun 与 rostopic 三件套4.1 rospy 发布与订阅API 差异与同样要填的 queue_size同一份消息定义Python 侧的写法有明显差异。person_publisher.py和person_subscriber.py是快速验证自定义消息的最佳工具——不需要编译但要记得先source devel/setup.bash让 Python 能找到learning_topic.msg模块。#!/usr/bin/env python3 # -*- coding: utf-8 -*- import rospy from learning_topic.msg import Person def velocity_publisher(): rospy.init_node(person_publisher, anonymousTrue) pub rospy.Publisher(/person_info, Person, queue_size10) rate rospy.Rate(10) msg Person() msg.name Tom msg.age 18 msg.height 1.75 msg.weight 65.0 msg.gender 1 while not rospy.is_shutdown(): pub.publish(msg) rate.sleep() if __name__ __main__: try: velocity_publisher() except rospy.ROSInterruptException: pass订阅端#!/usr/bin/env python3 # -*- coding: utf-8 -*- import rospy from learning_topic.msg import Person def person_callback(msg): rospy.loginfo(person msg: name %s, age %d, height %.2f, msg.name, msg.age, msg.height) def person_subscriber(): rospy.init_node(person_subscriber, anonymousTrue) rospy.Subscriber(/person_info, Person, person_callback) rospy.spin() if __name__ __main__: person_subscriber()rospy.Publisher(话题名, 消息类, queue_size)的第三个参数是必填项在 ROS Noetic 之后版本中不写queue_size会直接告警这与 C 侧advertise的队列语义一致。rospy.spin()等价于 C 的ros::spin()阻塞主线程让 rospy 内部线程去调度回调。两处明显的差异值得注意一是发布端rospy.Rate(10).sleep()的回调机制没有 C 侧丰富二是 Python 侧通过from learning_topic.msg import Person导入消息类时如果 devel/setup.bash 未 source导入会在运行时抛出 ModuleNotFoundError而不是在编译期被拦截——这也是刚才强调环境变量的原因。4.2 用 launch 一次拉起多个节点多个节点手动开三个终端分别rosrun过于低效用 launch 文件把发布者、订阅者和必要节点统一管理是标准做法。launch node nameperson_publisher pkglearning_topic typeperson_publisher.py outputscreen/ node nameperson_subscriber pkglearning_topic typeperson_subscriber.py outputscreen/ /launch注意type字段填的是 scripts 目录下脚本的可执行文件名不是包名。launch 文件启动时会自动保证 roscore 已经在运行若没有则会自动拉起。outputscreen会把节点的标准输出打到当前终端方便观察日志去掉该项则日志会重定向到~/.ros/log/下。4.3 验证链路src 脚本权限、rostopic 查看话题与数据流Python 脚本最隐蔽的坑是执行权限。在 Ubuntu 上rosrun是按可执行文件来调用脚本的没有x权限会直接报 Permission denied。cd ~/catkin_ws/src/learning_topic/scripts chmod x person_publisher.py person_subscriber.py pose_subscriber.py cd ~/catkin_ws catkin_make source devel/setup.bash roslaunch learning_topic start.launch跑起来之后用下面几个命令逐步验证通信链路是否通畅命令作用输出观察点rostopic list查看当前所有话题应出现/person_inforostopic info /person_info查看话题的消息类型、发布者与订阅者数量Type 应显示learning_topic/Person发布者和订阅者各为 1rostopic echo /person_info打印话题上的实时数据流每行应有 name、age、height 等字段rostopic hz /person_info统计消息发布频率稳定在 9.x~10.x Hz 为正常顺序建议是 list 先确认话题存在info 确认消息类型正确echo 确认数据内容没有异常最后用 hz 确认发布频率符合预期。如果rostopic list里没有/person_info多半是发布节点没起来有话题但 echo 无数据则是发布端已死或话题名与订阅端不一致——后一种情况在rostopic info里能看到发布者数量为 0两行命令就能定位到故障侧效率远高于一直看日志。5. 进阶队列深度与慢订阅者对通信质量的影响话题通信的队列参数是学习 ROS 时最容易被忽略的进阶点。queue_size不只影响数据包在内存中的滞留数量它实际上充当了发布端和订阅端之间的缓冲池。发布频率远高于订阅处理速率时消息会在订阅端的回调队列里积压队列满之后rospy 默认丢弃最旧的消息C 侧则根据传输机制的不同丢弃策略略有差异。对于里程计、雷达数据这类「只关心最新状态」的话题丢旧消息反而是正确行为但对于需要完整数据流离线分析的场景就要考虑增大队列或改用专门的录制工具。验证队列溢出是否影响业务可以借助rostopic bw查看话题实时带宽观察它是否在消息积压时出现周期性抖动。带宽曲线不稳定基本可以判断订阅端处理速度存在瓶颈此时优先优化回调函数而不是贸然加大队列。验证消息是否丢失用rostopic echo加字段过滤会更直接。例如只关心 Person 消息里的 name 字段rostopic echo /person_info/name这条命令会在终端持续输出订阅到的每个 name 值。配合rostopic hz观察频率变化能快速判断是发布频率不足还是中间传输阶段在丢帧。两个命令的组合等价于给话题通信做了一次端到端的探针这比盯着节点日志更能反映真实通信链路质量。rostopic工具集里还有两个容易被低估的参数。rostopic echo --noarr在消息里含数组字段时只打印标量字段适合快速查看数值rostopic hz -w 20 /person_info则是在 20 秒窗口内统计平均频率比默认的瞬时频率更能反映趋势。这些技巧在处理自定义消息调试时非常实用比如在Person.msg扩展出数组字段后配合--noarr可以让 echo 输出不再被大批量数据刷屏。拉通整套验证后对 ROS 话题的理解应该从「发布者发布、订阅者订阅」升级到「链路质量可观测、队列策略可验证」的层次。遇到奇怪的数据延迟或丢帧先不要怀疑网络先用rostopic info确认两端都在再用hz和bw检查频率与带宽最后回到队列参数找答案这一套排查顺序能把大部分通信问题控制在几分钟内定位。本文还有配套的精品资源点击获取