ros2 从零开始10 服务者和消费者C/S

前言

上节课介绍写了简单的Topic订阅模型。本章我们将要学习C/S模型,即服务者和消费者模型

背景

  1. 前面服务概念时提到过,服务是ROS2 节点的另一种通信方式。服务基于调用与响应模型,而非发布者-订阅者主题模型。服务就是我们常用的C/S模型,Client 客户端请求,而Service 服务端提供相应的响应。相较于发布者-订阅者主题模型,调用与响应模型是点对点的通信。
  2. 服务的通信数据结构用.srv文件来描述,如下图,srv文件用--- 来分隔,上面是请求数据结构,下面是响应数据结构,如果数据结构为空,则相应位置也为空。后面会有章节来介绍srv,先按下不表。
int64 a
int64 b
---
int64 sum

实践

  1. 创建一个包
    进入工作区的src目录,用如下指令创建我们的包ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_srvcli --dependencies rclcpp example_interfaces--dependencies 后面代表相关的依赖项rclcpp、example_interfaces ,命令会自动把依赖信息添加到package.xml和CMakeLists.txt里面。
root@bc2bf85b2e4a:~/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: /root/ros2_ws/src
    package format: 3
    version: 0.0.0
    description: TODO: Package description
    maintainer: ['root <*****@******>']
    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
root@bc2bf85b2e4a:~/ros2_ws/src# ls
    cpp_srvcli  example_interfaces  examples  mypackage

以上命令,ros2帮我们自动生成了cpp_srvcli。还记得example_interfaces吗,example_interfaces 是 ROS 2 中的一个消息接口包,它包含了一系列示例消息类型,主要用于测试、演示和学习目的。
package.xml 里面可以维护描述、作者信息、License等

<description>C++ client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
  1. 创建
    我们将设计2个节点(执行程序)
  • server 负责提供服务接口
  • client 负责调用client提供的服务接口
    开始吧:
    2.1 在src里面创建一个cpp文件 add_two_ints_server.cpp 其内容如下:
    #include "rclcpp/rclcpp.hpp"
    #include "example_interfaces/srv/add_two_ints.hpp"
    
    #include <memory>
    
    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);
    
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");
    
    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();
    }
    
    解析 : 下面代码创建节点add_two_ints_server, 并接下来创建 rclcpp::Service对象, 而 rclcpp::Service对象创建时,把实现函数add当成参数初始化了。在创建完成对象会自动通过在网络上进行广播些服务。rclcpp::spin(node)会阻塞,直到节点退出后才返回。
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");
    
    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);
    
    关于add函数,入参分别是 std::shared_ptr<example_interfaces::srv::AddTwoInts::Request>std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> 这2个定义的实现由前面提到的srv文件编译自动生成得到的。
    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);
    }
    
    2.2 在src里面一个cpp文件 add_two_ints_client.cpp 其内容如下:
    #include "rclcpp/rclcpp.hpp"
    #include "example_interfaces/srv/add_two_ints.hpp"
    
    #include <chrono>
    #include <cstdlib>
    #include <memory>
    
    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;
    }
    
    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");
    
    auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
    request->a = atoll(argv[1]);
    request->b = atoll(argv[2]);
    
    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);
    // Wait for the result.
    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();
    return 0;
    }
    
    解析 : 像server一样, 下面代码创建节点node, 并且创建rclcpp::Client对象(对应rclcpp::Service对象)。
    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");
    
    
    接下来创建请求入参request。并且给具体的值,具体值a和b 是我们node节点启动时的参数1、参数2。
    auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
    request->a = atoll(argv[1]);
    request->b = atoll(argv[2]);
    
    接下来查找服务 client->wait_for_service(1s) 如果找到就返回true,否则1s超时返回。auto result = client->async_send_request(request); 就是调用服务。
    2.3 修改CMakeLists.txt 在ament_package()的前一行新增如下信息
    add_executable(server src/add_two_ints_server.cpp)
    ament_target_dependencies(server rclcpp example_interfaces)
    
    add_executable(client src/add_two_ints_client.cpp)
    ament_target_dependencies(client rclcpp example_interfaces)
    
    install(TARGETS
    server
    client
    DESTINATION lib/${PROJECT_NAME})
    
    完整的CMakeLists.txt 如下:
    cmake_minimum_required(VERSION 3.8)
    project(cpp_srvcli)
    
    if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    add_compile_options(-Wall -Wextra -Wpedantic)
    endif()
    
    # find dependencies
    find_package(ament_cmake REQUIRED)
    find_package(rclcpp REQUIRED)
    find_package(example_interfaces REQUIRED)
    
    if(BUILD_TESTING)
    find_package(ament_lint_auto REQUIRED)
    # the following line skips the linter which checks for copyrights
    # comment the line when a copyright and license is added to all source files
    set(ament_cmake_copyright_FOUND TRUE)
    # the following line skips cpplint (only works in a git repo)
    # comment the line when this package is in a git repo and when
    # a copyright and license is added to all source files
    set(ament_cmake_cpplint_FOUND TRUE)
    ament_lint_auto_find_test_dependencies()
    endif()
    
    add_executable(server src/add_two_ints_server.cpp)
    ament_target_dependencies(server rclcpp example_interfaces)
    
    add_executable(client src/add_two_ints_client.cpp)
    ament_target_dependencies(client rclcpp example_interfaces)
    
    install(TARGETS
    server
    client
    DESTINATION lib/${PROJECT_NAME})
    
    ament_package()
    
    
  1. 开始编译
    进入工作区,用colcon build ,等待编译完成。
root@bc2bf85b2e4a:/# cd ~/ros2_ws
root@bc2bf85b2e4a:~/ros2_ws# colcon build --packages-select cpp_srvcli
Starting >>> cpp_srvcli
Finished <<< cpp_srvcli [5.22s]                     

Summary: 1 package finished [5.57s]
root@bc2bf85b2e4a:~/ros2_ws# 
  1. 运行
    编译完成后,我们需要安装后才能运行,使用
source install/setup.sh

打开一个终端,输入如下命令,启动server

root@bc2bf85b2e4a:~/ros2_ws# source install/setup.sh 
root@bc2bf85b2e4a:~/ros2_ws# ros2 run cpp_srvcli server
[INFO] [1774875183.179833701] [rclcpp]: Ready to add two ints.

另开一个终端,输入如下命令,启动client

root@bc2bf85b2e4a:~/ros2_ws# cd ~/ros2_ws/
root@bc2bf85b2e4a:~/ros2_ws# source install/setup.sh 
root@bc2bf85b2e4a:~/ros2_ws# ros2 run cpp_srvcli client 300 300
[INFO] [1774875293.803476695] [rclcpp]: Sum: 600
root@bc2bf85b2e4a:~/ros2_ws#

可以看到server计算2个数相加,并把结果返回给client。

总结

在最近几个教程中,你一直在使用主题和服务传递数据。 接下来,我们学习如何自定义消息数据接口(srv)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