目标:使用 C++ 创建并运行服务和客户端节点。
教程级别:初学者
时间:20 分钟
目录
背景
先决条件
任务
1. 创建一个包
2. 编写服务节点
3. 编写客户端节点
4. 构建并运行
摘要
下一步
相关内容
背景
当节点通过服务进行通信时,发送数据请求的节点称为客户端节点,而响应请求的节点称为服务节点。请求和响应的结构由一个 .srv
文件决定。
这里使用的例子是一个简单的整数加法系统;一个节点请求两个整数的和,另一个节点则响应结果。
先决条件
在之前的教程中,您学习了如何创建工作区和创建包。
任务
1. 创建一个包
打开一个新的终端并且初始化您的 ROS 2 安装,这样 ros2
命令就会生效。
导航到在之前教程中创建的 ros2_ws
目录。
请记住,包应该在 src
目录中创建,而不是在工作区的根目录中。导航到 ros2_ws/src
并创建一个新包:
cxy@ubuntu2404-cxy:~/ros2_ws/src$ ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_srvcli --dependencies rclcpp example_interfaces
going to create a new package
package name: cpp_srvcli
destination directory: /home/cxy/ros2_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['cxy <cxy@todo.todo>']
licenses: ['Apache-2.0']
build type: ament_cmake
dependencies: ['rclcpp', 'example_interfaces']
creating folder ./cpp_srvcli
creating ./cpp_srvcli/package.xml
creating source and include folder
creating folder ./cpp_srvcli/src
creating folder ./cpp_srvcli/include/cpp_srvcli
creating ./cpp_srvcli/CMakeLists.txt
您的终端将返回一条消息,确认您的包 cpp_srvcli
及其所有必要的文件和文件夹已创建。
`--dependencies` 参数将自动将必要的依赖项行添加到 package.xml 和 CMakeLists.txt 中。`example_interfaces` 是包含您将需要用来构建请求和响应的 `.srv` 文件 https://github.com/ros2/example_interfaces/blob/jazzy/srv/AddTwoInts.srv 的包。
int64 a
int64 b
---
int64 sum
请求的前两行是参数,破折号以下的是响应。
1.1 更新 package.xml
因为在创建包时您使用了 --dependencies
选项,所以您无需手动添加依赖项到 package.xml
或 CMakeLists.txt
。
一如既往,不过,请确保将描述、维护者电子邮件和姓名以及许可信息添加到 package.xml
。
<description>C++ client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache-2.0</license>
<?xml version="1.0"?> <!-- XML文件的版本为1.0 -->
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> <!-- XML模型的位置和模式类型 -->
<package format="3"> <!-- 定义一个格式为3的包 --><name>cpp_srvcli</name> <!-- 包的名称为cpp_srvcli --><version>0.0.0</version> <!-- 包的版本为0.0.0 --><description>C++ client server tutorial</description> <!-- 包的描述为C++ client server tutorial --><maintainer email="cxy@126.com">cxy</maintainer> <!-- 包的维护者的名字和邮箱 --><license>Apache-2.0</license> <!-- 包的许可证为Apache-2.0 --><buildtool_depend>ament_cmake</buildtool_depend> <!-- 构建工具依赖项为ament_cmake --><depend>rclcpp</depend> <!-- 依赖项为rclcpp --><depend>example_interfaces</depend> <!-- 依赖项为example_interfaces --><test_depend>ament_lint_auto</test_depend> <!-- 测试依赖项为ament_lint_auto --><test_depend>ament_lint_common</test_depend> <!-- 测试依赖项为ament_lint_common --><export> <!-- 导出部分 --><build_type>ament_cmake</build_type> <!-- 构建类型为ament_cmake --></export>
</package> <!-- 结束包的定义 -->
2. 编写服务节点
在 ros2_ws/src/cpp_srvcli/src
目录中,创建一个名为 add_two_ints_server.cpp
的新文件,并将以下代码粘贴其中:
cxy@ubuntu2404-cxy:~$ cd ~/ros2_ws/src/cpp_srvcli/src
cxy@ubuntu2404-cxy:~/ros2_ws/src/cpp_srvcli/src$ gedit add_two_ints_server.cpp
#include "rclcpp/rclcpp.hpp" // 引入ROS2的rclcpp库
#include "example_interfaces/srv/add_two_ints.hpp" // 引入AddTwoInts服务的接口#include <memory> // 引入内存管理库// 定义一个add函数,该函数接收一个AddTwoInts服务的请求,并返回一个响应
void add(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
{response->sum = request->a + request->b; // 计算两个整数的和,并将结果存储在响应中RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",request->a, request->b); // 打印接收到的请求信息RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum); // 打印发送回的响应信息
}int main(int argc, char **argv)
{rclcpp::init(argc, argv); // 初始化ROS2std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server"); // 创建一个名为"add_two_ints_server"的节点// 在该节点上创建一个AddTwoInts服务,并将add函数作为处理函数rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints."); // 打印服务已准备好的信息rclcpp::spin(node); // 进入循环,等待服务请求rclcpp::shutdown(); // 关闭ROS2
}
检查代码 2.1
前两个 #include
语句是您的包依赖。
` add
`函数将请求中的两个整数相加,并将和数返回给响应,同时使用日志通知控制台其状态。
void add(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
{response->sum = request->a + request->b;RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",request->a, request->b);RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}
该 main
函数逐行完成以下操作:
初始化 ROS 2 C++ 客户端库:
rclcpp::init(argc, argv);
创建名为 add_two_ints_server 的节点:
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");
为该节点创建一个名为 add_two_ints 的服务,并通过 &add 方法自动在网络上进行广告:
rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =
node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);
当准备好时,打印一条日志消息:
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints.");
旋转节点,使服务可用。
rclcpp::spin(node);
2.2 添加可执行文件
` add_executable
` 宏可以生成一个可执行文件,您可以使用 ` ros2 run
` 来运行它。将以下代码块添加到 ` CMakeLists.txt
` 中,以创建一个名为 ` server
` 的可执行文件:
add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server rclcpp example_interfaces)
为了让 ros2 run
能找到可执行文件,请在文件末尾, ament_package()
之前添加以下行:
install(TARGETSserverDESTINATION lib/${PROJECT_NAME})
您现在可以构建您的包,源本地设置文件,并运行它,但让我们先创建客户端节点,这样您就可以看到完整系统的运作。
3. 编写客户端节点
在 ros2_ws/src/cpp_srvcli/src
目录中,创建一个名为 add_two_ints_client.cpp
的新文件,并将以下代码粘贴其中:
#include "rclcpp/rclcpp.hpp" // 引入ROS2的rclcpp库
#include "example_interfaces/srv/add_two_ints.hpp" // 引入AddTwoInts服务的接口#include <chrono> // 引入时间库
#include <cstdlib> // 引入C标准库
#include <memory> // 引入内存管理库using namespace std::chrono_literals; // 使用std::chrono_literals命名空间,方便表示时间int main(int argc, char **argv)
{rclcpp::init(argc, argv); // 初始化ROS2// 如果参数数量不等于3,打印使用方法并返回1if (argc != 3) {RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y");return 1;}// 创建一个名为"add_two_ints_client"的节点std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");// 在该节点上创建一个AddTwoInts客户端rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");// 创建一个AddTwoInts服务的请求auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();request->a = atoll(argv[1]); // 将第一个参数转换为长整型并赋值给请求的arequest->b = atoll(argv[2]); // 将第二个参数转换为长整型并赋值给请求的b// 如果服务不可用,等待1秒后再次检查,直到服务可用while (!client->wait_for_service(1s)) {if (!rclcpp::ok()) {RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");return 0;}RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");}// 发送请求,并获取异步结果auto result = client->async_send_request(request);// 等待结果if (rclcpp::spin_until_future_complete(node, result) ==rclcpp::FutureReturnCode::SUCCESS){// 如果成功,打印结果RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);} else {// 如果失败,打印错误信息RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_two_ints");}rclcpp::shutdown(); // 关闭ROS2return 0; // 返回0,表示程序正常结束
}
3.1 检查代码
与服务节点类似,以下代码行创建节点然后为该节点创建客户端:
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");
接下来,请求被创建。其结构由前面提到的 .srv
文件定义。
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);
如果 while
循环在网络中找不到任何服务节点,客户端将有 1 秒钟的时间进行搜索。如果找不到,它将继续等待。
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
如果客户端被取消(例如,您在终端输入 Ctrl+C
),它将返回一个错误日志消息,说明它被中断了。
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
然后客户端发送其请求,节点旋转直到它收到响应,或者失败。
3.2 添加可执行文件
返回 CMakeLists.txt
以添加新节点的可执行文件和目标。从自动生成的文件中删除一些不必要的样板代码后,您的 CMakeLists.txt
应该如下所示:
cmake_minimum_required(VERSION 3.8) // 设置CMake的最低版本要求为3.8
project(cpp_srvcli) // 定义一个名为cpp_srvcli的项目// 如果编译器是GNU C++或Clang,添加编译选项
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")add_compile_options(-Wall -Wextra -Wpedantic)
endif()// 寻找依赖项
find_package(ament_cmake REQUIRED) // 寻找ament_cmake包,如果没有找到,编译将会失败
find_package(rclcpp REQUIRED) // 寻找rclcpp包,如果没有找到,编译将会失败
find_package(example_interfaces REQUIRED) // 寻找example_interfaces包,如果没有找到,编译将会失败// 添加可执行文件
add_executable(server src/add_two_ints_server.cpp) // 添加名为server的可执行文件,源文件为src/add_two_ints_server.cpp
ament_target_dependencies(server rclcpp example_interfaces) // 设置server的依赖项为rclcpp和example_interfacesadd_executable(client src/add_two_ints_client.cpp) // 添加名为client的可执行文件,源文件为src/add_two_ints_client.cpp
ament_target_dependencies(client rclcpp example_interfaces) // 设置client的依赖项为rclcpp和example_interfaces// 安装目标文件
install(TARGETSserverclientDESTINATION lib/${PROJECT_NAME}) // 将server和client安装到lib/${PROJECT_NAME}目录下// 如果开启了测试
if(BUILD_TESTING)find_package(ament_lint_auto REQUIRED) // 寻找ament_lint_auto包,如果没有找到,编译将会失败// 下面的行跳过了检查版权的linter// 当所有源文件都添加了版权和许可时,注释掉这行set(ament_cmake_copyright_FOUND TRUE)// 下面的行跳过了cpplint(只在git仓库中工作)// 当这个包在git仓库中,并且所有源文件都添加了版权和许可时,注释掉这行set(ament_cmake_cpplint_FOUND TRUE)ament_lint_auto_find_test_dependencies() // 自动寻找测试依赖项
endif()ament_package() // 打包
4. 构建并运行
在工作区的根目录运行 rosdep
( ros2_ws
)以检查构建前缺失的依赖项是一个好习惯:
rosdep install -i --from-path src --rosdistro jazzy -y
返回到您的工作区根目录, ros2_ws
,然后构建您的新包:
cxy@ubuntu2404-cxy:~/ros2_ws$ colcon build --packages-select cpp_srvcli
Starting >>> cpp_srvcli
Finished <<< cpp_srvcli [24.6s] Summary: 1 package finished [24.9s]
打开一个新的终端,导航到 ros2_ws
,并且导入设置文件:
source install/setup.bash
现在运行服务节点:
ros2 run cpp_srvcli server
终端应返回以下消息,然后等待:
cxy@ubuntu2404-cxy:~/ros2_ws$ source install/setup.bash
cxy@ubuntu2404-cxy:~/ros2_ws$ ros2 run cpp_srvcli server
[INFO] [1720240506.845990392] [rclcpp]: Ready to add two ints.
[INFO] [1720240512.380605165] [rclcpp]: Incoming request
a: 2 b: 3
[INFO] [1720240512.380631753] [rclcpp]: sending back response: [5]
打开另一个终端,再次从 ros2_ws
内部加载设置文件。启动客户端节点,后面跟着任意两个用空格分隔的整数:
ros2 run cpp_srvcli client 2 3
如果您选择了 2
和 3
,例如,客户将收到这样的回应:返回运行服务节点的终端。您将看到它在收到请求和数据时发布了日志消息,以及它发送回去的响应:
cxy@ubuntu2404-cxy:~/ros2_ws$ source install/setup.bash
cxy@ubuntu2404-cxy:~/ros2_ws$ ros2 run cpp_srvcli client 2 3
[INFO] [1720240512.382345443] [rclcpp]: Sum: 5
在服务器终端输入 Ctrl+C
以停止节点旋转。
摘要
您创建了两个节点,通过服务请求和响应数据。您将它们的依赖项和可执行文件添加到包配置文件中,以便您可以构建并运行它们,并看到服务/客户端系统的工作。
下一步
在过去的几个教程中,您一直在使用接口来跨主题和服务传递数据。接下来,您将学习如何创建自定义接口 https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Custom-ROS2-Interfaces.html 。
相关内容
有几种方法可以用 C++编写服务和客户端;请查看 ros2/examples 仓库中的
minimal_service
和minimal_client
包 https://github.com/ros2/examples/tree/jazzy/rclcpp/services。