ROS 通信机制
ROS 中的基本通信机制主要有如下三种实现策略:
- 话题通信(发布订阅模式)
- 服务通信(请求响应模式)
- 参数服务器(参数共享模式)
话题通信
话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的,也即:一个节点发布消息,另一个节点订阅该消息。话题通信的应用场景也极其广泛,比如下面一个常见场景:
机器人在执行导航功能,使用的传感器是激光雷达,机器人会采集激光雷达感知到的信息并计算,然后生成运动控制信息驱动机器人底盘运动。
在上述场景中,就不止一次使用到了话题通信。
- 以激光雷达信息的采集处理为例,在 ROS
中有一个节点需要实时地发布当前雷达采集到的数据,导航模块中也有节点会订阅并解析雷达数据。
- 再以运动消息的发布为例,导航模块会根据传感器采集的数据时时的计算出运动控制信息并发布给底盘,底盘也可以有一个节点订阅运动信息并最终转换成控制电机的脉冲信号。
以此类推,像雷达、摄像头、GPS….
等等一些传感器数据的采集,也都是使用了话题通信,换言之,话题通信适用于不断更新的数据传输相关的应用场景。
概念
以发布订阅的方式实现不同节点之间数据交互的通信模式。
作用
用于不断更新的、少逻辑处理的数据传输场景。
理论模型
话题通信模型如下图所示
- ROS Master (管理者)
- Talker (发布者)
- Listener (订阅者)
ROS Master 负责保管 Talker 和 Listener 注册的信息,并匹配话题相同的
Talker 与 Listener,帮助 Talker 与 Listener 建立连接,连接建立后,Talker
可以发布消息,且发布的消息会被 Listener 订阅。
整个流程由以下步骤实现:
- Talker注册:Talker启动后,会通过远程过程调用协议(Remote
Procedure Call Protocol,RPC)s在 ROS Master
中注册自身信息,其中包含所发布消息的话题名称。ROS Master
会将节点的注册信息加入到注册表中。
- Listener注册:Listener启动后,也会通过RPC在 ROS
Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master
会将节点的注册信息加入到注册表中。
- ROS Master实现信息匹配:ROS Master
会根据注册表中的信息匹配Talker 和 Listener,并通过 RPC 向 Listener 发送
Talker 的 RPC 地址信息。
- Listener向Talker发送请求:Listener 根据接收到的 RPC
地址,通过 RPC 向 Talker
发送连接请求,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。
- Talker确认请求:Talker 接收到 Listener
的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP
地址信息。
- Listener与Talker件里连接:Listener 根据步骤5
返回的消息使用 TCP 与 Talker 建立网络连接。
- Talker向Listener发送消息:连接建立后,Talker 开始向
Listener 发布消息。
注意1:上述实现流程中,前五步使用的
RPC协议,最后两步使用的是 TCP 协议
注意2: Talker 与 Listener 的启动无先后顺序要求
注意3: Talker 与 Listener 都可以有多个
注意4: Talker 与 Listener 连接建立后,不再需要 ROS
Master。也即,即便关闭ROS Master,Talker 与 Listerner
照常通信,即在启动节点后可以关闭 roscore
终端。
话题通信基本操作
发布者代码实现
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
|
#include "ros/ros.h" #include "std_msgs/String.h" #include <sstream>
int main(int argc, char *argv[]) {
setlocale(LC_ALL,"");
ros::init(argc,argv,"talker");
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10);
std_msgs::String msg;
std::string msg_front = "Hello 你好!"; int count = 0;
ros::Rate r(1);
while (ros::ok()) { std::stringstream ss; ss << msg_front << count; msg.data = ss.str(); pub.publish(msg); ROS_INFO("发送的消息:%s",msg.data.c_str());
r.sleep(); count++; ros::spinOnce(); } return 0; }
|
订阅方
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
#include "ros/ros.h" #include "std_msgs/String.h"
void doMsg(const std_msgs::String::ConstPtr& msg_p){ ROS_INFO("我听见:%s",msg_p->data.c_str()); } int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ros::init(argc,argv,"listener"); ros::NodeHandle nh;
ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);
ros::spin();
return 0; }
|
配置CMakeList.text
1 2 3 4 5 6 7 8 9 10 11 12 13
| add_executable(Hello_pub src/Hello_pub.cpp ) add_executable(Hello_sub src/Hello_sub.cpp )
target_link_libraries(Hello_pub ${catkin_LIBRARIES} ) target_link_libraries(Hello_sub ${catkin_LIBRARIES} )
|
通信msg文件
在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过
std_msgs
封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty….
但是,这些数据一般只包含一个 data
字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,比如:
激光雷达的信息… std_msgs
由于描述性较差而显得力不从心,这种场景下可以使用自定义的消息类型
**需求:**创建自定义消息,该消息包含人的信息:姓名、身高、年龄等。
流程:
- 按照固定格式创建 msg 文件
- 编辑配置文件
- 编译生成可以被 Python 或 C++ 调用的中间文件
定义msg文件
功能包下新建 msg 目录,添加文件 Person.msg
1 2 3
| string name uint16 age float64 height
|
编辑配置文件
package.xml中添加编译依赖与执行依赖
1 2 3 4 5
| <build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend>
|
CMakeLists.txt编辑 msg 相关配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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 roscpp rospy std_msgs message_runtime
)
|
编译
编译后的中间文件查看:
C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)

