第2章:ROS通信机制详解
各位同学,欢迎来到第二章。上一章我们搭好了ROS环境,今天来聊聊ROS最核心的东西——通信机制。
ROS本质上是一个分布式框架。节点之间怎么说话?靠的就是通信机制。我个人习惯把ROS通信分成两大类:话题通信和服务通信。前者是“发布-订阅”模式,后者是“请求-响应”模式。搞懂这两个,ROS就算入门了。
2.1 话题通信:Publisher/Subscriber
话题通信,说白了就是“广播”。一个节点发消息,其他节点谁想听谁听。发布者只管发,订阅者只管收,双方不需要知道对方的存在。
我在项目中遇到过这样一个场景:机器人上有激光雷达,需要把扫描数据发给导航模块。用话题通信就非常合适——雷达节点持续发布数据,导航节点订阅即可,哪怕同时有多个节点订阅也没问题。
2.1.1 编写一个Publisher节点
我们以C++为例,写一个发布“Hello ROS”字符串的节点。
#include <ros/ros.h>
#include <std_msgs/String.h>
int main(int argc, char **argv)
{
// 初始化节点,节点名称为"talker"
ros::init(argc, argv, "talker");
ros::NodeHandle nh;
// 创建发布者,话题名为"chatter",队列长度为1000
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 1000);
ros::Rate loop_rate(10); // 10Hz
while (ros::ok())
{
std_msgs::String msg;
msg.data = "Hello ROS!";
pub.publish(msg);
ROS_INFO("发布消息: %s", msg.data.c_str());
ros::spinOnce();
loop_rate.sleep();
}
return 0;
}
2.1.2 编写一个Subscriber节点
#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"话题,回调函数为chatterCallback
ros::Subscriber sub = nh.subscribe("chatter", 1000, chatterCallback);
ros::spin(); // 进入循环,等待回调
return 0;
}
嗯,这里要注意:ros::spin()会阻塞主线程,只处理回调。如果你的节点还要做其他事,用ros::spinOnce()配合循环。
2.1.3 编译与运行
在CMakeLists.txt中添加:
add_executable(talker src/talker.cpp)
add_executable(listener src/listener.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
target_link_libraries(listener ${catkin_LIBRARIES})
然后:
catkin_make
roscore
rosrun your_package talker
rosrun your_package listener
你会看到talker在发,listener在收。这就是最基础的话题通信。
2.2 服务通信:Server/Client
话题通信是“单向广播”,服务通信是“一问一答”。客户端发送请求,服务端处理并返回响应。适合需要即时结果的场景。
举个例子:机器人要抓取一个物体,需要知道当前机械臂的位置。这时候发一个请求,服务端返回位置数据,比用话题轮询高效得多。
2.2.1 编写一个Server节点
我们实现一个加法服务:客户端发两个整数,服务端返回它们的和。
#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", (long int)req.a, (long int)req.b);
ROS_INFO("响应: sum=%ld", (long int)res.sum);
return true;
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "add_two_ints_server");
ros::NodeHandle n;
ros::ServiceServer service = n.advertiseService("add_two_ints", add);
ROS_INFO("服务已就绪,等待请求...");
ros::spin();
return 0;
}
2.2.2 编写一个Client节点
#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");
if (argc != 3)
{
ROS_INFO("用法: add_two_ints_client X Y");
return 1;
}
ros::NodeHandle n;
ros::ServiceClient client = n.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;
}
ros::service::waitForService()等待服务就绪。
2.3 自定义消息与服务接口
ROS内置的消息类型(如String、Int32)够用吗?说实话,在真实项目中远远不够。你需要定义自己的数据结构。
比如,我要发布一个“机器人状态”消息,包含位置、速度、电池电量。这时候就得自定义消息。
2.3.1 自定义消息(.msg)
在功能包下创建msg/RobotStatus.msg:
float32 x
float32 y
float32 theta
float32 linear_velocity
float32 angular_velocity
float32 battery_percent
然后在CMakeLists.txt中添加:
add_message_files(FILES RobotStatus.msg)
generate_messages(DEPENDENCIES std_msgs)
在package.xml中添加:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
编译后,你就可以像使用标准消息一样使用your_package::RobotStatus了。
2.3.2 自定义服务(.srv)
创建srv/GetRobotPose.srv:
# 请求:机器人ID
string robot_id
---
# 响应:位置和朝向
float32 x
float32 y
float32 theta
CMakeLists.txt中同样要添加:
add_service_files(FILES GetRobotPose.srv)
这样,你的服务接口就有了自定义的数据结构。
2.4 话题 vs 服务:怎么选?
| 特性 | 话题通信 | 服务通信 |
|---|---|---|
| 通信模式 | 发布/订阅(一对多) | 请求/响应(一对一) |
| 实时性 | 高(持续流数据) | 中等(调用时才有) |
| 适用场景 | 传感器数据、状态广播 | 触发式操作、参数查询 |
| 是否阻塞 | 非阻塞 | 客户端会阻塞等待 |
说白了,如果你要持续发数据(比如激光雷达、摄像头),用话题。如果你要“问一个问题,等一个答案”(比如“给我算个路径”),用服务。
好了,这一章的内容就到这里。下一章我们会聊ROS的另一个重要机制——参数服务器和动作通信。到时候见。