4. ROS通信机制:Topic、Service、Action编程实战、自定义消息类型
ROS的通信机制,说白了就是机器人各个模块之间怎么“说话”。我刚开始接触ROS时,觉得这玩意儿不就是发发数据嘛,后来才发现,选错通信方式,系统跑起来会出大问题。今天咱们就把Topic、Service、Action这三个核心机制掰开揉碎,再聊聊自定义消息类型——这可是你搭建复杂系统的基本功。
4.1 三种通信机制的核心区别
先给个直观的对比。你想想看,一个机器人要完成“抓取杯子”这个任务:
- Topic(话题):像广播电台。摄像头一直在发图像数据,谁想听谁订阅。适合连续、高频、单向的数据流。
- Service(服务):像打电话。你问一句“杯子在哪?”,对方回答“在桌上”。适合请求-响应、一次性的任务。
- Action(动作):像发快递。你下单后,快递员会告诉你“已取件”、“运输中”、“已送达”,还能中途取消。适合长时间、可反馈、可取消的任务。
我的经验:在SLAM建图时,激光雷达数据用Topic发布(每秒10帧),地图保存用Service(点一下保存),导航到目标点用Action(可以实时看进度)。选错了,系统要么延迟爆炸,要么逻辑混乱。
4.2 Topic编程实战
Topic是最常用的通信方式。我习惯把它比作“发布-订阅”模式。写一个简单的发布者和订阅者,你就懂了。
4.2.1 发布者(Publisher)
// talker.cpp
#include "ros/ros.h"
#include "std_msgs/String.h"
int main(int argc, char **argv)
{
ros::init(argc, argv, "talker");
ros::NodeHandle nh;
// 创建一个发布者,话题名为"chatter",消息类型为std_msgs::String
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 10);
ros::Rate loop_rate(10); // 10Hz
int count = 0;
while (ros::ok())
{
std_msgs::String msg;
msg.data = "hello world " + std::to_string(count);
ROS_INFO("发布消息: %s", msg.data.c_str());
pub.publish(msg);
ros::spinOnce();
loop_rate.sleep();
count++;
}
return 0;
}
4.2.2 订阅者(Subscriber)
// listener.cpp
#include "ros/ros.h"
#include "std_msgs/String.h"
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
ROS_INFO("收到消息: %s", msg->data.c_str());
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "listener");
ros::NodeHandle nh;
// 订阅话题"chatter",回调函数处理数据
ros::Subscriber sub = nh.subscribe("chatter", 10, chatterCallback);
ros::spin(); // 进入循环,等待回调
return 0;
}
避坑指南:我曾经在队列大小上吃过亏。队列大小设为1,如果处理速度跟不上发布速度,数据会丢。SLAM中图像数据建议设大一点(比如10),不然关键帧丢了,地图就飘了。
4.3 Service编程实战
Service适合“问一次,答一次”的场景。比如你让机器人保存当前地图,它保存完告诉你“好了”。
4.3.1 服务端(Server)
// server.cpp
#include "ros/ros.h"
#include "beginner_tutorials/AddTwoInts.h"
bool add(beginner_tutorials::AddTwoInts::Request &req,
beginner_tutorials::AddTwoInts::Response &res)
{
res.sum = req.a + req.b;
ROS_INFO("请求: a=%ld, b=%ld, 响应: sum=%ld", (long int)req.a, (long int)req.b, (long int)res.sum);
return true;
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "add_two_ints_server");
ros::NodeHandle nh;
ros::ServiceServer service = nh.advertiseService("add_two_ints", add);
ROS_INFO("服务已就绪");
ros::spin();
return 0;
}
4.3.2 客户端(Client)
// client.cpp
#include "ros/ros.h"
#include "beginner_tutorials/AddTwoInts.h"
#include <cstdlib>
int main(int argc, char **argv)
{
ros::init(argc, argv, "add_two_ints_client");
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient<beginner_tutorials::AddTwoInts>("add_two_ints");
beginner_tutorials::AddTwoInts srv;
srv.request.a = atoll(argv[1]);
srv.request.b = atoll(argv[2]);
if (client.call(srv))
{
ROS_INFO("结果: %ld", (long int)srv.response.sum);
}
else
{
ROS_ERROR("服务调用失败");
return 1;
}
return 0;
}
注意:Service是同步的。客户端调用后必须等服务器响应,期间会阻塞。如果你在控制循环里频繁调用Service,小心系统卡死。我见过有人用Service做实时控制,结果机器人一顿一顿的。
4.4 Action编程实战
Action是Service的升级版。它支持:
- 目标(Goal):发起任务
- 反馈(Feedback):实时汇报进度
- 结果(Result):最终输出
- 取消(Cancel):随时中止
举个例子,导航到目标点:
// 定义Action文件(Fibonacci.action)
# 目标
int32 order
---
# 结果
int32[] sequence
---
# 反馈
int32[] sequence
4.4.1 Action服务端
// fibonacci_server.cpp
#include <ros/ros.h>
#include <actionlib/server/simple_action_server.h>
#include "beginner_tutorials/FibonacciAction.h"
class FibonacciAction
{
protected:
ros::NodeHandle nh_;
actionlib::SimpleActionServer<beginner_tutorials::FibonacciAction> as_;
std::string action_name_;
beginner_tutorials::FibonacciFeedback feedback_;
beginner_tutorials::FibonacciResult result_;
public:
FibonacciAction(std::string name) :
as_(nh_, name, boost::bind(&FibonacciAction::executeCB, this, _1), false),
action_name_(name)
{
as_.start();
}
void executeCB(const beginner_tutorials::FibonacciGoalConstPtr &goal)
{
ros::Rate r(1);
bool success = true;
std::vector<int> sequence;
sequence.push_back(0);
sequence.push_back(1);
feedback_.sequence = sequence;
as_.publishFeedback(feedback_);
for(int i=1; i<goal->order; i++)
{
if(as_.isPreemptRequested() || !ros::ok())
{
ROS_INFO("%s: 任务被取消", action_name_.c_str());
as_.setPreempted();
success = false;
break;
}
sequence.push_back(sequence[i] + sequence[i-1]);
feedback_.sequence = sequence;
as_.publishFeedback(feedback_);
r.sleep();
}
if(success)
{
result_.sequence = sequence;
ROS_INFO("%s: 任务完成", action_name_.c_str());
as_.setSucceeded(result_);
}
}
};
我的习惯:Action的反馈频率不要太高。我曾经设成100Hz反馈,结果网络带宽被占满,其他Topic都发不出去了。一般1-10Hz就够了,用户看起来是实时的。
4.5 自定义消息类型
ROS自带的消息类型(如String、Int32)够用吗?不够。在SLAM中,你需要传递点云、位姿、地图数据,这些都得自己定义。
4.5.1 定义.msg文件
// 文件: MySLAMData.msg
# 自定义SLAM数据消息
Header header
geometry_msgs/Point position # 当前位置
geometry_msgs/Quaternion orientation # 当前姿态
float32[] map_data # 地图数据(一维数组)
int32 map_width # 地图宽度
int32 map_height # 地图高度
float32 resolution # 地图分辨率
4.5.2 在CMakeLists.txt中配置
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
geometry_msgs
message_generation
)
add_message_files(
FILES
MySLAMData.msg
)
generate_messages(
DEPENDENCIES
std_msgs
geometry_msgs
)
catkin_package(
CATKIN_DEPENDS message_runtime
)
4.5.3 在package.xml中声明
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
避坑指南:我曾经在自定义消息里用了vector<vector<float>>,结果编译通过,运行时序列化报错。ROS的msg不支持嵌套动态数组。解决方案:用一维数组+长度字段,或者用自定义消息数组。
4.6 三种机制的选型对比
| 特性 | Topic | Service | Action |
|---|---|---|---|
| 通信模式 | 发布-订阅 | 请求-响应 | 目标-反馈-结果 |
| 实时性 | 高(异步) | 中(同步阻塞) | 高(异步+反馈) |
| 适用场景 | 传感器数据、状态发布 | 参数查询、触发操作 | 导航、机械臂控制 |
| 是否支持取消 | 否 | 否 | 是 |
| 是否支持反馈 | 否 | 否 | 是 |
4.7 知识体系总览
下面这张图,是我梳理的ROS通信机制知识体系。你可以把它当作学习地图:
总结一下:Topic是ROS的血液,Service是ROS的神经反射,Action是ROS的肌肉运动。自定义消息类型,就是给这些通信机制定制“语言”。掌握了这些,你就能搭建出健壮的机器人系统。
公众号:蓝海资料掘金营,微信deep3321