一、编写简单的C++服务端与客户端
目标:使用C++创建并运行服务端(service)和客户端(client)节点。
教程级别:初学者
预计时间:20分钟
目录
- 背景
- 前提条件
- 任务
- 创建功能包
- 编写服务端节点
- 编写客户端节点
- 构建并运行
- 总结
- 下一步
背景
当节点通过服务(service)进行通信时,发送数据请求的节点称为客户端节点,响应请求的节点称为服务端节点。请求和响应的结构由.srv文件定义。
本教程使用的示例是一个简单的整数加法系统:一个节点请求两个整数的和,另一个节点返回计算结果。
前提条件
在之前的教程中,你已学会如何创建工作空间和功能包。
任务
1. 创建功能包
- 打开新终端,配置ROS 2环境(确保
ros2命令可正常使用)。 - 进入之前教程中创建的
ros2_ws目录。 - 功能包需在
src目录下创建(而非工作空间根目录),因此进入ros2_ws/src并执行以下命令创建新功能包:ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_srvcli --dependencies rclcpp example_interfaces
终端会返回确认信息,提示cpp_srvcli功能包及其所需的所有文件和文件夹已创建。
--dependencies参数会自动在package.xml和CMakeLists.txt中添加必要的依赖项。example_interfaces是包含本教程所需.srv文件的功能包,该文件定义了请求和响应的结构:
其中,虚线以上的两行是请求参数,虚线以下是响应结果。int64 a int64 b --- int64 sum
1.1 更新package.xml
由于创建功能包时使用了--dependencies选项,无需手动向package.xml或CMakeLists.txt添加依赖。但仍需补充功能包描述、维护者信息和许可证信息:
<description>C++ client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache-2.0</license>
2. 编写服务端节点
- 进入
ros2_ws/src/cpp_srvcli/src目录,创建名为add_two_ints_server.cpp的新文件。 - 粘贴以下代码到文件中:
#include <cinttypes> #include <memory> #include "example_interfaces/srv/add_two_ints.hpp" #include "rclcpp/rclcpp.hpp" using AddTwoInts = example_interfaces::srv::AddTwoInts; rclcpp::Node::SharedPtr g_node = nullptr; void handle_service( const std::shared_ptr<rmw_request_id_t> request_header, const std::shared_ptr<AddTwoInts::Request> request, const std::shared_ptr<AddTwoInts::Response> response) { (void)request_header; RCLCPP_INFO( g_node->get_logger(), "request: %" PRId64 " + %" PRId64, request->a, request->b); response->sum = request->a + request->b; } int main(int argc, char **argv) { rclcpp::init(argc, argv); g_node = rclcpp::Node::make_shared("minimal_service"); auto server = g_node->create_service<AddTwoInts>("add_two_ints", handle_service); rclcpp::spin(g_node); rclcpp::shutdown(); g_node = nullptr; return 0; }
2.1 代码解析
-
开头的
#include语句引入了功能包依赖的头文件:example_interfaces/srv/add_two_ints.hpp:包含加法服务的请求/响应结构定义。rclcpp/rclcpp.hpp:ROS 2 C++客户端库核心头文件。
-
handle_service函数:
接收请求中的两个整数,计算它们的和并赋值给响应,同时通过日志在控制台输出请求状态。void handle_service( const std::shared_ptr<rmw_request_id_t> request_header, const std::shared_ptr<AddTwoInts::Request> request, const std::shared_ptr<AddTwoInts::Response> response) { (void)request_header; // 未使用的参数,避免编译警告 RCLCPP_INFO( g_node->get_logger(), "request: %" PRId64 " + %" PRId64, request->a, request->b); // 打印请求的两个整数 response->sum = request->a + request->b; // 计算和并赋值给响应 } -
main函数逐行功能:- 初始化ROS 2 C++客户端库:
rclcpp::init(argc, argv); - 创建名为
minimal_service的节点:g_node = rclcpp::Node::make_shared("minimal_service"); - 为节点创建名为
add_two_ints的服务,并绑定handle_service函数处理请求:auto server = g_node->create_service<AddTwoInts>("add_two_ints", handle_service); - 循环运行节点,使服务保持可用状态:
rclcpp::spin(g_node);
- 初始化ROS 2 C++客户端库:
2.2 添加可执行文件配置
在CMakeLists.txt中,在依赖项配置下方添加以下代码块,生成名为server的可执行文件:
add_executable(server src/add_two_ints_server.cpp)
target_link_libraries(server PUBLIC
rclcpp::rclcpp
${example_interfaces_TARGETS}
)
为了让ros2 run能找到该可执行文件,在CMakeLists.txt末尾、ament_package()之前添加以下安装配置:
install(TARGETS
server
DESTINATION lib/${PROJECT_NAME}
)
此时可构建功能包并运行服务端,但建议先编写客户端节点,以便观察完整的服务通信流程。
3. 编写客户端节点
- 仍在
ros2_ws/src/cpp_srvcli/src目录下,创建名为add_two_ints_client.cpp的新文件。 - 粘贴以下代码到文件中:
#include <chrono> #include <cinttypes> #include <memory> #include "example_interfaces/srv/add_two_ints.hpp" #include "rclcpp/rclcpp.hpp" using AddTwoInts = example_interfaces::srv::AddTwoInts; using namespace std::chrono_literals; int main(int argc, char *argv[]) { rclcpp::init(argc, argv); // 检查命令行参数是否足够(需传入两个整数) if (argc != 3) { RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y"); return 1; } // 创建名为"minimal_client"的节点 auto node = rclcpp::Node::make_shared("minimal_client"); // 创建客户端,指定服务类型为AddTwoInts,服务名为"add_two_ints" auto client = node->create_client<AddTwoInts>("add_two_ints"); // 等待服务端启动(最多等待1秒/次,直到服务可用) while (!client->wait_for_service(1s)) { if (!rclcpp::ok()) { // 若节点被中断(如按Ctrl+C),退出程序 RCLCPP_ERROR(node->get_logger(), "Interrupted while waiting for the service. Exiting."); return 0; } RCLCPP_INFO(node->get_logger(), "service not available, waiting again..."); } // 创建服务请求对象,赋值命令行传入的两个整数 auto request = std::make_shared<AddTwoInts::Request>(); request->a = atoll(argv[1]); request->b = atoll(argv[2]); // 发送异步请求,并定义响应回调函数 auto result_future = client->async_send_request(request); // 等待响应结果(超时时间为5秒) if (rclcpp::spin_until_future_complete(node, result_future, 5s) == rclcpp::FutureReturnCode::SUCCESS) { // 响应成功,打印结果 auto result = result_future.get(); RCLCPP_INFO( node->get_logger(), "Result of add_two_ints: %" PRId64 " + %" PRId64 " = %" PRId64, request->a, request->b, result->sum); } else { // 响应超时或失败 RCLCPP_ERROR(node->get_logger(), "Failed to call service add_two_ints"); } // 关闭节点并清理资源 rclcpp::shutdown(); return 0; }
3.1 代码解析
-
命令行参数检查:确保运行客户端时传入两个整数(作为加法的操作数),若参数不足则提示用法并退出:
if (argc != 3) { RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y"); return 1; } -
服务等待逻辑:客户端会循环等待服务端启动,每次等待1秒,直到服务可用或节点被中断:
while (!client->wait_for_service(1s)) { if (!rclcpp::ok()) { RCLCPP_ERROR(node->get_logger(), "Interrupted while waiting for the service. Exiting."); return 0; } RCLCPP_INFO(node->get_logger(), "service not available, waiting again..."); } -
请求发送与响应处理:
- 创建请求对象,将命令行传入的字符串参数转换为整数并赋值:
auto request = std::make_shared<AddTwoInts::Request>(); request->a = atoll(argv[1]); request->b = atoll(argv[2]); - 发送异步请求,获取未来(future)对象以等待响应:
auto result_future = client->async_send_request(request); - 等待响应结果(超时时间5秒),成功则打印结果,失败则提示错误:
if (rclcpp::spin_until_future_complete(node, result_future, 5s) == rclcpp::FutureReturnCode::SUCCESS) { auto result = result_future.get(); RCLCPP_INFO( node->get_logger(), "Result of add_two_ints: %" PRId64 " + %" PRId64 " = %" PRId64, request->a, request->b, result->sum); } else { RCLCPP_ERROR(node->get_logger(), "Failed to call service add_two_ints"); }
- 创建请求对象,将命令行传入的字符串参数转换为整数并赋值:
3.2 添加客户端可执行文件配置
在CMakeLists.txt中,在服务端可执行文件配置下方添加以下代码,生成名为client的可执行文件:
add_executable(client src/add_two_ints_client.cpp)
target_link_libraries(client PUBLIC
rclcpp::rclcpp
${example_interfaces_TARGETS}
)
更新安装配置,将client可执行文件一并安装(在install(TARGETS块中添加client):
install(TARGETS
server
client
DESTINATION lib/${PROJECT_NAME}
)
保存CMakeLists.txt,此时服务端和客户端的代码与配置已完成。
4. 构建并运行
-
检查依赖(仅Linux系统):
在工作空间根目录ros2_ws下运行rosdep,确保无缺失依赖:rosdep install -i --from-path src --rosdistro kilted -ymacOS和Windows系统可跳过此步骤(
rosdep仅支持Linux)。 -
构建功能包:
在ros2_ws目录下执行构建命令,仅构建cpp_srvcli功能包以节省时间:# Linux/macOS colcon build --packages-select cpp_srvcli # Windows(需使用VS 2019的x64 Native Tools Command Prompt) colcon build --merge-install --packages-select cpp_srvcli(Windows需
--merge-install参数,避免长路径问题) -
运行服务端:
- 打开新终端,进入
ros2_ws目录,配置环境:# Linux source install/setup.bash # macOS . install/setup.bash # Windows call install/setup.bat - 启动服务端节点:
ros2 run cpp_srvcli server
终端会保持运行状态,等待客户端请求。
- 打开新终端,进入
-
运行客户端:
- 再打开一个新终端,同样进入
ros2_ws并配置环境。 - 运行客户端节点,传入两个整数(如
3和5):ros2 run cpp_srvcli client 3 5 - 客户端终端会输出结果:
[INFO] [minimal_client]: Result of add_two_ints: 3 + 5 = 8 - 服务端终端会同步输出请求信息:
[INFO] [minimal_service]: request: 3 + 5
- 再打开一个新终端,同样进入
-
停止节点:
在每个终端中按Ctrl+C停止节点运行。
总结
你已创建了通过服务通信的两个节点:
- 服务端节点接收客户端的整数加法请求,计算并返回结果。
- 客户端节点发送带参数的请求,等待并打印服务端的响应。
在运行前,你通过package.xml和CMakeLists.txt配置了依赖项与可执行文件,确保ROS 2能正确构建和找到节点。
下一步
接下来,你可以学习如何创建更复杂的ROS 2功能,例如结合发布者/订阅者与服务的节点,或自定义.srv/.msg文件以满足特定通信需求。
二、编写简单的Python服务端与客户端
目标:使用Python创建并运行服务端(service)和客户端(client)节点。
教程级别:初学者
预计时间:20分钟
目录
- 背景
- 前提条件
- 任务
- 创建功能包
- 编写服务端节点
- 编写客户端节点
- 构建并运行
- 总结
- 下一步
背景
当节点通过服务(service)通信时,发送数据请求的节点称为客户端节点,响应请求的节点称为服务端节点。请求与响应的结构由.srv文件定义。
本教程使用的示例是一个简单的整数加法系统:一个节点请求两个整数的和,另一个节点返回计算结果。
前提条件
在之前的教程中,你已学会如何创建工作空间和功能包。
任务
1. 创建功能包
- 打开新终端,配置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
由于创建功能包时使用了--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
在setup.py文件中,为maintainer、maintainer_email、description和license字段补充与package.xml一致的信息:
maintainer='Your Name',
maintainer_email='you@email.com',
description='Python client server tutorial',
license='Apache-2.0',
2. 编写服务端节点
- 进入
ros2_ws/src/py_srvcli/py_srvcli目录(与功能包同名的Python包目录)。 - 创建名为
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()
2.1 代码解析
-
导入依赖:
from example_interfaces.srv import AddTwoInts:导入example_interfaces功能包中的AddTwoInts服务类型,包含请求和响应的结构定义。import rclpy及后续语句:导入ROS 2 Python客户端库的核心接口,用于创建节点和处理回调。
-
MinimalService类构造函数:
初始化节点并命名为minimal_service,同时创建服务:- 服务类型:
AddTwoInts(与.srv文件对应)。 - 服务名称:
add_two_ints(需与客户端请求的服务名称一致)。 - 回调函数:
self.add_two_ints_callback(用于处理客户端请求)。
def __init__(self): super().__init__('minimal_service') self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback) - 服务类型:
-
服务回调函数:
接收客户端请求的两个整数request.a和request.b,计算它们的和并赋值给response.sum,同时通过日志打印请求信息,最后返回响应。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类创建服务端节点,通过rclpy.spin循环运行节点以处理回调(直到节点被中断)。
2.2 添加入口点
为了让ros2 run命令能找到并运行服务端节点,需在ros2_ws/src/py_srvcli/setup.py文件的'console_scripts':括号内添加以下入口点:
'service = py_srvcli.service_member_function:main',
3. 编写客户端节点
- 仍在
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()
3.1 代码解析
-
导入依赖:与服务端代码一致,导入
AddTwoInts服务类型和ROS 2 Python核心库。 -
MinimalClientAsync类构造函数:- 初始化节点并命名为
minimal_client_async。 - 创建客户端:服务类型为
AddTwoInts,服务名称为add_two_ints(需与服务端完全一致,否则无法通信)。 - 等待服务端启动:通过
while循环每秒检查一次服务是否可用,若不可用则打印日志提示。 - 创建请求对象:初始化
AddTwoInts.Request()用于存储请求参数。
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方法:
为请求对象赋值(a=41,b=1),通过call_async发送异步请求,并返回用于获取响应结果的future对象。def send_request(self): self.req.a = 41 self.req.b = 1 return self.cli.call_async(self.req) -
main函数:- 初始化ROS 2环境,实例化客户端节点。
- 发送请求并获取
future对象。 - 通过
rclpy.spin_until_future_complete等待响应结果(直到请求完成或超时)。 - 打印响应结果(两个整数的和)。
警告:不要在ROS 2回调函数中使用
rclpy.spin_until_future_complete,详情可参考同步死锁相关文档。
3.2 添加入口点
在setup.py文件的entry_points字段中,在服务端入口点下方添加客户端入口点。最终entry_points字段应如下所示:
entry_points={
'console_scripts': [
'service = py_srvcli.service_member_function:main',
'client = py_srvcli.client_member_function:main',
],
},
4. 构建并运行
-
检查依赖(仅Linux系统):
在工作空间根目录ros2_ws下运行rosdep,确保无缺失依赖:rosdep install -i --from-path src --rosdistro kilted -ymacOS和Windows系统可跳过此步骤(
rosdep仅支持Linux)。 -
构建功能包:
在ros2_ws目录下执行构建命令,仅构建py_srvcli功能包以节省时间:colcon build --packages-select py_srvcli -
运行服务端:
- 打开新终端,进入
ros2_ws目录,配置环境:# Linux source install/setup.bash # macOS . install/setup.bash # Windows call install/setup.bat - 启动服务端节点:
ros2 run py_srvcli service
终端会保持运行状态,等待客户端请求。
- 打开新终端,进入
-
运行客户端:
- 再打开一个新终端,同样进入
ros2_ws并配置环境。 - 运行客户端节点:
ros2 run py_srvcli client - 客户端终端会输出结果:
[INFO] [minimal_client_async]: Result of add_two_ints: for 41 + 1 = 42 - 服务端终端会同步输出请求信息:
[INFO] [minimal_service]: Incoming request a: 41 b: 1
- 再打开一个新终端,同样进入
-
停止节点:
在服务端终端中按Ctrl+C停止节点运行。
总结
你已创建了通过服务通信的两个Python节点:
- 服务端节点接收客户端的整数加法请求,计算并返回结果,同时打印请求日志。
- 客户端节点发送预设的整数请求(41和1),等待并打印服务端返回的求和结果。
通过配置package.xml和setup.py,你完成了依赖项和入口点的设置,确保ROS 2能正确构建和运行节点,最终实现了服务端与客户端的完整通信流程。
下一步
在之前的教程中,你已使用接口(.msg/.srv)实现了话题和服务的数据传递。接下来,你可以学习如何创建自定义接口,以满足更灵活的通信需求。
1079

被折叠的 条评论
为什么被折叠?



