Skip to content

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

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

教程级别: 初级

预计时间: 20 分钟

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

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

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

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

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

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

Terminal window
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 文件的功能包,你需要它来构造请求和响应:

Terminal window
int64 a
int64 b
---
int64 sum

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

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

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

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

在 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;
}

前几个 #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);

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 文件并运行服务节点,不过我们先创建客户端节点,以便观察整个系统的运行效果。

在 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;
}

与服务节点类似,以下代码创建节点,然后为该节点创建客户端:

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 直到收到响应或调用失败。

回到 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()

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

Linux:

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

macOS:

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

Windows:

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

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

Linux/macOS:

Terminal window
colcon build --packages-select cpp_srvcli

Windows:

Terminal window
colcon build --merge-install --packages-select cpp_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 cpp_srvcli server

服务节点启动后会等待传入的请求。

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

Terminal window
$ ros2 run cpp_srvcli client
[INFO] [minimal_client]: result of 41 + 1: 42

客户端应该收到上面的响应。

切换到运行服务节点的终端,你会看到它在收到请求时输出了日志消息,包含接收到的数据以及返回的响应:

Terminal window
[INFO] [minimal_service]: request: 41 + 1

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

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

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

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