目录
一、概念
服务通信涉及两个角色:
服务服务器 (Service Server):
提供特定功能或数据的节点。
它会“打广告”说:“我能提供XX服务!”(例如,“我能计算两个整数的和”)。
它平时处于待命状态,一旦接收到请求,就执行相应的任务,并返回一个结果。
服务客户端 (Service Client):
需要某个特定功能或数据的节点。
它会向服务器发送一个具体的请求(例如,“请帮我计算 3 和 5 的和”)。
发送请求后,它会暂停自己的工作,一直等待,直到收到服务器的响应。
二、何时使用服务
当你需要立即得到一个确切的答复或触发一个必须完成的远程操作时,就应该使用服务。
典型应用场景:
查询数据: “机器人,你现在的坐标是多少?”
触发动作: “机械臂,移动到指定位置。” / “相机,拍一张照片。”
执行计算: “路径规划器,帮我计算一条从A到B的最优路径。”
更改状态: “机器人,切换到自动驾驶模式。”
三、话题通信与服务通信的区别
服务通信 (Service) | 话题通信 (Topic) | |
通信模型 | 请求/响应 | 发布/订阅 |
数据流向 | 双向 | 单向 |
同步性 | 同步 - 客户端会阻塞等待 | 异步 - 发布后立即返回 |
连接关系 | 一对一 | 一对多/多对多 |
数据流类型 | 离散的、事务性的 | 连续的数据流 |
主要目的 | 执行远程调用、获取确切结果 | 持续广播状态、传感器数据 |
四、案例
客户端发送两个整数,服务端计算它们的和并返回
4.1 C++实现
4.1.1 服务端
进入到工作空间的src目录下,输入如下指令来创建一个名为“plumbing_server_client”的功能包
catkin_create_pkg plumbing_server_client roscpp rospy std_msgs
在功能包中创建一个srv目录
在srv目录中创建一个.srv文件,这里命名为“AddTwoInts.srv”
“AddTwoInts.srv”内容如下:
int64 a
int64 b
---
int64 sum
打开功能包中的“package.xml”,添加如下两行内容
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
打开功能包下的“CMakeLists.txt”,添加如下内容:
添加“message_generation”
取消注释并添加“AddTwoInts.srv”
取消注释
取消注释
Ctrl+Shift+B编译一下
在功能包的src目录中新建一个.cpp文件,这里命名为“server.cpp”
在“server.cpp” 中添加如下代码
#include "ros/ros.h"
#include "plumbing_server_client/AddTwoInts.h"
// 服务处理函数。当收到请求时,ROS会调用这个函数。
// 函数的返回值是 bool 类型,如果成功处理返回 true,否则返回 false。
// 参数是请求对象(req)和响应对象(res)的引用。
bool add(plumbing_server_client::AddTwoInts::Request &req,
plumbing_server_client::AddTwoInts::Response &res)
{
// 从请求对象中取出数据
res.sum = req.a + req.b;
// ROS_INFO 用于在终端打印日志信息,类似于C++的cout
ROS_INFO("请求: x=%ld, y=%ld", (long int)req.a, (long int)req.b);
ROS_INFO("发送响应: sum=%ld", (long int)res.sum);
return true; // 表示服务成功执行
}
int main(int argc, char **argv)
{
setlocale(LC_ALL,"");
// 1. 初始化ROS节点
ros::init(argc, argv, "add_two_ints_server");
// 2. 创建节点句柄
ros::NodeHandle n;
// 3. 创建一个名为 "add_two_ints" 的服务
// 它会调用 add 函数来处理请求
ros::ServiceServer service = n.advertiseService("add_two_ints", add);
ROS_INFO("服务已就绪,等待客户端请求...");
// 4. 进入循环,等待回调函数的触发
ros::spin();
return 0;
}
4.1.2 客户端
在功能包的src目录下添加一个.cpp文件,这里命名为“client.cpp”
在“client.cpp”中添加如下代码
#include "ros/ros.h"
#include "plumbing_server_client/AddTwoInts.h"
#include <cstdlib> // 用于 atoll 函数
int main(int argc, char **argv)
{
setlocale(LC_ALL,"");
// 初始化ROS节点
ros::init(argc, argv, "add_two_ints_client");
// 检查命令行参数是否正确
if (argc != 3)
{
ROS_INFO("用法: add_two_ints_client X Y");
return 1;
}
// 创建节点句柄
ros::NodeHandle n;
// 创建一个客户端,连接到名为 "add_two_ints" 的服务
// serviceClient 会一直尝试连接,直到成功
ros::ServiceClient client = n.serviceClient<plumbing_server_client::AddTwoInts>("add_two_ints");
// 创建一个服务对象 srv
plumbing_server_client::AddTwoInts srv;
// 将命令行参数转换为 long long (int64) 并填充到请求中
srv.request.a = atoll(argv[1]);
srv.request.b = atoll(argv[2]);
// 调用服务
// client.call() 是一个阻塞操作。它会发送请求并等待,直到收到响应。
// 如果服务调用成功,call() 返回 true,响应数据会填充到 srv.response 中。
// 如果失败,call() 返回 false。
if (client.call(srv))
{
ROS_INFO("响应 Sum: %ld", (long int)srv.response.sum);
}
else
{
ROS_ERROR("调用服务失败");
return 1;
}
return 0;
}
打开功能包中“CMakeLists.txt”文件,添加如下内容
编译一下
4.1.3 测试执行
开启三个终端,分别输入如下指令来依次启动ros核心、启动服务端、启动客户端
roscore //启动ros核心
rosrun plumbing_server_client server //启动服务端
rosrun plumbing_server_client client 15 20 //启动客户端
可以看到服务端成功计算并返回计算结果。
但是如果我们先开启客户端再开启服务端就会导致客户端抛出异常:
为了避免这个问题,我们可以给客户端添加如下一行代码,这样就可以客户端就可以等服务端启动后再请求
client.waitForExistence();
// 或
ros::service::waitForService("add_two_ints_server"); //add_two_ints_server为服务器节点名称
可以看到客户端在服务端未启动时一直等待
等服务端启动后再执行请求
4.2 Python实现
4.2.1 服务端
打开“settings.json”,如果没有如下配置则需要补充
{
"editor.tabSize": 4,
"cmake.sourceDirectory": "/home/chaochao/demo02_ws/src/helloworld",
"files.associations": {
"sstream": "cpp"
},
"python.autoComplete.extraPaths": [
"/opt/ros/noetic/lib/python3/dist-packages",
"/home/chaochao/demo02_ws/devel/lib/python3/dist-packages"
]
}
新建一个文件夹“scripts”
添加一个Python文件,这里命名为“server_py.py”
在“server_py.py”添加如下代码:
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from plumbing_server_client.srv import AddTwoInts, AddTwoIntsRequest, AddTwoIntsResponse
# from plumbing_server_client.srv import *
"""
服务端:解析客户端请求,产生响应
1. 导包
2. 初始化 ROS 节点;
3. 创建服务端对象;
4. 处理逻辑(回调函数)
5. spin()
"""
# 4. 处理逻辑
def call_doInt(request):
num1 = request.a
num2 = request.b
sum = num1 + num2
response = AddTwoIntsResponse() # 将结果封装进response
response.sum = sum
rospy.loginfo("服务器收到:num1=%d, num2=%d, 响应:sum=%d", num1, num2, sum)
return response
if __name__ == "__main__":
# 2. 初始化 ROS 节点;
rospy.init_node("server")
# 3. 创建服务端对象;
server = rospy.Service("AddTwoInts", AddTwoInts, call_doInt) # "AddTwoInts":服务的名称; AddTwoInts:服务的类型; call_doInt:回调函数;
# 5. spin()
rospy.spin()
cd到“scripts”目录下,输入如下指令来给python文件添加可执行权限
chmod +x *.py
打开“CMakeLists.txt”,添加如下内容
编译一下,然后输入如下指令测试
source ./devel/setup.bash
rosrun plumbing_server_client server_py.py
source ./devel/setup.bash
rosservice call AddTwoInts "a: 12 b: 14"
4.2.2 客户端
在“scripts”目录下新建一个python文件,这里命名为“client_py.py”
在“client_py.py”中添加如下代码
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from plumbing_server_client.srv import AddTwoInts, AddTwoIntsRequest, AddTwoIntsResponse
"""
客户端:组织并提交请求,处理服务端响应。
1. 导包;
2. 初始化 ROS 节点;
3. 创建客户端对象;
4. 组织请求数据,并发送请求;
5. 处理响应。
"""
if __name__ == "__main__":
# 2. 初始化 ROS 节点;
rospy.init_node("client")
# 3. 创建客户端对象;
client = rospy.ServiceProxy("AddTwoInts", AddTwoInts)
# 4. 组织请求数据,并发送请求;
response = client.call(12, 24)
# 5. 处理响应。
rospy.loginfo("响应的数据:%d", response.sum)
给新建的python文件添加可执行权限
打开“CMakeLists.txt”,添加如下内容
编译后通过如下命令启动服务端和客户端,可以看到客户端正常接收到了服务端响应的数据
rosrun plumbing_server_client server_py.py //启动服务端
rosrun plumbing_server_client client_py.py //启动客户端
4.2.3 客户端优化——动态传参
如果我们希望客户端的参数是通过命令动态传入的,可以对客户端代码做如下修改:
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import rospy, sys
from plumbing_server_client.srv import AddTwoInts, AddTwoIntsRequest, AddTwoIntsResponse
"""
客户端:组织并提交请求,处理服务端响应。
1. 导包;
2. 初始化 ROS 节点;
3. 创建客户端对象;
4. 组织请求数据,并发送请求;
5. 处理响应。
"""
if __name__ == "__main__":
# 判断参数个数
if len(sys.argv) != 3:
rospy.logerr("传入参数个数有误...")
sys.exit(1)
# 2. 初始化 ROS 节点;
rospy.init_node("client")
# 3. 创建客户端对象;
client = rospy.ServiceProxy("AddTwoInts", AddTwoInts)
# 4. 组织请求数据,并发送请求;
num1 = int(sys.argv[1])
num2 = int(sys.argv[2])
response = client.call(num1, num2)
# 5. 处理响应。
rospy.loginfo("响应的数据:%d", response.sum)
此时执行效果如下
4.2.4 客户端优化——等待服务端启动后再发起请求
如果在服务端未启动的情况下启动客户端,会抛出异常
为了解决这个问题,我们可以添加如下一行代码,这样客户端就可以等待服务端启动后再发起请求
client.wait_for_service()
# 或
rospy.wait_for_service("AddTwoInts")