一、服务

服务可以实现类似你问我答的同步通信效果,此时可以在需要某个数据的时候,发一个查询的请求,然后尽快得到目标的最新位置,这样的通信模型和话题单向传输有所不同,变成了发送一个请求反馈一个应答的形式,这种通信机制在ROS中成为服务(Service)。

二、服务模型

1、客户端/服务器模型

发送请求和反馈应答:

从服务的实现机制上来看,这种你问我答的形式叫做客户端/服务器模型,简称为CS模型,客户端在需要某些数据的时候,针对某个具体的服务,发送请求信息,服务器端收到请求之后,就会进行处理并反馈应答信息。

浏览器的应用场景:

这种通信机制在生活中也很常见,比如我们经常浏览的各种网页,此时你的电脑浏览器就是客户端,通过域名或者各种操作,向网站服务器发送请求,服务器收到之后返回需要展现的页面数据。

图1 服务的发送和应答

三、同步通信

这个过程一般要求越快越好,假设服务器半天没有反应,你的浏览器一直转圈圈,那有可能是服务器宕机了,或者是网络不好,所以相比话题通信,在服务通信中,客户端可以通过接收到的应答信息,判断服务器端的状态,我们也称之为同步通信。

四、一对多通信

在服务通信中服务器端是唯一,而客户端可以不唯一,比如古月居这个网站,服务器是唯一存在的,并没有多个完全一样的古月居网站,但是可以访问古月居网站的客户端是不唯一的,大家每一个人都可以看到同样的界面。所以服务通信模型中,服务器端唯一,但客户端可以不唯一。

图2 一个服务器和多个客户端

五、服务接口

和话题通信类似,服务通信的核心还是要传递数据,数据变成了两个部分,一个请求的数据,比如请求苹果位置的命令,还有一个反馈的数据,比如反馈苹果坐标位置的数据,这些数据和话题消息一样,在ROS中也是要标准定义的,话题使用.msg文件定义,服务使用的是.srv文件定义。

六、案例

图3 加法器

通过服务实现一个加法求解器的功能,当我们需要计算两个加数的求和结果时,就通过客户端节点,将两个加数封装成请求数据,针对服务“add_two_ints”发送出去,提供这个服务的服务器端节点,收到请求数据后,开始进行加法计算,并将求和结果封装成应答数据,反馈给客户端,之后客户端就会得到想要的结果。

图4 功能包内部结构

1、创建服务接口的功能包

进入工作空间study_ws中src路径下:

cd ~/study_ws/src

创建功能包demo_interface:

ros2 pkg create --build-type ament_cmake demo_interface

进入功能包路径下,创建srv目录:

①cd ./demo_interface  ②mkdir srv

创建srv文件AddTwoInts.srv:

touch AddTwoInts.srv

对AddTwoInts.srv写入内容:

cat > srv/AddTwoInts.srv << 'EOF'
> int64 a
> int64 b
> ---
> int64 sum
> EOF

工作原理:

cat > 文件名 - 开始创建文件
<< 'EOF' - 表示接下来的内容直到遇到EOF为止,都写入文件
中间的内容 - 实际要写入文件的内容
最后的EOF - 结束标记,告诉系统内容写完了,可以用任何词代替EOF

2、功能包demo_interface的文件

 AddTwoInts.srv:

int64 a
int64 b
---
int64 sum

CMakeLists.txt:

cmake_minimum_required(VERSION 3.8)
project(demo_interface)

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(rosidl_default_generators REQUIRED)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)

# 声明服务文件
set(srv_files
  "srv/AddTwoInts.srv"
)

# 生成接口代码
rosidl_generate_interfaces(${PROJECT_NAME}
  ${srv_files}
  DEPENDENCIES
)

# 导出依赖
ament_export_dependencies(rosidl_default_runtime)

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

ament_package()

package.xml:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>demo_interface</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="sean@todo.todo">sean</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <!-- 添加必要的依赖 -->
  <build_depend>rosidl_default_generators</build_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>
  
  <!-- 声明这是一个接口包(关键!) -->
  <member_of_group>rosidl_interface_packages</member_of_group>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

3、功能包learning_pkg_c的文件

add_two_ints_server.cpp:

/**
 * @file add_two_ints_server.cpp
 * @brief ROS2服务示例-提供两个整数相加的服务器处理功能
 * @author 
 */

#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "demo_interface/srv/add_two_ints.hpp"

using namespace std::placeholders;

class AddServer : public rclcpp::Node
{
public:
    AddServer(const std::string& node_name) : Node(node_name)
    {
        RCLCPP_INFO(this->get_logger(), "%s节点启动,欢迎使用双数据过滤器!", node_name.c_str());
        
        // 创建服务器对象(服务接口类型、服务名、服务器回调函数)
        srv_ = this->create_service<demo_interface::srv::AddTwoInts>(
            "add_two_int", 
            std::bind(&AddServer::adder_callback, this, _1, _2));
    }

private:
    // 服务回调函数
    void adder_callback(
        const std::shared_ptr<demo_interface::srv::AddTwoInts::Request> request,
        std::shared_ptr<demo_interface::srv::AddTwoInts::Response> response)
    {
        // 完成加法求和计算,将结果放到反馈的数据中
        response->sum = request->a + request->b;
        
        // 输出日志信息,提示已经完成加法求和计算
        RCLCPP_INFO(this->get_logger(), "Incoming request: %ld b: %ld", 
                   request->a, request->b);
    }
    
