一、服务通信:
基于请求响应模式,一个节点A向节点B发送请求,B接受处理请求并产生响应结果返回给A。适用于对时时性有要求、具有一定逻辑处理的应用场景。
二、理论顺序:
1、Serve首先和ROS Master建立联系,注册话题和RPC地址
2、Client再和ROS Master建立联系,注册话题
3、当Serve和Client的话题匹配时,ROS Master向Client发送ROSRPC地址
4、之后,Client可以向Serve发送请求
5、Serve收到请求后,通过TCP地址,响应数据给Client
三、注意事项:
1、保证顺序:需要服务端提前启动,客户端才能发起请求
2、客户端和服务端可以存在多个
四、服务通信自定义srv消息
1、创建srv文件
(1)在新建的功能包里,建立一个srv目录,并在里面新建.srv文件
(2)srv文件内容如下:
#客户端请求时发送的两个数字:Request
int32 num1
int32 num2
---#这里用---来直接区别Request和Response
#服务器响应发送的数据:Response
int32 sum
(注意:int末尾没有;)
2、编辑配置文件
(1)在package.xml文件中第55行添加<build_depend>message_generation</build_depend>
第65行添加<exec_depend>message_runtime</exec_depend>
(2)在CMakeLists.txt文件中第14行,添加依赖message_generation
(3)在CMakeLists.txt文件中第58行:解除注释并改为Addints.srv
(4)在CMakeLists.txt文件中第71行:解除注释并添加依赖std_msgs
(5)在CMakeLists.txt文件中第108行:解除注释,并添加message_runtime
3、编译生成中间文件
在devel开发目录下
五、编写程序:
0、vscode配置:
在c_cpp_properties.json文件中"includePath"数组内添加路径:
1、编写服务端实现:
在src目录下添加.cpp文件并编写:
#include "ros/ros.h"
#include "plumbing_server_client/Addints.h"
/*
服务端实现:解析客户端提交的数据,并运算再产生响应
1.包含头文件
2.初始化ROS节点
3.创建节点句柄
4.创建服务对象
5.处理请求并产生响应
6.spin()
*/
//回调函数是处理请求的
//参数1:plumbing_server_client文件下的Addints文件下的Request,地址给指针request了
//参数2同理
bool doNums(plumbing_server_client::Addints::Request &request,
plumbing_server_client::Addints::Response &response)
{
//1.处理请求
//根据.srv文件可知,上方的request所指的结构体下有num1和num2两个参数
int num1=request.num1;
int num2=request.num2;
ROS_INFO("收到的请求数据:num1=%d,num2=%d",num1,num2);
//2.组织响应
int sum=num1+num2;
response.sum=sum; //返回给response的sum=
ROS_INFO("求和结果:sum=%d",sum);
return true;
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
//2.初始化ROS节点
ros::init(argc,argv,"Server1"); //节点名称需要保证唯一
//3.创建节点句柄
ros::NodeHandle nh;
//4.创建服务对象(参数一:话题名称;参数二:回调函数,返回布尔值;)
ros::ServiceServer server =nh.advertiseService("calculate",doNums);
ROS_INFO("服务器启动");
//5.处理请求并产生响应
//6.spin()
ros::spin();
return 0;
}
2、编写客户端实现:
#include "ros/ros.h"
#include "plumbing_server_client/Addints.h"
/*
客户端:提交两个整数,并处理响应的结果
1.包含头文件
2.初始化ROS节点
3.创建节点句柄
4.创建客户端对象
5.提交请求并处理响应
*/
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
//2.初始化ROS节点
ros::init(argc,argv,"Client1");
//3.创建节点句柄
ros::NodeHandle nh;
//4.创建客户端对象
//调用了NodeHandle函数serviceClient,首先定下范型:plumbing_server_client::Addints;
//其次参数写入话题:"calculate"(要和服务端的一样)
ros::ServiceClient client=nh.serviceClient<plumbing_server_client::Addints>("calculate");
//5.提交请求并处理响应
plumbing_server_client::Addints ai; //新建一个ai表示plumbing_server_client::Addints路径
//(1)组织请求
ai.request.num1=100;
ai.request.num2=200;
//这时.srv文件里的num1和num2被赋值了
//(2)处理响应
//调用了ServiceClient里的函数call,返回布尔值,表示是否成功
bool flag = client.call(ai);
if(flag)
{
ROS_INFO("SUCCEED");
//记得把sum的路径写清楚ai.response.sum
ROS_INFO("响应结果为:%d",ai.response.sum);
}
else
{
ROS_INFO("FAILED");
}
return 0;
}
3、编辑配置文件:
(1)在第137行添加:add_executable(server src/server.cpp)和
add_executable(client src/client.cpp)
(2)在第147行修改成:add_dependencies(server ${PROJECT_NAME}_gencpp)和add_dependencies(client ${PROJECT_NAME}_gencpp)
(3)在第150行添加:target_link_libraries(server${catkin_LIBRARIES})和
target_link_libraries(client${catkin_LIBRARIES})
4、编译并执行:
(1)服务端编写完后测试:
①启动roscore:
②在文件下,进行初始化更新,并且测试是否正常:
③测试完后,尝试发送服务和响应服务测试:
在另一个命令窗口重新更新后,启动rosservice call + 话题 + 空格 + Tab补齐 + 修改数字
可以得到sum=11
在上一个命令窗口输出:收到的请求数据:num1=5,num2=6
求和结果:sum=11
(2)客户端编写完后测试:
①启动roscore:
②启动服务端:
③启动客户端并发送请求:
此时,客户端,服务端收到请求数据,并计算出sum,并发送响应。客户端收到响应结果并打印了出来。
六、程序优化
我们上面是将客户端输入的数字固定死了,但正常而言我们需要可编辑的数字,因此我们进行了如下的改进。
1、首先我们需要知道int main(int argc, char *argv[])里的argc表示的是参数个数,argv[ ]里面装的就是参数的字符串类型
(1)判断输入参数的个数是否正确:
//优化实现,获取命令中的参数
if(argc!=3)
{
ROS_INFO("提交的参数个数不对");
return 1;
}
(2)将原本的定值换成改变值:
//(1)组织请求
//atoi()将字符串类型的参数转换为int类型的参数:ascill to interger
ai.request.num1=atoi(argv[1]);
ai.request.num2=atoi(argv[2]);
(3)编译并下发执行命令:
需要注意就是在运行客户端rosrun的时候,我们需要加入两个参数(需要用空格隔开)
2、我们还要优化的是,先启动客户端,但不直接报出异常,而是挂起,等到服务器启动后,再正常请求
(1)解决方案:在ROS中内置了相关函数,这些函数可以让客户端启动后挂起,等到服务器启动后再进行请求
(2)在程序发送请求之前,我们判断一下服务器的状态:
client.waitForExistence(); //只有当服务器启动了,才会执行下面的程序
ros::service::waitForService("calculate"); //同样是判断服务器,这个需要添加话题参数
这样,当我们先启动客户端时,客户端会挂起,等待服务端启动后,客户端会接着发送请求。