ROS服务通信,最详细最易懂(从文件构建到原理解析)_ros service 和client 实时性如何

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

话题通信这种无应答的通信方式虽实时性不高,但是传输效率明显高于服务通信;服务通信由于多了一个回应的机制所以在效率上要低于话题通信,但是通信可以根据需求随时快速响应大大提高了实时性。

服务通信的实现

.srv文件的配置

一定要在功能包下建立名称为“srv”的文件夹,再在文件夹中创建.srv文件,否则编译失败,文件内容格式如下:

int32 num1  
int32 num1  
---  
int32 sum 

注:由于服务通信有clientàserver的响应以及serveràclient的回复,因此我们需要分别对两种消息进行数据类型设置,两种数据类型设置以”—”三个小杠为分界线。

CMakelist.txt编译器配置文件的配置

包含编译自定义功能包所需的符合ROS标准的功能包
find_package(catkin REQUIRED COMPONENTS  
  roscpp  
  rospy  
  std_msgs  
  message_generation  
) 
将符合ROS标准功能包编译成符合CMake标准的功能包
catkin_package(  
#  INCLUDE_DIRS include  
#  LIBRARIES hello_ros  
 CATKIN_DEPENDS roscpp rospy std_msgs message_runtime  
#  DEPENDS system_lib  
)  
添加要被编译的.srv文件
​## Generate services in the 'srv' folder  
add_service_files(  
  FILES  
  Addlints.srv  
) 
添加处理.srv文件的功能包
generate_messages(  
  DEPENDENCIES  
  std_msgs  
) 
声明C++可执行文件
generate_messages(  
  DEPENDENCIES  
  std_msgs  
)  
指定要链接库或可执行目标的库
target_link_libraries(srv_num  
  ${catkin_LIBRARIES}  
)  
target_link_libraries(cli_num  
  ${catkin_LIBRARIES}  
)  

该段代码的功能是:链接程序是将有关的目标文件批次链接,也即将一个文件中引用的符号同该符号所在的另外一个文件中的定义链接起来,使得所有的这些目标文件成为一个能够操作系统装入执行的统一整体。

添加源文件运行前需要提前编译的文件

代码格式

add_dependencies(cli_num ${PROJECT_NAME}_gencpp)  
add_dependencies(srv_num ${PROJECT_NAME}_gencpp)  

由于我们的.cpp源文件包含了.srv/.msg编译生成的.h头文件,因此我们需要在编译.cpp源文件之前首先编译.srv/.msg文件,使得由.srv/.msg文件生成的对应.h头文件先于.cpp源文件而编译。

代码位置

确保在正确的地方使用了正确的add dependencies()调用。如果您仔细阅读了CMakeList.txt,您会发现两个add dependencies()调用发生在两个不同的地方。一个在add executable()之前,另一个在之后。第一个是“Add cmake target dependencies of The library”,第二个是“Add cmake target dependencies of The executable”。很明显,如果我要在.cpp源文件执行之前先执行对.srv/.msg的编译就要将add dependencies()的调用放在add executable()之后。要是放在add executable()之前,那就报出如下错误:

Cannot add target-level dependencies to non-existent target “srv_num”.

目标文件是什么?

编译器编译源代码后生成的文件叫做目标文件。从文件结构上来讲,目标文件已经是二进制文件。编译是针对单个源文件的,有几个源文件就会生成几个目标文件,并且在生成过程中不受其他源文件的影响。也就是说,不管当前工程中有多少个源文件,编译器每次只编译一个源文件、生成一个目标文件。

链接是什么?静态链接、动态链接是什么,它们具体做了什么?

静态链接方式:在程序执行之前完成所有的组装工作,生成一个可执行的目标文件(EXE文件),再次方式下,只要一个文件包含库,就将库拷贝至内存中不管是否重复拷贝,这一步仅仅将多个二进制文件物理的组装起来,实际上是在为代码做加法;

动态链接方式:在程序已经为了执行被装入内存之后完成链接工作,并且在内存中一般只保留该编译单元的一份拷贝,也就是说动态连接方式的作用是将程序中的冗余库删除,当多个文件引用一个库时,在内存中只拷贝一份库而不是有几个引用拷贝几个,实际上是在为代码做减法。

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

其实package.xml文件的作用就相当于告诉系统“在编译我自定义的.cpp源文件和消息文件时,我要用到那些包,即依赖包”。比如我们需要roscpp,rospy,std_msgs,message_generation功能包来将我们使用C++/Python编写的非ROS标准功能包转化为标准ROS功能包,那我们就必须向编译器声明我需要roscpp,rospy,std_msgs,message_generation功能包。

