Skip to main content

话题通信机制

话题通信的实现依赖于三个角色的设立:

  1. 订阅者(Subscriber)
  2. 发布者(Publisher)
  3. 管理者(Master)

简单来说,Master负责保管发布者和订阅者注册的信息,帮助匹配话题相同的发布者和订阅者,实现话题通信。

这样的连接建立之后,发布者可以发布消息,且发布的消息会被订阅者订阅。

2.1 话题通信的概念

2.1.1 发布者的注册

发布者启动之后,会通过RPC在Masterzhub注册自己,同时会注册自己发布的话题(名称)。Master会将节点的注册信息加入注册表。

2.1.2 订阅者的注册

订阅者启动之后,也会通过RPC在Masterzhub注册自己,同时会注册自己订阅的话题(名称)。Master会将节点的注册信息加入注册表。

2.1.3 Master实现匹配

Master会定时扫描注册表,将注册表内的节点信息进行匹配(并通过RPC向订阅者发送发布者的RPC地址),将匹配成功的节点信息加入匹配表。

2.1.4 Subscriber向Pubilsher发送连接请求

订阅者根据接受的RPC地址,通过RPC向发布者发布连接请求(订阅的话题名称、消息类型以及通信协议)。

通信协议有TCP和UDP两种。

2.1.5 Publisher确认请求

Publisher收到请求,通过RPC返回一个信息确认,发送自己的地址信息。

2.1.6 连接建立

Subscriber根据上一步返回的消息,拿到确认了拿到地址了,这样就可以建立连接了。

2.1.7 发送消息

连接建立后,由发布者发送消息,订阅者收到消息。

我感觉这个协议很像TCP的那套握手挥手,事实上前五步都是在使用RPC协议,最后两步使用了TCP协议。

需要注意的是发布者和订阅者之间没有“先有鸡后有蛋”的讲究,也就是不考虑启动顺序;发布者和订阅者都可以有多个;连接建立之后,Master就不再重要(做了个媒),关闭Master也可以照常通信。

2.2 C++实现话题通信

ROS master不需要实现了,而且连接的建立也已经被封装了。所以现在回想ROS的话题通信,还剩下发布方和接收方,以及流通的“货币”————数据。

实现流程一般是如下框架:

  1. 编写发布方实现
  2. 编写接收方实现
  3. 编辑配置文件
  4. 编译运行

2.2.1 发布方Talker

# 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"); //初始化talker节点
ros::NodeHandle nh; //创建节点句柄
ros::Publisher chatter_pub = nh.advertise<std_msgs::String>("chatter", 1000); //创建发布者

std_msgs::String msg;// 动态组织数据
std::string msg_front = "Hello 你好!";
int count = 0; // 消息计数器

ros::Rate r(10); //设置循环频率


while (ros::ok())
{
std::stringstream ss;
ss << msg_front << count;
msg.data = ss.str(); //将数据组织成字符串

chatter_pub.publish(msg); //发布消息
ROS_INFO("%s", msg.data.c_str()); //打印消息
r.sleep(); //循环频率
count++; //消息计数器

ros::spinOnce(); //处理回调函数
}
}

2.2.2 接收方Listener

#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>

void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
ROS_INFO("我听见你说: [%s]", msg->data.c_str()); // 输出接收到的消息
}

int main(int argc, char **argv)
{
setlocale(LC_ALL, ""); // 设置中文编码
ros::init(argc, argv, "listener"); //初始化listener节点(命名唯一)
ros::NodeHandle nh;// 实例化ROS句柄
// 创建订阅者
ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);
// 处理订阅的消息(回调函数)
ros::spin();
return 0;
}
"

2.2.3 配置CMakeLists.txt

add_executable(talker src/talker.cpp) // 编译talker
add_executable(listener src/listener.cpp) // 编译listener

target_link_libraries(Hello_pub
${catkin_LIBRARIES}
) // 链接库
target_link_libraries(Hello_sub
${catkin_LIBRARIES}
) // 链接库

2.2.4 执行

  1. 启动roscore
  2. 启动talker
  3. 启动listener

2.3 话题通信自定义msg

在ROS通信协议中,还需要注意msg(数据),ROS通过std_msgs封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty...

这些数据一般只包含一个data字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,std_msgs本身提供的描述性就有点弱,这时,需要自定义消息类型。

msgs只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型:

  • int8, int16, int32, int64(uint*)
  • float32, float64
  • string
  • time, duration
  • other msg files
  • variable-length array[] and fixed-length array[C]

ROS中还有一种特殊类型:Header这个标头包含时间戳和ROS中常用的坐标帧信息.

需求:创建自定义消息,该消息包含人的信息:姓名、身高、年龄等。

流程:

  1. 按照固定格式创建msg文件
  2. 编辑配置文件
  3. 编译运行(中间文件)

2.3.1 创建msg文件

功能包下新建msg目录,添加文件Person.msg

string name
uint16 age
float64 height

2.3.2 编辑配置文件

package.xml中添加编译依赖与执行依赖

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

CMakeLists.txt编辑 msg 相关配置

find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
// 配置 msg 源文件
add_message_files(
FILES
Person.msg
)
// 生成消息时依赖于 std_msgs
generate_messages(
DEPENDENCIES
std_msgs
)
// 执行时依赖
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES demo02_talker_listener
CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)

2.3.4 编译运行

编译后的中间文件查看:

C++需要调用的中间文件(.../工作空间/devel/include/PACKAGE_NAME/MSG_NAME.h)

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

