本知识点将用一个实例(服务端处理客户端提交的两个数字求和,然后返回处理结果)来理解 服务通信的基本使用,大致分为三块:
1.自定义消息*.srv的编写()
2.服务端编写
3.客户端编写
一.首先创建一个工作空间和软件包
创建工作空间
mkdir -p ~/catkin_ws_srv_self/src
cd catkin_ws_srv_self
catkin_make
创建软件包
cd src
catkin_create_pkg server_client std_msg rospy roscpp
二.自定义消息*.srv的编写——srv编写其实与前面说的话题通信步骤一样,只不过改成了srv自己的东西。
(PS:这里可以完全参考ros wki上面的教程,消息创建部分,照搬就可以)
2.1编写srv文件
方式一:在终端中创建msg
cd ~/catkin_ws_srv_self/src/server_client
mkdir msg
gedit sum_ints.srv
在打开的文本,然后输入如下自己定义的信息(像结构体一样编写就行):
int32 num1
int32 num2
---
int32 sum
方式二:在vscode中创建srv
在终端中输入:
cd ~/catkin_ws_srv_self
code .
这样就打开了vscode编辑器,在~/catkin_ws_srv_self/src/plumbing_server_client/目录下,创建一个srv文件夹,在srv文件下,创建一个sum_ints.srv文件,在输入自己定义的信息:
int32 num1
int32 num2
---
int32 sum
最终效果如下:
以上两种方式实现效果一样,但是显然vscode的交互界面舒服。
2.2编辑配置文件(简记:231——package.xml改动两项,CMakeLists.txt改动三项,放开注释一项)
2.2.1首先在package.xml中添加编译依赖与执行依赖
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
2.2.2然后在CMakeLists.txt编辑srv相关配置
# 不要直接复制这一大段,只需将message_generation加在括号闭合前即可
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
#执行时依赖
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES demo02_talker_listener
CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)
#配置srv的源文件
add_service_files(
FILES
num_sum.srv//这里放你自己定义的消息文件名
)
# 生成消息时依赖于 std_msgs
generate_messages(
DEPENDENCIES
std_msgs
)
好了,至此已经自定义消息配置完了。但是还没完,编译完了之后,还需要继续配置环境。
2.3编译
编译之后,会产生一些中间文件(后续调用相关 msg 时,是从这些中间文件调用的),但我只关注头文件在创建的工作空间的开发空间下 include的文件夹里 即(~/catkin_ws_example/devel/inlcude下)。
因为这是自己定义的消息,所以现在需要做的是,把这个头文件包含进本工作空间的路径里面(在c_cpp_properties.json 里的 includepath属性引入,这样可以避免的是 在用vscode开发时,引入 自定义消息头文件,不会出现误报错误和代码无法补齐的情况)
PS:如何快速的得到这个头文件的路径呢?——这个路径可以鼠标放在include上,然后右击鼠标打开终端,输入pwd可得
引入的框架大致如下:
{
"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
}
在本例中,引入图为:
最后面 ~/include/**的意思是,引入inlcude文件夹下的所有头文件。
至此,自定义消息部分,已经配置完成。
总结一下,做了哪些事?编写:
自定义消息
然后配置package.xml,CMakeLists.txt
引入头文件
三.服务端编写
3.1在~/catkin_ws_srv_self/src/server_client/src文件夹下创建一个*.cpp文件(本例为 server.cpp),然后放如下代码:
#include<ros/ros.h>
#include "plumbing_server_client/sum_ints.h"
/*
服务端实现:解析客户端提交的数据,并运算在产生结果
1.包含头文件
2. 初始化ros节点
3.创建句柄
4.创建句柄对象(服务对象)
5.处理请求并产生响应
spin()
*/
//5.处理请求并产生响应
bool add(plumbing_server_client::sum_ints::Request &request,plumbing_server_client::sum_ints::Response &response)
{
/*操作主要有两步*/
//1.处理请求
int num1 = request.num1;/*问题:这里为什么用的是. 而不是->呢? ——因为函数形参列表不是以指针方式引用的,拿到的是引用,而不是指针,进一步的描述,这里的request和response不是指针,没有用ConstPtr来接收,而是对象。*/
int num2 = request.num2;
ROS_INFO("收到的请求数据:num2 = %d,num2 = %d",num1,num2);
//ROS_INFO(" 收到的数据: num1= %d,num2=%d",request.num1,request.num2);//以上三条消息,可以融合成一条打印,只是多了中间变量接收一下而已
//2.处理响应
int sum = num1+num2;
response.sum = sum;
//response.sum = request.num1+request.num2;//以上两条信息,也可以融合成一条打印,只是多了一个中间变量接收而已
ROS_INFO("求和结果:%d",sum);
// ROS_INFO("求和结果:%d",response.sum);//说明返回response.sum和sum中间变量都可以
return true;
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");//日志输出,如果用中文的话,不要忘了这一句。
// 2. 初始化ros节点
ros::init(argc,argv,"server");
//3.创建句柄
ros::NodeHandle nh ;
//4.创建句柄对象(服务对象)
ros::ServiceServer server = nh.advertiseService("add_two_ints",add) ;/*第二个参数就是回调函数,处理请求的,只要是关于有回调函数的就不用写模板函数,他会自己推导然后填写的
回调函数返回的是 布尔类型的,因为这个处理结果请求,可能性有两个,成功或失败*/
ROS_INFO("服务器端启动了!");
ros::spin();
return 0;
}
3.2配置CMakeLists.txt(需要修改三个地方,都是找到对应的地方,然后复制注释下来,修改需要改的部分即可)
第二句放的位置 有坑的说明,已经在发布的 话题通信之自定义消息 里已经说明了。
OK~发布方已经编写好了,接下来就可以编译测试了(这个模块写好了,先测试没有问题在写其他模块)。
3.3编译测试
3.3.1
方式一(终端编译方式):
cd ~/catkin_ws_srv_self
catkin_make
方式二(vscode的快捷键编译方式):
在vscode中,shift+ctrl+B
3.3.2运行发布者节点(前提是已经运行了 roscore)
打开一个新的终端:
roscore
另开一个终端:
cd ~/catkin_ws_srv_self
soure devel/setup.bash
rosrun server_client server
到这一步之后,就运行成功了,服务端已经挂起了,但是在终端看不到什么反应,但是我们可以用内置指令,在终端上提交数字让服务端计算并把结果打印出来到终端来看结果,如:
另开一个终端:输入rosservice list 可以查看当前挂起的服务(发现/two_sum为我们所需的服务),然后输入rosservice -h可以查看rosservice有什么功能(指令忘记了,可以走这一步),然后输入rosservice call /two_sum "num1:[自己随便输入数字,如1] num2:[自己随便输入数字,如2]"
(这一串指令这么长,怎么记得住?——小技巧:可以输入到rosservice call /two_sum 然后多按几次tab键盘,就能自动补齐后面的东西,PS:后面可以自己输入的数字,其实就是自己写的服务端所做的事情。)
结果如图:
验证成功~
总结一下干了哪些事?
编写*.cpp文件
配置CMakeLists.txt
四.客户端编写
以固定方式(即程序运行前,数字已经固定死)提交:
4.1在~/catkin_ws_srv_self/src/server_client/src文件夹下创建一个*.cpp文件(本例为 client.cpp),然后放如下代码:
#include<ros/ros.h>
#include "server_client/num_sum.h"
/*
客户端:提交两个整数,并处理响应的结果。
1.包含头文件;
2.初始化ros节点
3.创建节点句柄
4.创建一个客户端对象
5.提交请求并处理响应
*/
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
//2.初始化ros节点
ros::init(argc,argv,"client");
// 3.创建节点句柄
ros::NodeHandle nh;
//4.创建一个客户端对象
ros::ServiceClient client = nh.serviceClient<server_client::num_sum>("two_sum");//serviceClient有三个重载,意思是可以 形参输入有三种样式,这里使用第二个重载,有范型的那一个,范型可以就是自己创建的那个服务消息srv(具体解释可看视频第67个4min10s)
//5.提交请求并处理响应(主逻辑实现)
server_client::num_sum srv;
//5.1 组织请求
srv.request.num1 = 1;
srv.request.num2 = 2;
//5.2 处理响应
bool flag = client.call(srv); //返回值是一个布尔类型的,提交完之后,是有返回结果的,所以可以用一个bool值接收一下。如果是true就正常处理,false就是处理失败了。并且响应的结果(比如这里的sum),也封装进了这个srv对象(在这个srv对象中,有request还有response这两个属性)。
if(flag)
{
ROS_INFO("响应成功!");
//获取结果
ROS_INFO("处理结果 = %d" ,srv.response);
}
else
{
ROS_INFO("响应失败...");
}
ros::spin();
return 0;
}
4. 2配置CMakeLists.txt(需要修改三个地方,都是找到对应的地方,然后复制注释下来,修改需要改的部分即可,改的方法与发布方一样,因此这里只放了最终效果图即可,用红笔标出改了哪些部分)
shift+ctrl+B编译(或者用终端方式的编译,上面3.3.1写过了)通过,好了。可以联合调试了。
总结一下干了哪些事?
编写*.cpp文件
配置CMakeLists.xtxt
五.联合调试
5.1 编译(不会编译的见3.1)
5.2运行roscore
5.3运行服务端节点
新开一个终端:
cd ~/catkin_ws_srv_self
soure devel/setup.bash
rosrun server_client server
5.4运行客户端节点
新开一个终端:
cd ~/catkin_ws_srv_self
soure devel/setup.bash
rosrun server_client client
最后效果如图所示:
OK~完成,结束~
--------------------------------------------------------分割线--------------------------------------------------------
这里值得一提的是(主要体会思想动态提交的思想,可以从终端输入数据,然后从mian函数入口进到程序里面):上面第四部分,写了一个以固定方式(即程序运行前,数字已经固定死)提交,这里优化客户端的编写,以动态的方式提交(就是灵活的填写两个数字,都可以实现求解):
可以在client.cpp文件中,放如下代码:
//优化客户端,实现参数的动态提交
//1.终端输入格式:rosrun xxxx(包名) xxxx(节点名) 1 2
//2.节点执行时,需要获取命令中的参数,然后数据放入requerst中
#include<ros/ros.h>
#include "server_client/num_sum.h"
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
//优化实现,获取命令中的参数
/*此时argc 应该等于3(节点文件名,两个参数,所以是三个),*argv指针数组应该也是3,并且argv[0]为客户端文件名,argv[1]为终端传进来的第一个数,如1,,argv[2]为传进来的第二个数,如2*/
if(argc != 3)
{
ROS_INFO("输入个数不对...");
return 1;
}
ros::init(argc,argv,"client");
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient<server_client::num_sum>("two_sum");
server_client::num_sum srv;
srv.request.num1 = atoi(argv[1]);
srv.request.num2 = atoi(argv[2]);
bool flag = client.call(srv);
if(flag)
{
ROS_INFO("求和结果为:%d",srv.response.sum);
}
else
{
ROS_INFO("响应失败...");
return 1;
}
//return 0;
ros::spin();//经过测试,程序运行到这里,会回调(因为终端就停在那了,并没有结束,继续给输入命令,需要按ctrl+c才能结束)。然后就不经过 return 0了。 ( return 0为正常停止,return 1为非正常停止的意思。)
return 0;
}
然后就可以开始调试了。
1. 编译(不会编译的见3.1)
2。运行roscore
3运行服务端节点
新开一个终端:
cd ~/catkin_ws_srv_self
soure devel/setup.bash
rosrun server_client server
4。运行客户端节点
新开一个终端:
cd ~/catkin_ws_srv_self
soure devel/setup.bash
rosrun server_client client
最后效果如图所示(如图演示了 两组动态提交数据,分别是 1和2 ,5和10):
OK~完成,结束~