<build_depend>roscpp</build_depend>  // 在基础ROS项目上创建功能包时所设置的依赖项
<build_depend>rospy</build_depend>  // 在基础ROS项目上创建功能包时所设置的依赖项
<build_depend>std_msgs</build_depend>  // 在基础ROS项目上创建功能包时所设置的依赖项
<build_depend>message_generation</build_depend> // 处理自定义消息文件时需人为添加

然后,我们还要设置运行ROS标准功能包时所需的功能包,即运行功能包的依赖包:

<exec_depend>roscpp</exec_depend>  
<exec_depend>rospy</exec_depend>  
<exec_depend>std_msgs</exec_depend>  
<exec_depend>message_runtime</exec_depend>  

上述roscpp,rospy,std_msgs,message_runtime这四个功能包使得ROS标准功能包最终被编译成CMake标准下的可执行文件。

Package.xml和CMakelist.txt文件的区别

Package.xml文件向编译器说明了“在使用ROS标准/CMake标准进行编译时分别所需的功能包”,但是有是一回事用又是另一回事,package.xml只告诉编译器我需要这些东西,但又没指明“我需要如何操作这些功能包,这些功能包的执行顺序又是什么样的“,CMakelist.txt文件给我们指明了这一点,在CMakelist.txt文件中find_package告知系统我编译功能包时真正需要哪些功能包,当然了这些功能包必须必须先在package.xml文件被声明才行,二者关系如下:

在CMakelist.txt中主要指明了以下几点:

① 在使用ROS标准编译该.cpp源文件之前需要先准备好什么功能包;

② 在使用CMake标准编译ROS标准文件时需要先准备好什么功能包;

③ 自定义消息文件是什么,将这些自定义ROS标准消息文件转化成可以嵌入.cpp源文件的.h文件需要什么功能包;

④ 我需要编译执行的操作对象有哪些,即我需要处理那些.cpp源文件,以及我需要将那些文件链接哪些CMake标准下的目标文件,最终一个项目会被编译链接生成一个可执行目标文件。

而package.xml文件主要指明了以下几点:

① 你别管我如何用这些功能包,我将.cpp源文件编译成ROS标准文件需要这些功能包,你给我准备好就行,具体怎么做我也不知道,但是我就需要这些功能包;

② 你别管我如何用这些功能包,我将ROS标准文件编译成CMake标准下的二进制可执行目标文件需要这些功能包,你给我准备好就行,具体怎么做我也不知道,但是我就需要这些功能包。

功能包文件编译程序流程

注意:第一个是将ROS中.srv/.msg文件编译生成可以嵌入C++源文件的.h头文件,第二个才是将ROS项目中src文件夹下的所有.cpp源文件进行链接编译最终生成可执行的二进制文件。

代码预期实现功能

服务端代码编写——srv_num.cpp

#include "ros/ros.h"  
#include "hello_ros/Addlints.h"  
  
bool DoSum(hello_ros::Addlints::Request& request,  
            hello_ros::Addlints::Response& response)  
{  
    int num1 = request.num1,num2 = request.num2;  
    ROS_INFO("request:%d,%d\n\t",num1,num2);  
    response.sum = num1 + num2;  
    ROS_INFO("response:%d+%d=%d",num1,num2,response.sum);  
  
    return true;  
}  
  
int main(int argc,char* argv[])  
{  
    // locate language  
    setlocale(LC_ALL,"");  
    // initial ROS node  
    ros::init(argc,argv,"srv_object1");  
    // set NodeHandler  
    ros::NodeHandle nh;  
    // create server object  
    ros::ServiceServer service1 = nh.advertiseService<hello_ros::Addlints::Request,  
        hello_ros::Addlints::Response>("Addint",DoSum);  
    // ROS_INFO  
    ROS_INFO("service has been started......");  
    // Callback mechanism  
    ros::spin();  
  
    return 0;  
} 

Server/client的数据通信流程

Client与server信息之间的交互逻辑

有些人不明白为什么执行如下的赋值代码就可以将client的请求发送给server:

hello_ros::Addlints ai;  
ai.request.num1 = atoi(argv[1]);  
ai.request.num2 = atoi(argv[2]); 

其实我们编译完我们的自定义.srv服务消息文件之后,devel/include文件夹下会出现三个.h头文件:

其中Addlints.h是给我们封装好了的,我们直接用这个就可以了,但是其他两个.h文件怎么用呢?这就涉及到数据的交互问题,下面是简单的原理(说明了代码编写的逻辑):

这是封装之后的简单原理图,我们知道server和client之间是通过topic相互联系的,因此server和client之间可以以topic为为中间媒介建立一下模型(说明了server和client如何实时同步信息的):

在整个通信过程中我们所需要操作的部分如下:

我们需要做的就是:

① server端:编写回调函数从函数入口参数中的AddlintsRequest对象中提取request并将其在函数体中处理成response,并将其再次传给函数入口参数AddlintsResponse对象;

