ROS学习笔记(4):通信机制之服务通信

ROS学习笔记(4):通信机制之服务通信

StarHui Lv3

服务通信

前面介绍了话题通信,虽然使用频率较高,但是有一个小小的不足之处,就是话题通信的数据是单向传输的,订阅者被动接收发布者的消息。那么如果想要主动接收数据怎么办?这就要好好介绍今天的主角--服务通信了。

服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。适用于偶然的、对实时性有要求、有一定逻辑处理需求的数据传输场景

理论模型

该模型中涉及三个角色:ROS Master(管理者)Server(服务端)Client(客户端)

  1. Server向ROS Master注册,有PRC、ROSRPC地址

  2. Client向ROS Master查询 指定服务

  3. ROS Master进行匹配,匹配成功返回Server的 ROSRPC URL

  4. Client向Server发送请求数据

  5. Server向Client返回响应数据

注意:Server不需要协商的过程,直接返回ROSPRC地址。

服务通信与话题通信的区别如下图

详细区别请看该问答:ROS Topic 和 ROS Service 有哪些区别?

自定义srv

在使用服务通信的时候,数据载体是srv,和msg一样,需要我们手动创建。
srv 文件内的可用数据类型与 msg 文件一致,且定义 srv 实现流程与自定义 msg 实现流程类似:

  1. 按照固定格式创建srv文件

  2. 编辑配置文件

  3. 编译生成中间文件

咱们就用一个实例来进行演示。

编写服务通信,客户端提交两个整数至服务端,服务端求和并响应结果到客户端。

定义srv文件

在ROS包下新建一个srv文件夹,在srv文件夹里面新建一个srv文件

1
2
3
4
5
6
7
8
9
10
11
12
#这里我又新建了一个工作空间、ROS包,你们可以选择在原来的工作空间内新建一个ROS包
mkdir -p service_communication/src
cd service_communication/
catkin_make

cd src
catkin_create_pkg request_response roscpp rospy std_msgs

cd request_response/
mkdir srv
cd srv
vim AddTwoNum.srv

服务通信中,数据分成两部分,请求与响应,在 srv 文件中请求和响应使用 --- 分割.
AddTwoNum.srv内容如下

1
2
3
4
int32 num1
int32 num2
---
int32 sum

编辑配置文件

和自定义msg编辑配置文件差不多,不同的是添加的是自定义srv,而不是自定义msg
首先回到ROS包下,修改 package.xml文件内容,添加编译依赖项、执行依赖项

1
2
#此时路径:~/service_communication/src/request_response/srv
vim ../package.xml


下面修改ROS包下的CMakeLists.txt文件

1
2
#此时路径:~/service_communication/src/request_response/srv
vim ../CMakeLists.txt

添加CMake编译时找到依赖的包 message_generation

添加自定义srv文件 AddTwoNum.srv

生成srv

添加了当前项目依赖的其他catkin项目

查看srv中间文件

回到工作空间下,编译

1
2
cd ~/service_communication/
catkin_make

编译成功。
可以在工作空间下的devel文件夹下查找中间文件。
C++可以用的中间文件在 ~/service_communication/devel/include/request_response
其中service_communication为工作空间名字,request_response为ROS包的名字。

Python可以用的中间文件在 ~/service_communication/devel/lib/python3/dist-packages/request_response/srv

可以看一下官方示例:创建msg、srv官方示例

C++服务通信实例

再次明确一下目标,编写服务通信,客户端提交两个整数至服务端,服务端求和并响应结果到客户端

Server实现

首先在ROS包下的src文件夹里面创建 cpp文件,用于实现Server。

1
2
cd ~/service_communication/src/request_response/src/
vim server.cpp

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "ros/ros.h"
#include "request_response/AddTwoNum.h"

//回调函数,处理成功返回true
bool handle_request(request_response::AddTwoNum::Request& req,
request_response::AddTwoNum::Response& resp)
{
//根据业务逻辑,写两数之和。req为请求数据,resp为返回数据
int num1 = req.num1;
int num2 = req.num2;

ROS_INFO("服务器接收到的请求数据:num1 = %d,num2 = %d",num1,num2);

resp.sum = num1 + num2;

ROS_INFO("求和结果为:sum = %d",resp.sum);

return true;
}

int main(int argc, char *argv[])
{
//设置中文编码
setlocale(LC_ALL,"");

//初始化节点 server_cpp
ros::init(argc,argv,"server_cpp");

//创建节点句柄
ros::NodeHandle nh;

//创建服务端
ros::ServiceServer server = nh.advertiseService("AddInts",handle_request);

ROS_INFO("服务器启动......");
//处理节点的循环事件,循环等待并调用回调函数
ros::spin();

return 0;
}