    rclcpp::Service<demo_interface::srv::AddTwoInts>::SharedPtr srv_;
};

int main(int argc, char* argv[])
{
    // ROS2 C++接口初始化
    rclcpp::init(argc, argv);
    
    // 创建ROS2节点对象并进行初始化
    auto node = std::make_shared<AddServer>("add2Ints_server_node");
    
    // 循环等待ROS2退出
    rclcpp::spin(node);
    
    // 销毁节点对象
    rclcpp::shutdown();
    
    return 0;
}

add_two_ints_client.cpp:

/**
 * @file add_two_ints_client.cpp
 * @brief ROS2服务示例-发送两个加数,请求加法器计算
 */

#include <chrono>
#include <memory>
#include <iostream>
#include "rclcpp/rclcpp.hpp"
#include "demo_interface/srv/add_two_ints.hpp"

using namespace std::chrono_literals;

class AddClient : public rclcpp::Node
{
public:
    AddClient(const std::string& name) : Node(name)
    {
        // 创建服务客户端对象(服务接口类型,服务名)
        client_ = this->create_client<demo_interface::srv::AddTwoInts>("add_two_int");
        
        // 创建服务请求的数据对象
        request_ = std::make_shared<demo_interface::srv::AddTwoInts::Request>();
    }
    
    // 发送服务请求
    void send_request(int a, int b)
    {
        request_->a = a;
        request_->b = b;
        
        // 循环等待服务器端成功启动
        while (!client_->wait_for_service(1s))
        {
            RCLCPP_WARN(this->get_logger(), "服务器不可用,等待中...");
        }
        
        // 异步方式发送服务请求,并绑定回调函数
        auto future_result = client_->async_send_request(
            request_, 
            std::bind(&AddClient::request_result_callback, this, std::placeholders::_1));
    }
    
private:
    // 请求结果接收回调函数
    void request_result_callback(rclcpp::Client<demo_interface::srv::AddTwoInts>::SharedFuture future)
    {
        auto response = future.get();
        
        // 将收到的反馈信息打印输出
        RCLCPP_INFO(this->get_logger(), "Result of add_two_int: %ld + %ld = %ld", 
                   request_->a, request_->b, response->sum);
        
        // 销毁节点对象并关闭
        rclcpp::shutdown();
    }
    
    rclcpp::Client<demo_interface::srv::AddTwoInts>::SharedPtr client_;
    std::shared_ptr<demo_interface::srv::AddTwoInts::Request> request_;
};

int main(int argc, char* argv[])
{
    // 检查命令行参数
    if (argc != 3)
    {
        std::cerr << "使用方法: " << argv[0] << " <数字A> <数字B>" << std::endl;
        return 1;
    }
    
    // ROS2 C++接口初始化
    rclcpp::init(argc, argv);
    
    // 创建ROS2节点对象并进行初始化
    auto node = std::make_shared<AddClient>("add2Ints_client_node");
    
    // 解析命令行参数并发送服务请求
    int a = std::atoi(argv[1]);
    int b = std::atoi(argv[2]);
    node->send_request(a, b);
    
    // 循环等待回调完成
    rclcpp::spin(node);
    
    return 0;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.8)
project(learning_pkg_c)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# 查找依赖包
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(demo_interface REQUIRED)

# 编译服务端节点
add_executable(add_server src/add_two_ints_server.cpp)
ament_target_dependencies(add_server rclcpp demo_interface)

# 编译客户端节点
add_executable(add_client src/add_two_ints_client.cpp)
ament_target_dependencies(add_client rclcpp demo_interface)

# 安装可执行文件 - 使用 ${PROJECT_NAME} 而不是 ${learning_pkg_c}
install(TARGETS 
  add_server
  add_client
  DESTINATION lib/${PROJECT_NAME})

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

ament_package()

package.xml:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>learning_pkg_c</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="sean@todo.todo">sean</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>rclcpp</depend>
  <depend>demo_interface</depend>   
  
  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

4、编译所有功能包并运行

切换路径到工作空间根目录下:cd ~/study_ws

可以先编译单个功能包,检查接口包是否安装成功:

(单个功能包的编译命令:colcon build --packages-select 功能包名)

colcon build --packages-select demo_interface

ros2 interface package demo_interface

编译创建的功能包:colcon build  

设置环境变量:source install/local_setup.sh

5、运行结果:

图5 服务端

图6 客户端

七、服务命令的常用操作

查看服务列表:ros2 service list

查看服务数据类型:ros2 service type <service_name>

发送服务请求:ros2 service call <service_name> <service_type> <service_data>

Logo

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

更多推荐