后续调用相关msg,从这些中间文件调用。

2.4 话题通信自定义msg调用(C++)

warning

需求: 编写发布订阅实现,要求发布方以10Hz的频率发布自定义消息,订阅方订阅自定义消息并将消息内容打印输出。

tip

回顾三要素:发布方,接收方,数据 回顾实现流程:

  1. 编写发布方实现
  2. 编写接收方实现
  3. 编辑配置文件
  4. 编译运行

2.4.1 vscode配置

为了方便代码提示以及避免误抛异常,需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性:

{
"configurations": [
{
"browse":{
"databaseFilename": "",
"limitSymbolsToIncludedHeaders": true,
}
"includePath": [
"/opt/ros/noetic/include/**"
"/usr/include/**"
"/xxx/yy工作空间/devel/include/**"//配置head文件
"name":ROS,
"intelliSenseMode": "gcc-x64",
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17"
]
}
],
"version": 4
}

2.4.2 编写发布方实现

#include "ros/ros.h"
#include "demo02_talker_listener/Person.h"

int main(int argc, char **argv)
{
setlocale(LC_ALL, ""); // 设置中文编码
ros::init(argc, argv, "talker"); //初始化talker节点(命名唯一)
ros::NodeHandle nh;// 实例化ROS句柄
// 创建发布者对象
// advertise<数据类型>("话题名称",队列长度)
ros::Publisher pub = nh.advertise<demo02_talker_listener::Person>("chatter_person",1000);
demo02_talker_listener::Person p;
p.name = "八戒";
p.age = 800;
p.height = 5.5;

ros::Rate r(1);
while (ros::ok())
{
pub.publish(p);
p.age += 1;
ROS_INFO("我叫:%s,今年%d岁,高%.2f米", p.name.c_str(), p.age, p.height);

r.sleep();
ros::spinOnce();
}
return 0;
}

2.4.3 编写接收方实现

#include "ros/ros.h"
#include "demo02_talker_listener/Person.h"

void doMsg(const demo02_talker_listener::Person::ConstPtr &msg)
{
ROS_INFO("我:%s,今年%d岁,%.2f米", msg->name.c_str(), msg->age, msg->height);
}

int main(int argc, char **argv)
{
setlocale(LC_ALL, "");
// 初始化节点
ros::init(argc, argv, "listener");
// 创建句柄
ros::NodeHandle nh;
// 创建一个Listener的对象
ros::Subscriber sub = nh.subscribe<demo02_talker_listener::Person>("chatter_person", 1000, doMsg);
ros::spin();
return 0;
}

2.4.4 配置CMakeLists.txt

需要添加add_dependencies用以设置所依赖的消息相关的中间文件。

add_executable(person_talker src/person_talker.cpp)
add_executable(person_listener src/person_listener.cpp)

add_dependencies(person_talker ${catkin_EXPORTED_TARGETS})
add_dependencies(person_listener ${catkin_EXPORTED_TARGETS})

target_link_libraries(person_talker ${catkin_LIBRARIES})
target_link_libraries(person_listener ${catkin_LIBRARIES})

2.4.5 编译运行

  1. 启动roscore
  2. 启动发布节点
  3. 启动订阅节点

2.5 服务通信理论模型

ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client 发送请求信息,Server 返回响应信息。

2.5.1 Server注册

Server启动之后,通过RPC在ROS Master中注册自身信息,其中包含提供的服务的名称,ROS Master会将节点的注册信息加入到注册表中。

2.5.2 Client注册

Client启动后,也会通过RPC在ROS Master中注册自身信息,其中包含请求的服务的名称,ROS Master会将节点的注册信息加入到注册表中。

2.5.3 ROS Master匹配

ROS Master根据注册表中的信息匹配Server和Client,并通过RPC向Client发送Server的TCP地址信息。

2.5.4 Client发送请求

Client根据信息匹配使用TCP与Server建立网络连接,并发送请求数据。

2.5.5 Server发送响应

Server接收、解析请求的数据,把响应结果返回给Client。

2.6 服务通信自定义srv

srv文件内的可用数据类型与msg文件一致,且定义srv实现流程与自定义msg实现流程类似,

2.6.1 定义srv文件

srv文件内的可用数据类型与msg文件一致,且定义srv实现流程与自定义msg实现流程类似:

功能包下新建srv目录,添加文件xxx.srv:

int32 a;
int32 b;

int32 sum;# 服务响应数据

2.6.2 编辑配置文件

package.xml中添加编译依赖与执行依赖

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

CMakeLists.txt中添加编译依赖与执行依赖

find_package(catkin REQUIRED COMPONENTS
message_generation
roscpp
rospy
std_msgs
)
# 必须有std_msgs,否则编译会报错
add_service_files(
FILES
AddInts.srv
)

2.6.3 编译

编译之后的中间文件: c++: xxx.h python: srv

2.7 服务通信自定义srv调用(C++)

2.7.1 vscode配置

{
"configurations": [
{
"browse": {
"databaseFilename": "",
"limitSymbolsToIncludedHeaders": true
},
"includePath": [
"/opt/ros/noetic/include/**",
"/usr/include/**",
"/xxx/yyy工作空间/devel/include/**" //配置 head 文件的路径
],
"name": "ROS",
"intelliSenseMode": "gcc-x64",
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17"
}
],
"version": 4
}

2.7.2 服务端

#include "ros/ros.h"
#include "demo03_server_client/AddInts.h"