对于这段代码,需要解释的有以下内容

1
ros::ServiceServer server = nh.advertiseService("AddInts",handle_request);

首先ros::ServiceServer是server的数据类型,是用来提供服务的服务器端接口。server由nh.advertiseService方法创建。
advertiseService有9个重载,这里我们用的是第二个。第一个参数为创建服务的名字,第二个参数为回调函数,用于处理客户端请求

关于回调函数,传递的两个参数分别为 请求数据、响应数据。


接下来就是修改ROS包下的CMakeLists.txt了

add_dependencies(server_cpp ${PROJECT_NAME}_gencpp)这一行是为了防止srv文件没有编译完就直接编译cpp文件而导致报错。

返回工作空间,编译、刷新环境变量,运行节点

但是这时候没有请求服务器,看不到什么东西,此时可以使用rosservice call命令来请求。

1
2
rosservice call [service] [args]
#把[service]替换为自己的服务名字,然后空格 再按tab键会自动补全参数,只需要修改参数值即可

Client实现

首先在ROS包下的src文件夹里面创建 cpp文件,用于实现Client。

1
2
cd ~/service_communication/src/request_response/src/
vim client.cpp

文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include "ros/ros.h"
#include "request_response/AddTwoNum.h"

int main(int argc, char *argv[])
{
//设置中文编码
setlocale(LC_ALL,"");

//初始化节点 client_cpp
ros::init(argc,argv,"client_cpp");

//创建节点句柄
ros::NodeHandle nh;

//创建客户端
ros::ServiceClient client = nh.serviceClient<request_response::AddTwoNum>("AddInts");

//定义数据
request_response::AddTwoNum data;
data.request.num1 = 100;
data.request.num2 = 200;

//向服务器发送请求数据,返回值是bool类型。true表示响应成功,false表示响应失败。响应成功后,结果会在 data.response.sum里面
bool flag = client.call(data);

//处理响应数据
if(flag)
ROS_INFO("请求正常处理,响应结果:%d",data.response.sum);
else
{
ROS_ERROR("请求处理失败....");
return 1;
}

return 0;
}

这里,为了快速检验框架是否正确,直接把请求数据写好了,不是根据命令行传参的,后面会改进的。
对于这段代码,有如下几点需要解释。

1
ros::ServiceClient client = nh.serviceClient<request_response::AddTwoNum>("AddInts");

ros::ServiceClient表明client的的数据类型,提供基于句柄的接口,为客户端连接提供服务。
client由 serviceClient方法创建,有3个重载,我们使用的是第二个。<>里面的是服务类型,即自己生成的类;第一个参数为要连接的服务的名称

1
2
3
request_response::AddTwoNum data;
data.request.num1 = 100;
data.request.num2 = 200;

request_response::AddTwoNum为自己定义的srv生成的类,下面有request、response两个成员。而request有num1、num2两个成员变量,它们表示要发送的请求数据。


然后修改ROS包下的CMakeLists.txt文件。

回到工作空间编译,刷新环境变量,运行节点。

可以看到,Client、Server都是没问题的。接下来在这个框架下面优化即可。


前面咱们直接给请求数据赋值了,这是不符合要求的,我们需要根据命令行的参数去发送请求信息。
运行client_cpp节点的命令应该是下面这个格式

1
2
rosrun request_response client_cpp num1 num2
#num1、num2替换为实际值

这时候就需要用到main函数的两个参数argc、argv了。

argc(Argument Count):表示命令行参数的数量。它是一个整数,并且至少为 1,因为第一个参数总是程序的名称或者路径。
argv(Argument Vector):是一个指向字符串数组的指针,这个数组包含了每个命令行参数的具体值。argv[0] 是程序的名称,argv[1] 是传递给程序的第一个参数,以此类推,直到 argv[argc-1]。数组的最后一个元素 argv[argc] 是一个空指针(NULL),用于标识数组的结束。 根据这两个,咱们重新写一些数据部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include "ros/ros.h"
#include "request_response/AddTwoNum.h"

