编写简单的服务端和客户端(C++)
目标: 使用 C++ 创建并运行服务端(service)和客户端(client)节点。
教程级别: 初级
预计时间: 20 分钟
节点通过服务通信时,发送请求的节点称为客户端节点,响应请求的节点称为服务端节点。请求和响应的结构由 .srv 文件定义。
这里使用的示例是一个简单的整数加法系统:一个节点请求两个整数的和,另一个节点返回结果。
在之前的教程中,你已经学习了如何创建工作空间和功能包。
1 创建功能包
Section titled “1 创建功能包”打开一个新终端并 source 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 aint64 b---int64 sum前两行是请求的参数,虚线下方是响应。
1.1 更新 package.xml
Section titled “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 编写服务节点
Section titled “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 代码解析
Section titled “2.1 代码解析”前几个 #include 语句是你的功能包依赖。
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);- spin 该节点,使服务可用。
rclcpp::spin(g_node);2.2 添加可执行文件
Section titled “2.2 添加可执行文件”add_executable 宏生成一个可以通过 ros2 run 运行的可执行文件。在 CMakeLists.txt 中,紧接依赖下方添加以下代码块以创建名为 server 的可执行文件:
add_executable(server src/add_two_ints_server.cpp)target_link_libraries(server PUBLIC rclcpp::rclcpp example_interfaces::example_interfaces)为了让 ros2 run 能找到可执行文件,在文件末尾、ament_package() 之前添加以下内容:
install(TARGETS server DESTINATION lib/${PROJECT_NAME})你现在可以构建功能包、source 本地 setup 文件并运行服务节点,不过我们先创建客户端节点,以便观察整个系统的运行效果。
3 编写客户端节点
Section titled “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;
int main(int argc, char * argv[]){ rclcpp::init(argc, argv); auto node = rclcpp::Node::make_shared("minimal_client"); auto client = node->create_client<AddTwoInts>("add_two_ints"); while (!client->wait_for_service(std::chrono::seconds(1))) { if (!rclcpp::ok()) { RCLCPP_ERROR(node->get_logger(), "client interrupted while waiting for service to appear."); return 1; } RCLCPP_INFO(node->get_logger(), "waiting for service to appear..."); } auto request = std::make_shared<AddTwoInts::Request>(); request->a = 41; request->b = 1; auto result_future = client->async_send_request(request); if (rclcpp::spin_until_future_complete(node, result_future) != rclcpp::FutureReturnCode::SUCCESS) { RCLCPP_ERROR(node->get_logger(), "service call failed :("); client->remove_pending_request(result_future); return 1; } auto result = result_future.get(); RCLCPP_INFO( node->get_logger(), "result of %" PRId64 " + %" PRId64 " = %" PRId64, request->a, request->b, result->sum); rclcpp::shutdown(); return 0;}3.1 代码解析
Section titled “3.1 代码解析”与服务节点类似,以下代码创建节点,然后为该节点创建客户端:
auto node = rclcpp::Node::make_shared("minimal_client");auto client = node->create_client<AddTwoInts>("add_two_ints");接下来,代码会等待服务上线。while 循环让客户端每隔 1 秒在网络中搜索服务节点。如果找不到,就继续等待。如果客户端被取消(例如你在终端中按了 Ctrl+C),则会输出一条错误日志,提示已被中断。
while (!client->wait_for_service(std::chrono::seconds(1))) { if (!rclcpp::ok()) { RCLCPP_ERROR(node->get_logger(), "client interrupted while waiting for service to appear."); return 1; } RCLCPP_INFO(node->get_logger(), "waiting for service to appear...");}接下来,创建请求。其结构由前面提到的 .srv 文件定义。
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();request->a = 41;request->b = 1;然后客户端发送请求,节点持续 spin 直到收到响应或调用失败。
3.2 添加可执行文件
Section titled “3.2 添加可执行文件”回到 CMakeLists.txt,为新节点添加可执行文件和安装目标。删掉自动生成的样板代码后,CMakeLists.txt 应如下所示:
cmake_minimum_required(VERSION 3.20)project(cpp_srvcli)
find_package(ament_cmake REQUIRED)find_package(rclcpp REQUIRED)find_package(example_interfaces REQUIRED)
add_executable(server src/add_two_ints_server.cpp)target_link_libraries(server PUBLIC rclcpp::rclcpp example_interfaces::example_interfaces)
add_executable(client src/add_two_ints_client.cpp)target_link_libraries(client PUBLIC rclcpp::rclcpp example_interfaces::example_interfaces)
install(TARGETS server client DESTINATION lib/${PROJECT_NAME})
ament_package()4 构建与运行
Section titled “4 构建与运行”构建前,建议在工作空间(ros2_ws)根目录下运行 rosdep 来检查缺失的依赖:
Linux:
rosdep install -i --from-path src --rosdistro {DISTRO} -ymacOS:
rosdep 仅在 Linux 上运行,可直接跳到下一步。
Windows:
rosdep 仅在 Linux 上运行,可直接跳到下一步。
回到工作空间根目录 ros2_ws,构建新功能包:
Linux/macOS:
colcon build --packages-select cpp_srvcliWindows:
colcon build --merge-install --packages-select cpp_srvcli打开一个新终端,进入 ros2_ws,并 source 环境配置文件:
Linux:
source install/setup.bashmacOS:
. install/setup.bashWindows:
call install/setup.bat现在运行服务节点:
ros2 run cpp_srvcli server服务节点启动后会等待传入的请求。
打开另一个终端,同样在 ros2_ws 中 source 环境配置文件。启动客户端节点。客户端将请求发送给服务端,服务端计算两数之和并返回结果:
$ ros2 run cpp_srvcli client[INFO] [minimal_client]: result of 41 + 1: 42客户端应该收到上面的响应。
切换到运行服务节点的终端,你会看到它在收到请求时输出了日志消息,包含接收到的数据以及返回的响应:
[INFO] [minimal_service]: request: 41 + 1在服务端终端中按 Ctrl+C 停止节点运行。
你创建了两个节点,通过服务来请求和响应数据。你还将它们的依赖和可执行文件添加到了功能包配置文件中,从而可以构建和运行,并观察服务/客户端系统的完整工作流程。
在最近的几个教程中,你一直在使用接口在话题和服务之间传递数据。接下来,你将学习如何创建自定义接口。
- 用 C++ 编写服务端和客户端有多种方式,请查看 ros2/examples 仓库中的
minimal_service和minimal_client功能包。