Python
需要调用的中间文件(…/工作空间/devel/lib/python3/dist-packages/包名/msg)

后续调用相关 msg 时,是从这些中间文件调用的
服务通信
服务通信较之于话题通信更简单些,理论模型如下图所示,该模型中涉及到三个角色:
- ROS master(管理者)
- Server(服务端)
- Client(客户端)
理论模型
ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的
Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client
发送请求信息,Server 返回响应信息。
整个流程由以下步骤实现:
- Server注册 Server 启动后,会通过RPC在 ROS Master
中注册自身信息,其中包含提供的服务的名称。ROS Master
会将节点的注册信息加入到注册表中。
- Client注册 Client 启动后,也会通过RPC在 ROS Master
中注册自身信息,包含需要请求的服务的名称。ROS Master
会将节点的注册信息加入到注册表中。
- ROS Master实现信息匹配 ROS Master
会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送
Server 的 TCP 地址信息。
- Client发送请求 Client 根据步骤2 响应的信息,使用
TCP 与 Server 建立网络连接,并发送请求数据。
- Server发送响应 Server
接收、解析请求的数据,并产生响应结果返回给 Client。
注意:
1.客户端请求被处理时,需要保证服务器已经启动;
2.服务端和客户端都可以存在多个。
服务srv文件
定义srv文件
服务通信中,数据分成两部分,请求与响应,在 srv
文件中请求和响应使用---
分割,具体实现如下:
功能包下新建 srv 目录,添加 xxx.srv 文件,内容:
1 2 3 4 5 6
| # 客户端请求时发送的两个数字 int32 num1 int32 num2 --- # 服务器响应发送的数据 int32 sum
|
编辑配置文件
配置几乎与话题一致
package.xml中添加编译依赖与执行依赖
1 2 3 4 5
| <build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend>
|
CMakeLists.txt编辑 srv 相关配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation )
add_service_files( FILES AddInts.srv ) generate_messages( DEPENDENCIES std_msgs )
|
注意: 官网没有在 catkin_package 中配置
message_runtime,经测试配置也可以
编译
编译后的中间文件查看:
C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)

Python
需要调用的中间文件(…/工作空间/devel/lib/python3/dist-packages/包名/srv)

