编写简单的服务端和客户端(Python)
目标: 使用 Python 创建并运行服务端(service)和客户端(client)节点。
教程级别: 初级
预计时间: 20 分钟
节点通过服务通信时,发送请求的节点称为客户端节点,响应请求的节点称为服务端节点。请求和响应的结构由 .srv 文件定义。
这里使用的示例是一个简单的整数加法系统:一个节点请求两个整数的和,另一个节点返回结果。
在之前的教程中,你已经学习了如何创建工作空间和功能包。
1 创建功能包
Section titled “1 创建功能包”打开一个新终端并 source ROS 2 安装环境,以便使用 ros2 命令。
进入之前教程中创建的 ros2_ws 目录。
功能包应在 src 目录中创建,而非工作空间根目录。进入 ros2_ws/src,创建一个新功能包:
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 文件的功能包,你需要它来构造请求和响应:
int64 a int64 b --- int64 sum前两行是请求的参数,虚线下方是响应。
1.1 更新 package.xml
Section titled “1.1 更新 package.xml”由于你在创建功能包时使用了 --dependencies 选项,因此不需要手动向 package.xml 添加依赖。
不过,仍需照例填写描述、维护者姓名和邮箱以及许可证信息。
<description>Python client server tutorial</description><maintainer email="you@email.com">Your Name</maintainer><license>Apache-2.0</license>1.2 更新 setup.py
Section titled “1.2 更新 setup.py”在 setup.py 文件中为 maintainer、maintainer_email、description 和 license 字段添加相同的信息:
maintainer='Your Name', maintainer_email='you@email.com', description='Python client server tutorial', license='Apache-2.0',2 编写服务节点
Section titled “2 编写服务节点”在 ros2_ws/src/py_srvcli/py_srvcli 目录中,创建一个名为 service_member_function.py 的新文件,并粘贴以下代码:
from example_interfaces.srv import AddTwoInts
import rclpyfrom rclpy.executors import ExternalShutdownExceptionfrom 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()2.1 代码解析
Section titled “2.1 代码解析”第一条 import 语句从 example_interfaces 功能包导入 AddTwoInts 服务类型。后续 import 语句导入所需的 ROS 2 Python 客户端库接口。
from example_interfaces.srv import AddTwoInts
import rclpyfrom rclpy.executors import ExternalShutdownExceptionfrom rclpy.node import NodeMinimalService 类的构造函数以名称 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 该节点以处理回调。
2.2 添加入口点
Section titled “2.2 添加入口点”为了让 ros2 run 命令运行你的节点,你必须在 setup.py(位于 ros2_ws/src/py_srvcli 目录中)中添加入口点。
在 'console_scripts': 列表中添加以下行:
'service = py_srvcli.service_member_function:main',3 编写客户端节点
Section titled “3 编写客户端节点”在 ros2_ws/src/py_srvcli/py_srvcli 目录中,创建一个名为 client_member_function.py 的新文件,并粘贴以下代码:
from example_interfaces.srv import AddTwoInts
import rclpyfrom rclpy.executors import ExternalShutdownExceptionfrom 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()3.1 代码解析
Section titled “3.1 代码解析”与服务代码一样,首先 import 必要的库。
from example_interfaces.srv import AddTwoInts
import rclpyfrom rclpy.executors import ExternalShutdownExceptionfrom rclpy.node import NodeMinimalClientAsync 类的构造函数以名称 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。更多详情请参阅同步死锁文章。
3.2 添加入口点
Section titled “3.2 添加入口点”与服务节点一样,也需要添加入口点才能运行客户端节点。
你的 setup.py 文件中的 entry_points 字段应该如下所示:
entry_points={ 'console_scripts': [ 'service = py_srvcli.service_member_function:main', 'client = py_srvcli.client_member_function:main', ],},4 构建与运行
Section titled “4 构建与运行”构建前,建议在工作空间(ros2_ws)根目录下运行 rosdep 来检查缺失的依赖:
Linux:
rosdep install -i --from-path src --rosdistro {DISTRO} -ymacOS:
rosdep 仅在 Linux 上运行,可直接跳到下一步。
Windows:
rosdep 仅在 Linux 上运行,可直接跳到下一步。
回到工作空间根目录 ros2_ws,构建新功能包:
colcon build --packages-select py_srvcli打开一个新终端,进入 ros2_ws,并 source 环境配置文件:
Linux:
source install/setup.bashmacOS:
. install/setup.bashWindows:
call install/setup.bat现在运行服务节点:
ros2 run py_srvcli service服务节点启动后会等待客户端的请求。
打开另一个终端,同样在 ros2_ws 中 source 环境配置文件。启动客户端节点。客户端将请求发送给服务端,服务端计算两数之和并返回结果:
$ ros2 run py_srvcli client[INFO] [minimal_client_async]: Result of add_two_ints: for 41 + 1 = 42切换到运行服务节点的终端,你会看到它在收到请求时输出了日志消息:
[INFO] [minimal_service]: Incoming requesta: 41 b: 1在服务端终端中按 Ctrl+C 停止节点运行。
你创建了两个节点,通过服务来请求和响应数据。你还将它们的依赖和可执行文件添加到了功能包配置文件中,从而可以构建和运行,并观察服务/客户端系统的完整工作流程。
在最近的几个教程中,你一直在使用接口在话题和服务之间传递数据。接下来,你将学习如何创建自定义接口。
-
用 Python 编写服务端和客户端有多种方式,请查看 ros2/examples 仓库中的
minimal_client和minimal_service功能包。 -
在本教程中,你在客户端节点中使用了
call_async()API 来调用服务。Python 还有另一种服务调用 API,即同步调用。我们不推荐使用同步调用,但如果你想了解更多,请阅读同步与异步客户端的指南。