服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。用于偶然的、对时时性有要求、有一定逻辑处理需求的数据传输场景。
服务通信较之于话题通信更简单些,理论模型如上图所示,该模型中涉及到三个角色:
ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client 发送请求信息,Server 返回响应信息。
advertise Service("bar",foo:1234)
Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。
lookupService("bar")
Client 启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。
{foo:3456}
ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的TCP 地址信息。
request data (args)
Client 根据步骤2 响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。
reply data
Server 接收、解析请求的数据,并产生响应结果返回给 Client。
1、保证顺序,客户端发起请求时,服务器需要已经启动
2、客户端和服务器都可以存在多个
要求:服务通信中,客户端提交两个整数至服务端,服务端求和并响应结果到客户端,请创建服务器与客户端通信的数据载体。
在功能包下创建srv目录,添加文件AddInts.srv.
package.xml中添加编译依赖与执行依赖
a、find_package
编译自定义的功能包时需要依赖find_package中的功能包
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
# 需要加入 message_generation,必须有 std_msgs
b、add_service_files
在“srv”文件夹中生成服务
add_service_files(
FILES
AddInts.srv
)
c、 generate_messages
生成消息是依赖于std_mags。因为自定义msg也是由std_msgs 中一些原生的数据类型组成的
generate_messages(
DEPENDENCIES
std_msgs
)
d、catkin_package
编译find_package中的依赖包时,依赖于catkin_package中的功能包
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES plumbing_server_client
CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)
快捷键ctrl+alt+B,配置方法之前分享过。
编译后的中间文件会放在工作空间的dev目录下,C++的中间文件在include中,python的在lib中,
后续调用相关 srv 时,是从这些中间文件调用的。
为了方便代码提示以及避免误抛异常,需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includePath属性:
#include "ros/ros.h"
#include "plumbing_server_client/AddInts.h"
/*
服务器端实现:解析客户端提交的数据,并运算,再产生响应
1、包含头文件
2、初始化 ROS 节点
3、创建 ROS节点 句柄
4、创建 服务 对象
5、回调函数 处理请求并产生响应
6、由于请求有多个,需要调用 ros::spin()
*/
//回调函数
bool doNums(plumbing_server_client::AddInts::Request &request,
plumbing_server_client::AddInts::Response &response)
{
//处理请求
int num1 = request.num1;
int num2 = request.num2;
ROS_INFO("收到的请求数据:num1 = %d,num2 = %d",num1,num2);
//组织响应
int sum = num1 + num2;
response.sum = sum;
ROS_INFO("求和的结果:sum = %d",sum);
return true;
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
// 2、初始化 ROS 节点
ros::init(argc,argv,"server");//保证节点名称唯一
// 3、创建 ROS节点 句柄
ros::NodeHandle nh;
// 4、创建 服务 对象
ros::ServiceServer server = nh.advertiseService("AddInts",doNums);
ROS_INFO("服务器启动");
// 5、回调函数 处理请求并产生响应
// 6、由于请求有多个,需要调用 ros::spin()
ros::spin();
return 0;
}
#include "ros/ros.h"
#include "plumbing_server_client/AddInts.h"
/*
客户端实现:提交两个整数,并处理响应的结果
1、包含头文件
2、初始化 ROS 节点
3、创建 ROS节点 句柄
4、创建 客户端 对象
5、提交请求并处理响应
实现参数的动态提交:
1、格式:rosrun xxx xxx 1 2
2、节点执行时,需要获取命令中的参数,并组织进request
问题:
若先启动客户端则会报错
需求:
如果先启动客户端,不要直接抛出异常,而是挂起,等待服务器启动后,再正常请求
在ROS中内置了相关函数,这些函数可以让客户端启动后挂起,等待服务器启动
client.waitForExistence();
ros::service::waitForService("服务话题");
*/
int main(int argc, char *argv[])
{
/* code */
setlocale(LC_ALL,"");
//获取命令中的参数
if(argc != 3)
{
ROS_INFO("提交的参数个数不对");
return 1;
}
ros::init(argc,argv,"client");;
ros::NodeHandle nh;
/*
泛型< >: 提交的消息类型 srv
参数1: 要提交的话题
*/
ros::ServiceClient client = nh.serviceClient("AddInts");
//提交请求并处理响应
plumbing_server_client::AddInts ai;
//组织请求
ai.request.num1 = atoi(argv[1]);
ai.request.num2 = atoi(argv[2]);
//处理响应
//调用判断服务器状态的函数
//方法1
client.waitForExistence();
//方法2 参数:等待的服务
//ros::service::waitForService("AddInts");
bool flag = client.call(ai);
if(flag)
{
ROS_INFO("正常响应了!");
//获取响应结果
ROS_INFO("响应结果 = %d",ai.response.sum);
}
else{
ROS_INFO("处理失败了!");
}
return 0;
}
除了常规的add_executable和target_link_libraries配置之外。还需要配置add_dependencies,这是为了编译srv文件之后,再编译包含srv文件的cpp文件,保证不会出现逻辑错误。
add_dependencies(server ${PROJECT_NAME}_gencpp)
add_dependencies(client ${PROJECT_NAME}_gencpp)
add_executable(server src/server.cpp)
add_executable(client src/client.cpp)
target_link_libraries(server ${catkin_LIBRARIES})
target_link_libraries(client ${catkin_LIBRARIES})
开启roscore
需要先启动服务端:rosrun 包名 服务端
rosrun plumbing_server_client server
然后再调用客户端 :rosrun 包名 客户端 参数1 参数2
rosrun plumbing_server_client client 1 2
a、若先启动客户端则会报错
b、解决办法:
在客户端发送请求前添加:
client.waitForExistence();
或:
ros::service::waitForService("AddInts");
可以让客户端启动后挂起,等待服务器启动
这是一个阻塞式函数,只有服务启动成功后才会继续执行,。