int main(int argc, char *argv[])
{
//设置中文编码
setlocale(LC_ALL,"");

if(argc != 3)
{
ROS_INFO("请提交两个整数!");
return 1;
}

//初始化节点 client_cpp
ros::init(argc,argv,"client_cpp");

//创建节点句柄
ros::NodeHandle nh;

//创建客户端
ros::ServiceClient client = nh.serviceClient<request_response::AddTwoNum>("AddInts");

//定义数据
request_response::AddTwoNum data;
data.request.num1 = atoi(argv[1]); //atoi函数把字符类型转换为整数
data.request.num2 = atoi(argv[2]);

//向服务器发送请求数据,返回值是bool类型。true表示响应成功,false表示响应失败
bool flag = client.call(data);

//处理响应数据
if(flag)
ROS_INFO("请求正常处理,响应结果:%d",data.response.sum);
else
{
ROS_ERROR("请求处理失败....");
return 1;
}

return 0;
}

看效果图

注意:这里是先启动了服务端,再启动客户端。如果先启动客户端的话,会报错,因为服务端还未启动。

这里咱们可以使用 client.waitForExistence 函数等待服务端启动 (client.waitForExistence)[https://docs.ros.org/en/noetic/api/roscpp/html/classros_1_1ServiceClient.html#a530e3f1d55cf50ab00b8e5bbd9e8230a]

或者使用 ros::service::waitForService("AddInts"); "AddInts"为服务名字。

ros::service::waitForService()

Python服务通信实例

还是上面的需求,用Pyhton来实现一下。

Server实现

首先在ROS包下新建一个scripts文件夹,然后在该文件夹内创建一个python文件。

1
2
3
4
cd ~/service_communication/src/request_response/
mkdir scripts
cd scripts
vim server.py

文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#! /usr//env binpython

import rospy
from request_response.srv import AddTwoNum,AddTwoNumRequest,AddTwoNumResponse

"""
回调函数,用于处理请求数据
参数:请求数据
返回:响应数据
"""
def handle_request(request):
#取出请求数据
num1 = request.num1
num2 = request.num2

#求和
sum = num1 + num2

rospy.loginfo("请求数据为: num1 = %d,num2 = %d;求和结果: sum = %d",num1,num2,sum)

#实例化响应数据,赋值后返回
response = AddTwoNumResponse()
response.sum = sum

return response

if __name__ == "__main__":

#初始化节点server_py
rospy.init_node("server_py")

#创建server
server = rospy.Service("AddInts_py",AddTwoNum,handle_request)

rospy.loginfo("服务已启动...")
#循环处理回调函数
rospy.spin()

1
2
server = rospy.Service("AddInts_py",AddTwoNum,handle_request)
#server = rospy.Service(name,service_class,handler,buff_size=DEFAULT_BUFF_SIZE, error_handler=None)

就是创建服务端。第一个参数为服务名字,第二个参数为srv生成的类;第三个参数为处理请求数据的函数。


修改ROS包下的CMakeLists.txt文件

运行节点,使用rosservice call测试

通过,接下来开始写client

Client实现

首先ROS包下的scripts文件夹内创建一个python文件。

1
2
cd ~/service_communication/src/request_response/scripts
vim client.py

文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import rospy
from request_response.srv import AddTwoNum,AddTwoNumRequest,AddTwoNumResponse

if __name__ == "__main__":
#初始化节点
rospy.init_node("client_py")

#创建客户端
client = rospy.ServiceProxy("AddInts_py",AddTwoNum)

#等待服务端启动
client.wait_for_service()

#向服务端发送请求
response = client.call(11,55)

rospy.loginfo("响应数据为:sum = %d",response.sum)

修改CMakeLists.txt文件

最后运行测试

这里咱们把请求数据固定了,再优化一下,可以根据命令行的参数自定义请求数据。
在sys下有一个列表可以判断参数个数,咱们利用这个来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import rospy
from request_response.srv import AddTwoNum,AddTwoNumRequest,AddTwoNumResponse
import sys

if __name__ == "__main__":
if len(sys.argv) != 3:
rospy.logerr("请输入两个参数!!!")
sys.exit(1)

#初始化节点
rospy.init_node("client_py")

#创建客户端
client = rospy.ServiceProxy("AddInts_py",AddTwoNum)

#等待服务端启动
client.wait_for_service()

#向服务端发送请求
num1 = int(sys.argv[1])
num2 = int(sys.argv[2])

response = client.call(num1,num2)

rospy.loginfo("响应数据为:sum = %d",response.sum)

通过ServiceProxy创建一个客户端,第一个参数为服务名字,第二个参数为srv生成的类

官方示例:官方示例

  • Title: ROS学习笔记(4):通信机制之服务通信
  • Author: StarHui
  • Created at : 2023-12-05 21:58:04
  • Updated at : 2023-12-05 22:34:20
  • Link: https://renyuhui0415.github.io/post/service_communication.html
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments