[TOC]

ROS Topic 通讯方式

一、Topic 通讯概念

在 ROS 系统中,多个 Node(节点)都需要向 ROS Master 注册。每个 Node 完成各自的功能逻辑,当节点之间需要传递数据时,ROS 提供了基于发布-订阅模式的数据通信机制。

rostopic2

核心角色:

角色 英文 职责
发布者 Publisher 发送消息的一方
订阅者 Subscriber 接收消息的一方
主题 Topic 消息的中转通道

与点对点的直接传输不同,ROS 将消息发布到 Topic 中,任何需要该数据的节点都可以从 Topic 中取走消息——可以理解为”聚宝盆”,东西放进去后,不管有多少人来取,都能拿到完整数据。

二、Topic 通讯实现

下面分别用 C++ 和 Python 实现完整的 Publisher 与 Subscriber。

2.1 C++ 实现

Publisher(发布者)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "ros/ros.h"
#include <iostream>
#include "std_msgs/String.h"

using namespace std;

int main(int argc, char **argv) {
string nodeName = "cpppublisher";
string topicName = "cpptopic";

// 初始化 ROS 节点
ros::init(argc, argv, nodeName);
ros::NodeHandle node;

// 通过节点创建 publisher
const ros::Publisher &publisher = node.advertise<std_msgs::String>(topicName, 1000);

// 按固定周期发布消息
int index = 0;
ros::Rate rate(1); // 1Hz
while (ros::ok()) {
std_msgs::String msg;
msg.data = "hello pub " + to_string(index);
publisher.publish(msg);

cout << msg.data << endl;
rate.sleep();
index++;
}
return 0;
}

Subscriber(订阅者)

topicCallback 是收到订阅消息时的回调函数。

⚠️ 必须用 ros::Subscriber 变量接收 node.subscribe() 的返回值,否则对象会被系统立即回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "ros/ros.h"
#include <iostream>
#include "std_msgs/String.h"

using namespace std;

void topicCallback(const std_msgs::String::ConstPtr &msg) {
cout << (*msg).data << endl;
}

int main(int argc, char **argv) {
string nodeName = "cppsubscriber";
string topicName = "cpptopic";

// 初始化节点
ros::init(argc, argv, nodeName);
ros::NodeHandle node;

// 创建订阅者
const ros::Subscriber &subscriber = node.subscribe(topicName, 1000, topicCallback);

// 阻塞线程,进入事件循环
ros::spin();
return 0;
}

2.2 Python 实现

Publisher(发布者)

💡 Python 脚本必须包含以下两行:

  • #!/usr/bin/env python —— 声明为 Python 脚本(不加则默认按 bash 执行)
  • #coding:utf-8 —— 提供中文支持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python
#coding:utf-8

import rospy
from std_msgs.msg import String

if __name__ == '__main__':
nodeName = "pypublisher"
topicName = "pytopic"

# 初始化节点
rospy.init_node(nodeName)

# 创建发布者
publisher = rospy.Publisher(topicName, String, queue_size=1000)

rate = rospy.Rate(10)
count = 0
while not rospy.is_shutdown():
# 发布消息
publisher.publish("hello %d" % count)
rate.sleep()
count += 1

Subscriber(订阅者)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
#coding:utf-8
import rospy
from std_msgs.msg import String

def topicCallback(msg):
print(msg.data) # ROS Python 中直接访问 .data 属性

if __name__ == '__main__':
nodeName = "pysubscriber"
topicName = "pytopic"

# 初始化节点
rospy.init_node(nodeName)

# 创建订阅者
rospy.Subscriber(topicName, String, topicCallback)

# 阻塞线程,进入事件循环
rospy.spin()

2.3 调试工具

工具 命令 说明
Topic 列表与监控 rosrun rqt_topic rqt_topic 图形化查看所有 Topic
Publisher 调试 rosrun rqt_publisher rqt_publisher 手动发布消息测试
命令行查看消息 rostopic echo /topic_name 终端打印消息内容
命令行发布消息 rostopic pub /topic_name std_msgs/String "data: hello" 手动发布一条消息

三、Topic 自定义消息

除了使用 std_msgs 等内置消息类型,我们也可以定义自己的消息格式。

3.1 定义步骤

Step 1:创建 msg 目录

在 package 目录下新建 msg 目录:

1
2
cd ~/catkin_ws/src/<your_package>
mkdir msg

Step 2:编写 .msg 文件

创建 Student.msg 文件,每行一个字段(格式:类型 名称):

1
2
string name
int64 age

ROS 提供的基础数据类型远不止 stringint64,还包括 float32/64booltimeduration 以及各类数组等。完整列表可参考 ROS 官方文档的 msg 类型说明

Step 3:配置 package.xml

添加消息生成和运行时依赖:

1
2
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
依赖 作用
message_generation 编译时生成消息对应的 C++ 头文件 / Python 模块
message_runtime 运行时消息序列化/反序列化支持

Step 4:配置 CMakeLists.txt

find_package 中添加 message_generation

1
2
3
4
5
6
find_package(catkin REQUIRED COMPONENTS
roscpp
rosmsg
rospy
message_generation
)

② 添加消息文件声明:

1
2
3
4
add_message_files(
FILES
Student.msg
)

文件名必须与 msg/ 目录下的实际文件名一致,且必须位于 msg/ 目录下,否则编译会报错。

③ 配置消息生成依赖:

1
2
3
4
generate_messages(
DEPENDENCIES
std_msgs
)

默认需添加 std_msgs 依赖,如果消息中引用了其他自定义消息,也需要在此处声明。

④ 配置 catkin_package 导出:

1
2
3
4
5
6
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES demo_msg
CATKIN_DEPENDS roscpp rosmsg rospy message_runtime
# DEPENDS system_lib
)

此配置将 message_runtime 依赖导出给依赖当前包的其他包使用。

Step 5:编译并验证

1
2
3
# 进入工作空间编译
cd ~/catkin_ws
catkin_make

验证生成的文件:

语言 验证路径 说明
C++ devel/include/<package_name>/ 应出现 Student.h 头文件
Python devel/lib/python2.7/dist-packages/<package_name>/msg.py 出现对应 py 文件即成功

使用 rosmsg 工具校验:

1
rosmsg show demo_msgs/Student

输出结果应与定义的字段一致,说明自定义消息创建成功。

四、使用自定义消息

自定义消息本质上是一个独立的 package。当其他 package 需要使用它时,必须显式添加依赖关系。

4.1 配置 package.xml

1
2
3
<build_depend>demo_msgs</build_depend>
<build_export_depend>demo_msgs</build_export_depend>
<exec_depend>demo_msgs</exec_depend>

4.2 配置 CMakeLists.txt

find_package 中添加自定义消息包:

1
2
3
4
5
6
find_package(catkin REQUIRED COMPONENTS
roscpp
rosmsg
rospy
demo_msgs
)

4.3 使用示例

C++ 发布自定义消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "ros/ros.h"
#include "demo_msgs/Student.h"

int main(int argc, char **argv) {
ros::init(argc, argv, "student_publisher");
ros::NodeHandle node;

ros::Publisher pub = node.advertise<demo_msgs::Student>("student_info", 1000);

ros::Rate rate(1);
int count = 0;
while (ros::ok()) {
demo_msgs::Student msg;
msg.name = "张三";
msg.age = 20 + count;

pub.publish(msg);
ROS_INFO("Published: %s, %d years old", msg.name.c_str(), msg.age);

rate.sleep();
count++;
}
return 0;
}

C++ 订阅自定义消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "ros/ros.h"
#include "demo_msgs/Student.h"

void studentCallback(const demo_msgs::Student::ConstPtr &msg) {
ROS_INFO("Received student: %s, age: %d", msg->name.c_str(), msg->age);
}

int main(int argc, char **argv) {
ros::init(argc, argv, "student_subscriber");
ros::NodeHandle node;

const ros::Subscriber &sub = node.subscribe("student_info", 1000, studentCallback);

ros::spin();
return 0;
}

Python 发布自定义消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python
#coding:utf-8

import rospy
from demo_msgs.msg import Student

if __name__ == '__main__':
rospy.init_node("student_publisher_py")
pub = rospy.Publisher("student_info", Student, queue_size=1000)

rate = rospy.Rate(1)
count = 0
while not rospy.is_shutdown():
msg = Student()
msg.name = "李四"
msg.age = 20 + count

pub.publish(msg)
rospy.loginfo("Published: %s, %d years old", msg.name, msg.age)

rate.sleep()
count += 1

Python 订阅自定义消息

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python
#coding:utf-8

import rospy
from demo_msgs.msg import Student

def student_callback(msg):
rospy.loginfo("Received student: %s, age: %d", msg.name, msg.age)

if __name__ == '__main__':
rospy.init_node("student_subscriber_py")
rospy.Subscriber("student_info", Student, student_callback)
rospy.spin()

五、总结

要点 说明
通信模型 发布-订阅(Publish-Subscribe),通过 Topic 解耦
数据流向 Publisher → Topic → Subscriber(一对多)
节点注册 所有 Node 启动时向 ROS Master 注册
自定义消息 通过 .msg 文件定义,编译后自动生成各语言绑定
跨包依赖 消费方需同时在 package.xmlCMakeLists.txt 中声明对消息包的依赖
调试利器 rqt_topicrqt_publisherrostopic echo/pub 是日常开发的好帮手