Skip to content

编写简单的服务端和客户端(Python)

目标: 使用 Python 创建并运行服务端(service)和客户端(client)节点。

教程级别: 初级

预计时间: 20 分钟

节点通过服务通信时,发送请求的节点称为客户端节点,响应请求的节点称为服务端节点。请求和响应的结构由 .srv 文件定义。

这里使用的示例是一个简单的整数加法系统:一个节点请求两个整数的和,另一个节点返回结果。

在之前的教程中,你已经学习了如何创建工作空间和功能包。

打开一个新终端并 source ROS 2 安装环境,以便使用 ros2 命令。

进入之前教程中创建的 ros2_ws 目录。

功能包应在 src 目录中创建,而非工作空间根目录。进入 ros2_ws/src,创建一个新功能包:

Terminal window
ros2 pkg create --build-type ament_python --license Apache-2.0 py_srvcli --dependencies rclpy example_interfaces

终端会返回一条消息,确认功能包 py_srvcli 及其所有必需的文件和文件夹已创建完成。

--dependencies 参数会自动将必要的依赖添加到 package.xml 中。example_interfaces 是包含 .srv 文件的功能包,你需要它来构造请求和响应:

Terminal window
int64 a
int64 b
---
int64 sum

前两行是请求的参数,虚线下方是响应。

由于你在创建功能包时使用了 --dependencies 选项,因此不需要手动向 package.xml 添加依赖。

不过,仍需照例填写描述、维护者姓名和邮箱以及许可证信息。

<description>Python client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache-2.0</license>

在 setup.py 文件中为 maintainer、maintainer_email、description 和 license 字段添加相同的信息:

maintainer='Your Name',
maintainer_email='you@email.com',
description='Python client server tutorial',
license='Apache-2.0',

在 ros2_ws/src/py_srvcli/py_srvcli 目录中,创建一个名为 service_member_function.py 的新文件,并粘贴以下代码:

from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
class MinimalService(Node):
def __init__(self):
super().__init__('minimal_service')
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)
def add_two_ints_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))
return response
def main():
try:
with rclpy.init():
minimal_service = MinimalService()
rclpy.spin(minimal_service)
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()

第一条 import 语句从 example_interfaces 功能包导入 AddTwoInts 服务类型。后续 import 语句导入所需的 ROS 2 Python 客户端库接口。

from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node

MinimalService 类的构造函数以名称 minimal_service 初始化节点,然后创建一个服务,并定义其类型、名称和回调。

def __init__(self):
super().__init__('minimal_service')
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)

服务回调接收请求数据,计算两数之和,并将结果作为响应返回。

def add_two_ints_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))
return response

最后,main 函数初始化 ROS 2 Python 客户端库,实例化 MinimalService 类来创建服务节点,并 spin 该节点以处理回调。

为了让 ros2 run 命令运行你的节点,你必须在 setup.py(位于 ros2_ws/src/py_srvcli 目录中)中添加入口点。

在 'console_scripts': 列表中添加以下行:

'service = py_srvcli.service_member_function:main',

在 ros2_ws/src/py_srvcli/py_srvcli 目录中,创建一个名为 client_member_function.py 的新文件,并粘贴以下代码:

from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
class MinimalClientAsync(Node):
def __init__(self):
super().__init__('minimal_client_async')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
self.req = AddTwoInts.Request()
def send_request(self):
self.req.a = 41
self.req.b = 1
return self.cli.call_async(self.req)
def main(args=None):
try:
with rclpy.init(args=args):
minimal_client = MinimalClientAsync()
future = minimal_client.send_request()
rclpy.spin_until_future_complete(minimal_client, future)
response = future.result()
minimal_client.get_logger().info(
'Result of add_two_ints: for %d + %d = %d' %
(minimal_client.req.a, minimal_client.req.b, response.sum))
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()

与服务代码一样,首先 import 必要的库。

from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node

MinimalClientAsync 类的构造函数以名称 minimal_client_async 初始化节点。构造函数创建一个与服务端类型和名称都相同的客户端——两者必须匹配,客户端和服务端才能通信。构造函数中的 while 循环每秒检查一次是否有匹配的服务可用。最后,它创建一个新的 AddTwoInts 请求对象。

def __init__(self):
super().__init__('minimal_client_async')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
self.req = AddTwoInts.Request()

构造函数下方是 send_request 方法,它发送请求,节点持续 spin 直到收到响应或调用失败。

def send_request(self):
self.req.a = 41
self.req.b = 1
return self.cli.call_async(self.req)

最后是 main 函数,它创建一个 MinimalClientAsync 对象,使用传入的命令行参数发送请求,调用 rclpy.spin_until_future_complete 等待结果,并输出最终结果。

def main(args=None):
try:
with rclpy.init(args=args):
minimal_client = MinimalClientAsync()
future = minimal_client.send_request()
rclpy.spin_until_future_complete(minimal_client, future)
response = future.result()
minimal_client.get_logger().info(
'Result of add_two_ints: for %d + %d = %d' %
(minimal_client.req.a, minimal_client.req.b, response.sum))
except (KeyboardInterrupt, ExternalShutdownException):
pass

警告:不要在 ROS 2 回调中使用 rclpy.spin_until_future_complete。更多详情请参阅同步死锁文章。

与服务节点一样,也需要添加入口点才能运行客户端节点。

你的 setup.py 文件中的 entry_points 字段应该如下所示:

entry_points={
'console_scripts': [
'service = py_srvcli.service_member_function:main',
'client = py_srvcli.client_member_function:main',
],
},

构建前,建议在工作空间(ros2_ws)根目录下运行 rosdep 来检查缺失的依赖:

Linux:

Terminal window
rosdep install -i --from-path src --rosdistro {DISTRO} -y

macOS:

rosdep 仅在 Linux 上运行,可直接跳到下一步。

Windows:

rosdep 仅在 Linux 上运行,可直接跳到下一步。

回到工作空间根目录 ros2_ws,构建新功能包:

Terminal window
colcon build --packages-select py_srvcli

打开一个新终端,进入 ros2_ws,并 source 环境配置文件:

Linux:

Terminal window
source install/setup.bash

macOS:

Terminal window
. install/setup.bash

Windows:

Terminal window
call install/setup.bat

现在运行服务节点:

Terminal window
ros2 run py_srvcli service

服务节点启动后会等待客户端的请求。

打开另一个终端,同样在 ros2_ws 中 source 环境配置文件。启动客户端节点。客户端将请求发送给服务端,服务端计算两数之和并返回结果:

Terminal window
$ ros2 run py_srvcli client
[INFO] [minimal_client_async]: Result of add_two_ints: for 41 + 1 = 42

切换到运行服务节点的终端,你会看到它在收到请求时输出了日志消息:

Terminal window
[INFO] [minimal_service]: Incoming request
a: 41 b: 1

在服务端终端中按 Ctrl+C 停止节点运行。

你创建了两个节点,通过服务来请求和响应数据。你还将它们的依赖和可执行文件添加到了功能包配置文件中,从而可以构建和运行,并观察服务/客户端系统的完整工作流程。

在最近的几个教程中,你一直在使用接口在话题和服务之间传递数据。接下来,你将学习如何创建自定义接口。

  • 用 Python 编写服务端和客户端有多种方式,请查看 ros2/examples 仓库中的 minimal_client 和 minimal_service 功能包。

  • 在本教程中,你在客户端节点中使用了 call_async() API 来调用服务。Python 还有另一种服务调用 API,即同步调用。我们不推荐使用同步调用,但如果你想了解更多,请阅读同步与异步客户端的指南。