后续调用相关 srv 时,是从这些中间文件调用的
服务通信基本操作
服务端
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
#include "ros/ros.h" #include "test_server_client/AddInts.h"
bool doReq(test_server_client::AddInts::Request& req, test_server_client::AddInts::Response& resp){ int num1 = req.num1; int num2 = req.num2;
ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);
if (num1 < 0 || num2 < 0) { ROS_ERROR("提交的数据异常:数据不可以为负数"); return false; }
resp.sum = num1 + num2; return true;
}
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ros::init(argc,argv,"AddInts_Server"); ros::NodeHandle nh; ros::ServiceServer server = nh.advertiseService("AddInts",doReq); ROS_INFO("服务已经启动...."); ros::spin(); return 0; }
|
客户端
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
#include "ros/ros.h" #include "test_server_client/AddInts.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,"");
if (argc != 3) { ROS_ERROR("请提交两个整数"); return 1; }
ros::init(argc,argv,"AddInts_Client"); ros::NodeHandle nh; ros::ServiceClient client = nh.serviceClient<test_server_client::AddInts>("AddInts"); ros::service::waitForService("AddInts"); test_server_client::AddInts ai; ai.request.num1 = atoi(argv[1]); ai.request.num2 = atoi(argv[2]); bool flag = client.call(ai); if (flag) { ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum); } else { ROS_ERROR("请求处理失败...."); return 1; }
return 0; }
|
配置 CMakeLists.txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| add_executable(AddInts_Server src/AddInts_Server.cpp) add_executable(AddInts_Client src/AddInts_Client.cpp)
add_dependencies(AddInts_Server ${PROJECT_NAME}_gencpp) add_dependencies(AddInts_Client ${PROJECT_NAME}_gencpp)
target_link_libraries(AddInts_Server ${catkin_LIBRARIES} ) target_link_libraries(AddInts_Client ${catkin_LIBRARIES} )
|
执行
流程:
- 需要先启动服务:
rosrun 包名 服务
- 然后再调用客户端 :
rosrun 包名 客户端 参数1 参数2
结果:
会根据提交的数据响应相加后的结果。
注意:
如果先启动客户端,那么会导致运行失败
优化:
在客户端发送请求前添加:client.waitForExistence();
或:ros::service::waitForService("AddInts");
这是一个阻塞式函数,只有服务启动成功后才会继续执行
此处可以使用 launch 文件优化,但是需要注意 args 传参特点
参数服务器
参数服务器实现是最为简单的,该模型如下图所示,该模型中涉及到三个角色:
- ROS Master (管理者)
- Talker (参数设置者)
- Listener (参数调用者)
理论模型
ROS Master 作为一个公共容器保存参数,Talker
可以向容器中设置参数,Listener 可以获取参数。
整个流程由以下步骤实现:
- Talker 设置参数 Talker 通过 RPC
向参数服务器发送参数(包括参数名与参数值),ROS Master
将参数保存到参数列表中。
- Listener 获取参数 Listener 通过 RPC
向参数服务器发送参数查找请求,请求中包含要查找的参数名。
- ROS Master 向 Listener 发送参数值 ROS Master
根据步骤2请求提供的参数名查找参数值,并将查询结果通过 RPC 发送给
Listener。
参数可使用数据类型:
- 32-bit integers
- booleans
- strings
- doubles
- iso8601 dates
- lists
- base64-encoded binary data
- 字典
注意:参数服务器不是为高性能而设计的,因此最好用于存储静态的非二进制的简单数据
参数服务器基本操作
**需求:**实现参数服务器参数的增删改查操作。
在 C++ 中实现参数服务器数据的增删改查,可以通过两套 API 实现:
- ros::NodeHandle
- ros::param
参数服务器新增(修改)参数
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
|
#include "ros/ros.h"
int main(int argc, char *argv[]) { ros::init(argc,argv,"set_update_param");
std::vector<std::string> stus; stus.push_back("zhangsan"); stus.push_back("李四"); stus.push_back("王五"); stus.push_back("孙大脑袋");
std::map<std::string,std::string> friends; friends["guo"] = "huang"; friends["yuang"] = "xiao";
ros::NodeHandle nh; nh.setParam("nh_int",10); nh.setParam("nh_double",3.14); nh.setParam("nh_bool",true); nh.setParam("nh_string","hello NodeHandle"); nh.setParam("nh_vector",stus); nh.setParam("nh_map",friends);
nh.setParam("nh_int",10000);
ros::param::set("param_int",20); ros::param::set("param_double",3.14); ros::param::set("param_string","Hello Param"); ros::param::set("param_bool",false); ros::param::set("param_vector",stus); ros::param::set("param_map",friends);
ros::param::set("param_int",20000);
return 0; }
|
参数服务器获取参数
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
|
#include "ros/ros.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ros::init(argc,argv,"get_param");
ROS_INFO("++++++++++++++++++++++++++++++++++++++++"); int res3 = ros::param::param("param_int",20); int res4 = ros::param::param("param_int2",20); ROS_INFO("param获取结果:%d,%d",res3,res4);
int param_int_value; double param_double_value; bool param_bool_value; std::string param_string_value; std::vector<std::string> param_stus; std::map<std::string, std::string> param_friends;
ros::param::get("param_int",param_int_value); ros::param::get("param_double",param_double_value); ros::param::get("param_bool",param_bool_value); ros::param::get("param_string",param_string_value); ros::param::get("param_vector",param_stus); ros::param::get("param_map",param_friends);
ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d", param_int_value, param_double_value, param_string_value.c_str(), param_bool_value ); for (auto &&stu : param_stus) { ROS_INFO("stus 元素:%s",stu.c_str()); }
for (auto &&f : param_friends) { ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str()); }
ros::param::getCached("param_int",param_int_value); ROS_INFO("通过缓存获取数据:%d",param_int_value);
std::vector<std::string> param_names2; ros::param::getParamNames(param_names2); for (auto &&name : param_names2) { ROS_INFO("名称解析name = %s",name.c_str()); } ROS_INFO("----------------------------");
ROS_INFO("存在 param_int 吗? %d",ros::param::has("param_int")); ROS_INFO("存在 param_intttt 吗? %d",ros::param::has("param_intttt"));
std::string key; ros::param::search("param_int",key); ROS_INFO("搜索键:%s",key.c_str());
return 0; }
|
参数服务器删除参数
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
|
#include "ros/ros.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ros::init(argc,argv,"delete_param");
ros::NodeHandle nh; bool r1 = nh.deleteParam("nh_int"); ROS_INFO("nh 删除结果:%d",r1);
bool r2 = ros::param::del("param_int"); ROS_INFO("param 删除结果:%d",r2);
return 0; }
|
通信机制比较
三种通信机制中,参数服务器是一种数据共享机制,可以在不同的节点之间共享数据,话题通信与服务通信是在不同的节点之间传递数据的,三者是ROS中最基础也是应用最为广泛的通信机制。
这其中,话题通信和服务通信有一定的相似性也有本质上的差异,在此将二者做一下简单比较:
二者的实现流程是比较相似的,都是涉及到四个要素:
- 要素1: 消息的发布方/客户端(Publisher/Client)
- 要素2: 消息的订阅方/服务端(Subscriber/Server)
- 要素3: 话题名称(Topic/Service)
- 要素4: 数据载体(msg/srv)
可以概括为:
两个节点通过话题关联到一起,并使用某种类型的数据载体实现数据传输。
二者的实现也是有本质差异的,具体比较如下:
|
Topic(话题) |
Service(服务) |
通信模式 |
发布/订阅 |
请求/响应 |
同步性 |
异步 |
同步 |
底层协议 |
ROSTCP/ROSUDP |
ROSTCP/ROSUDP |
缓冲区 |
有 |
无 |
实时性 |
弱 |
强 |
节点关系 |
多对多 |
一对多(一个 Server) |
通信数据 |
msg |
srv |
使用场景 |
连续高频的数据发布与接收:雷达、里程计 |
偶尔调用或执行某一项特定功能:拍照、语音识别 |
不同通信机制有一定的互补性,都有各自适应的应用场景。尤其是话题与服务通信,需要结合具体的应用场景与二者的差异,选择合适的通信机制。