② client端:自定义Addlints对象,将request所需的所有信息赋值给Addlints对象中的子类对象request完成对自定义请求消息类型的封装,然后从Addlints对象中的子类对象response中提取server返回给client的response。

client向master上报的数据类型

当我们看了advertiseService函数的重载时,我们就会发现,advertiseService函数需要实例化模板参数,分别是类类型:请求数据类型和响应数据类型。代码如下:

ros::ServiceServer service1 = nh.advertiseService<hello_ros::Addlints::Request,hello_ros::Addlints::Response>("Addint",DoSum);  

上述代码的含义:将request的数据类型和response的数据类型以及topic服务通信主题名称上报给master主节点,从而在master中注册信息。值得注意的是,节点句柄是操作节点和读取关于节点的属性的,例如操作节点与主节点之间通信(即将节点的信息在master主节点中进行注册),读取通过命令行了解节点从命令行中获取的参数等。

回调函数的编写
bool DoSum(hello_ros::Addlints::Request& request,  
            hello_ros::Addlints::Response& response)  
{  
    int num1 = request.num1,num2 = request.num2;  
    ROS_INFO("request:%d,%d\n\t",num1,num2);  
    response.sum = num1 + num2;  
    ROS_INFO("response:%d+%d=%d",num1,num2,response.sum);  
  
    return true;  
} 

首先,函数返回值需要时bool类型的,其中true代表server接收client客户端请求成功并且成功调用回调函数处理client传来的请求信息,false则代表server没有接收到client客户端请求或者处理从client发来的信息时出现异常导致失败。

其次,函数的入口参数要和server服务端所能接收的request和response信息数据类型一致,其中server服务端在master中注册的request和response信息数据类型就是我们下面程序中的实例化模板参数:

ros::ServiceServer service1 = nh.advertiseService<hello_ros::Addlints::Request,hello_ros::Addlints::Response>("Addint",DoSum);  

其实,我们可以把回调函数看作一个“发酵罐”,进去的是酿酒的原料(request)出来的就是成品酒(response),关于“将来自client的request处理成server返回给client的response“的形象化比喻如下所示:

可以将回调函数与client直接的关系也进行如下形象化比喻:

当从client读取到“server处理请求的相关情况(失败/成功)“,我们可以下一步操作。

回调函数参数为何为类对象的引用,仅仅传入函数对象不行吗?

我们前面提到过:

我们要将函数值处理的结果(即response)在装进Addlints自定义类类型对象之中的话,我们必须要将函数参数设置为引用,这样当函数体内部改变了传入参数的数值,传入参数的数值也会发生改变,这就涉及到C++引用传参的具体知识了。

但是传入参数的引用,也导致了一些问题:我们可以在server端的回调函数中修改client传给server的request。具体代码如下:

bool DoSum(hello_ros::Addlints::Request& request,  
            hello_ros::Addlints::Response& response)  
{  
    request.num1 = 0; request.num2 = 0;  
    ROS_INFO("request have been changed:%d,%d\n\t",request.num1,request.num2 );  
    int num1 = request.num1,num2 = request.num2;  
    ROS_INFO("request:%d,%d\n\t",num1,num2);  
    response.sum = num1 + num2;  
    ROS_INFO("response:%d+%d=%d",num1,num2,response.sum);  
  
    return true;  
}  

客户端响应相应如下:

服务器端相应如下:

上面的响应结果告诉我们:由于相互连接的server和client有相同的topic,这样就会导致一端改变全都改变的局面,即我们居然从server端修改了从client发送过来的request,这一下子就会导致“现在的request已不同于client最初发送的request了“,就相当于“我原来给机械臂发送了向左转的信号,但是由于server端将request进行了误修改导致机械臂的响应是向右转”。

那我们该如何防止在函数体内部的误修改呢?

我们函数体内部将函数入口参数赋值给我们函数体的内部变量,我们禁止改变的变量就赋值给const类型的类类型的引用,我们想改变的可以不进行任何多于操作,具体代码实现如下:

bool DoSum(hello_ros::Addlints::Request& request,  
            hello_ros::Addlints::Response& response)  
{  
    const hello_ros::Addlints::Request& rqu = request;  


![img](https://img-blog.csdnimg.cn/img_convert/ef60540b862c1f7630d40ed4902d5e03.png)
![img](https://img-blog.csdnimg.cn/img_convert/cedef7fa0c78412d88510b58a6e5aa2b.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**

操作,具体代码实现如下:




bool DoSum(hello_ros::Addlints::Request& request,  
            hello_ros::Addlints::Response& response)  
{  
    const hello_ros::Addlints::Request& rqu = request;

[外链图片转存中…(img-x6UQpHxX-1715842153823)]
[外链图片转存中…(img-alUC1eqo-1715842153824)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值