【ros2】ROS2 C++服务端与客户端开发指南

  • 时间:2025-11-20 20:44 作者: 来源: 阅读:1
  • 扫一扫,手机访问
摘要: 文章目录 ROS2 C++服务端与客户端开发指南一、核心概念:C++服务通信的特点与优势二、基于书中第4章的C++服务开发核心步骤1. 前置准备:创建功能包与依赖配置(1)创建C++功能包(2)配置`package.xml`(3)配置`CMakeLists.txt` 2. 服务端开发步骤(以书中海龟巡逻为例)(1)服务接口回顾(`.srv`文件)(2)服务端代码实现 3

文章目录

ROS2 C++服务端与客户端开发指南一、核心概念:C++服务通信的特点与优势二、基于书中第4章的C++服务开发核心步骤1. 前置准备:创建功能包与依赖配置(1)创建C++功能包(2)配置`package.xml`(3)配置`CMakeLists.txt` 2. 服务端开发步骤(以书中海龟巡逻为例)(1)服务接口回顾(`.srv`文件)(2)服务端代码实现 3. 客户端开发步骤(以书中随机巡逻为例)(1)客户端代码实现 4. 编译与运行(1)编译功能包(2)启动节点测试 三、书外拓展知识:C++服务通信高级特性1. 同步与异步调用的区别2. 服务的并发处理3. 服务的超时控制4. 服务与参数的结合5. 调试与监控工具 四、常见问题与解决方案五、总结

ROS2 C++服务端与客户端开发指南

(基于《ROS2机器人开发:从入门到实践》第4章及拓展知识)

一、核心概念:C++服务通信的特点与优势

服务(Service)是ROS2中基于“请求-响应”模式的双向通信机制,适用于需要明确反馈的交互场景(如任务执行、参数查询、算法计算等)。相比Python,C++实现的服务通信具有运行效率高、实时性强的特点,更适合机器人控制、传感器数据处理等性能敏感场景。

核心特征:

通信模式:客户端主动发送请求,服务端处理后返回响应(一对一交互)。接口定义:通过 .srv文件声明请求(Request)和响应(Response)数据结构,用 ---分隔。生命周期:服务仅在被调用时激活,处理完成后释放资源,适合低频率、高复杂度的任务。

二、基于书中第4章的C++服务开发核心步骤

1. 前置准备:创建功能包与依赖配置

(1)创建C++功能包

服务端和客户端需依赖ROS2核心库及自定义接口包,创建功能包命令如下:


# 创建C++功能包,依赖rclcpp(ROS2 C++库)和自定义接口包
ros2 pkg create demo_cpp_service --build-type ament_cmake 
--dependencies rclcpp chapt4_interfaces turtlesim
依赖说明: rclcpp:ROS2 C++核心库,提供节点、服务等基础功能。 chapt4_interfaces:自定义服务接口包(包含 .srv文件)。 turtlesim:示例中控制海龟运动的依赖(根据实际场景调整)。
(2)配置 package.xml

声明编译和运行依赖,确保功能包能正确引用接口和ROS2库:


<?xml version="1.0"?>
<package format="3">
  <name>demo_cpp_service</name>
  <version>0.0.0</version>
  <description>ROS2 C++服务示例</description>
  
  <!-- 编译依赖 -->
  <buildtool_depend>ament_cmake</buildtool_depend>
  <build_depend>rclcpp</build_depend>
  <build_depend>chapt4_interfaces</build_depend>
  <build_depend>turtlesim</build_depend>
  
  <!-- 运行依赖 -->
  <exec_depend>rclcpp</exec_depend>
  <exec_depend>chapt4_interfaces</exec_depend>
  <exec_depend>turtlesim</exec_depend>
  
  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>
(3)配置 CMakeLists.txt

指定可执行文件、依赖库及安装规则(核心配置):


cmake_minimum_required(VERSION 3.8)
project(demo_cpp_service)

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(chapt4_interfaces REQUIRED)
find_package(turtlesim REQUIRED)

# 服务端可执行文件
add_executable(turtle_control src/turtle_control.cpp)
ament_target_dependencies(turtle_control
  rclcpp
  chapt4_interfaces
  turtlesim
)

# 客户端可执行文件
add_executable(patrol_client src/patrol_client.cpp)
ament_target_dependencies(patrol_client
  rclcpp
  chapt4_interfaces
)

# 安装可执行文件到install目录
install(TARGETS
  turtle_control
  patrol_client
  DESTINATION lib/${PROJECT_NAME}
)

ament_package()

2. 服务端开发步骤(以书中海龟巡逻为例)

服务端的核心逻辑是:创建服务对象 → 定义请求处理回调函数 → 处理请求并生成响应

(1)服务接口回顾( .srv文件)

书中4.3节定义的巡逻服务接口 Patrol.srv


# 请求:目标位置坐标
float32 target_x
float32 target_y
---
# 响应:处理结果
int8 SUCCESS=1  # 成功常量
int8 FAIL=0     # 失败常量
int8 result     # 1表示成功,0表示失败
(2)服务端代码实现

// src/turtle_control.cpp
#include "rclcpp/rclcpp.hpp"
#include "geometry_msgs/msg/twist.hpp"          // 速度控制消息
#include "turtlesim/msg/pose.hpp"              // 海龟位置消息
#include "chapt4_interfaces/srv/patrol.hpp"    // 自定义巡逻服务接口

// 命名空间简化代码
using namespace std::placeholders;
using Patrol = chapt4_interfaces::srv::Patrol;
using Twist = geometry_msgs::msg::Twist;
using Pose = turtlesim::msg::Pose;

class TurtleController : public rclcpp::Node {
public:
  TurtleController() : Node("turtle_control_node") {
    // 1. 创建速度发布者(控制海龟运动)
    vel_pub_ = this->create_publisher<Twist>("/turtle1/cmd_vel", 10);
    
    // 2. 创建位置订阅者(获取海龟当前位置)
    pose_sub_ = this->create_subscription<Pose>(
      "/turtle1/pose", 10,
      std::bind(&TurtleController::pose_callback, this, _1)
    );
    
    // 3. 创建服务对象(服务名称:"patrol",回调函数:patrol_callback)
    patrol_service_ = this->create_service<Patrol>(
      "patrol",
      std::bind(&TurtleController::patrol_callback, this, _1, _2)
    );
    
    // 初始化目标位置和控制参数
    target_x_ = 5.0;
    target_y_ = 5.0;
    kp_ = 1.0;  // 比例控制系数
    
    RCLCPP_INFO(this->get_logger(), "海龟巡逻服务已启动");
  }

private:
  // 位置回调:根据当前位置计算速度指令(闭环控制)
  void pose_callback(const Pose::SharedPtr pose) {
    // 计算当前位置与目标位置的误差
    float dx = target_x_ - pose->x;
    float dy = target_y_ - pose->y;
    float distance = sqrt(dx*dx + dy*dy);  // 距离误差
    
    // 生成速度指令(比例控制)
    auto cmd_vel = Twist();
    if (distance > 0.1) {  // 误差大于阈值时移动
      cmd_vel.linear.x = kp_ * distance;  // 线速度与距离成正比
      cmd_vel.angular.z = 4.0 * atan2(dy, dx) - pose->theta;  // 角速度控制转向
    } else {
      cmd_vel.linear.x = 0.0;  // 误差小于阈值时停止
      cmd_vel.angular.z = 0.0;
    }
    vel_pub_->publish(cmd_vel);
  }
  
  // 服务回调:处理客户端的巡逻请求
  void patrol_callback(
    const std::shared_ptr<Patrol::Request> request,
    std::shared_ptr<Patrol::Response> response
  ) {
    // 检查目标位置是否在海龟模拟器边界内(0~12.0)
    if (request->target_x > 0 && request->target_x < 12.0 &&
        request->target_y > 0 && request->target_y < 12.0) {
      target_x_ = request->target_x;  // 更新目标位置
      target_y_ = request->target_y;
      response->result = Patrol::Response::SUCCESS;  // 响应成功
      RCLCPP_INFO(this->get_logger(), "接受巡逻请求:(%.2f, %.2f)", 
                 request->target_x, request->target_y);
    } else {
      response->result = Patrol::Response::FAIL;  // 响应失败(目标越界)
      RCLCPP_WARN(this->get_logger(), "目标位置越界:(%.2f, %.2f)", 
                 request->target_x, request->target_y);
    }
  }

  // 成员变量
  rclcpp::Publisher<Twist>::SharedPtr vel_pub_;         // 速度发布者
  rclcpp::Subscription<Pose>::SharedPtr pose_sub_;      // 位置订阅者
  rclcpp::Service<Patrol>::SharedPtr patrol_service_;   // 服务对象
  float target_x_, target_y_;  // 目标位置
  float kp_;  // 控制系数
};

// 主函数
int main(int argc, char * argv[]) {
  rclcpp::init(argc, argv);                  // 初始化ROS2
  rclcpp::spin(std::make_shared<TurtleController>());  // 运行节点
  rclcpp::shutdown();                        // 退出时清理
  return 0;
}

3. 客户端开发步骤(以书中随机巡逻为例)

客户端的核心逻辑是:创建客户端对象 → 发送请求 → 等待并处理响应

(1)客户端代码实现

// src/patrol_client.cpp
#include "rclcpp/rclcpp.hpp"
#include "chapt4_interfaces/srv/patrol.hpp"  // 巡逻服务接口
#include <chrono>       // 时间相关
#include <cstdlib>      // 随机数
#include <ctime>        // 时间种子

using namespace std::chrono_literals;
using Patrol = chapt4_interfaces::srv::Patrol;

class PatrolClient : public rclcpp::Node {
public:
  PatrolClient() : Node("patrol_client_node") {
    // 1. 创建客户端对象(服务名称:"patrol",需与服务端一致)
    client_ = this->create_client<Patrol>("patrol");
    
    // 2. 创建定时器:每10秒发送一次巡逻请求
    timer_ = this->create_wall_timer(
      10s,  // 10秒间隔
      std::bind(&PatrolClient::send_request, this)
    );
    
    // 初始化随机数种子
    std::srand(std::time(nullptr));
    RCLCPP_INFO(this->get_logger(), "巡逻客户端已启动");
  }

private:
  // 发送请求函数
  void send_request() {
    // 等待服务端上线(超时1秒重试)
    while (!client_->wait_for_service(1s)) {
      if (!rclcpp::ok()) {  // 若ROS2退出,终止等待
        RCLCPP_ERROR(this->get_logger(), "等待服务被中断");
        return;
      }
      RCLCPP_INFO(this->get_logger(), "等待服务端上线...");
    }
    
    // 生成随机目标位置(0~12.0,海龟模拟器边界)
    auto request = std::make_shared<Patrol::Request>();
    request->target_x = std::rand() % 12;  // 随机x坐标
    request->target_y = std::rand() % 12;  // 随机y坐标
    
    // 异步发送请求(非阻塞),并绑定响应回调
    auto result_future = client_->async_send_request(
      request,
      std::bind(&PatrolClient::response_callback, this, _1)
    );
  }
  
  // 响应回调函数:处理服务端返回的结果
  void response_callback(rclcpp::Client<Patrol>::SharedFuture future) {
    auto response = future.get();  // 获取响应
    if (response->result == Patrol::Response::SUCCESS) {
      RCLCPP_INFO(this->get_logger(), "巡逻请求已执行");
    } else {
      RCLCPP_WARN(this->get_logger(), "巡逻请求失败(目标越界)");
    }
  }

  // 成员变量
  rclcpp::Client<Patrol>::SharedPtr client_;  // 客户端对象
  rclcpp::TimerBase::SharedPtr timer_;        // 定时器
};

// 主函数
int main(int argc, char * argv[]) {
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<PatrolClient>());
  rclcpp::shutdown();
  return 0;
}

4. 编译与运行

(1)编译功能包

colcon build --packages-select demo_cpp_service
source install/setup.bash  # 生效环境变量
(2)启动节点测试

# 终端1:启动海龟模拟器
ros2 run turtlesim turtlesim_node

# 终端2:启动服务端(海龟控制节点)
ros2 run demo_cpp_service turtle_control

# 终端3:启动客户端(随机巡逻请求)
ros2 run demo_cpp_service patrol_client

三、书外拓展知识:C++服务通信高级特性

1. 同步与异步调用的区别

同步调用 call()):阻塞当前线程,直到收到响应,适合简单场景:


// 同步调用示例(会阻塞,不推荐在回调中使用)
auto request = std::make_shared<Patrol::Request>();
request->target_x = 5.0;
request->target_y = 5.0;
auto response = client_->call(request);  // 阻塞等待响应

异步调用 async_send_request()):非阻塞,通过 SharedFuture获取结果,适合实时性要求高的场景:


// 异步调用(非阻塞)
auto result_future = client_->async_send_request(request);
// 可通过等待或回调处理结果(推荐回调方式)

2. 服务的并发处理

ROS2服务端默认支持多客户端并发调用,回调函数会在独立线程中执行。若服务处理逻辑涉及共享资源(如全局变量),需通过互斥锁保证线程安全:


#include <mutex>  // 互斥锁头文件

class TurtleController : public rclcpp::Node {
private:
  std::mutex mutex_;  // 互斥锁
  float shared_data_; // 共享资源

  void patrol_callback(
    const std::shared_ptr<Patrol::Request> request,
    std::shared_ptr<Patrol::Response> response
  ) {
    std::lock_guard<std::mutex> lock(mutex_);  // 自动加锁/解锁
    shared_data_ = request->target_x;  // 安全访问共享资源
    // ...
  }
};

3. 服务的超时控制

客户端可设置请求超时时间,避免无限等待:


// 异步调用+超时控制
auto result_future = client_->async_send_request(request);
// 等待5秒,若超时则处理错误
if (result_future.wait_for(5s) == std::future_status::timeout) {
  RCLCPP_ERROR(this->get_logger(), "请求超时");
} else {
  auto response = result_future.get();  // 处理响应
}

4. 服务与参数的结合

服务端可通过参数动态调整行为(如控制系数),示例:


// 在服务端初始化时声明参数
this->declare_parameter("kp", 1.0);  // 声明比例系数参数
kp_ = this->get_parameter("kp").as_double();  // 获取参数值

// 动态更新参数的回调
rclcpp::ParameterCallbackHandle::SharedPtr param_handle_;
param_handle_ = this->add_on_set_parameters_callback(
  [this](const std::vector<rclcpp::Parameter> &params) {
    auto result = rcl_interfaces::msg::SetParametersResult();
    result.successful = true;
    for (const auto &param : params) {
      if (param.get_name() == "kp") {
        kp_ = param.as_double();  // 更新控制系数
        RCLCPP_INFO(this->get_logger(), "kp更新为:%.2f", kp_);
      }
    }
    return result;
  }
);

5. 调试与监控工具

ros2 service list:查看所有活跃服务。 ros2 service type /patrol:查看服务接口类型。 ros2 service call /patrol chapt4_interfaces/srv/Patrol "{target_x: 3.0, target_y: 4.0}":命令行调用服务。 rqt_graph:可视化服务与节点的连接关系。 ros2 doctor:检查服务通信是否存在异常。

四、常见问题与解决方案

问题现象可能原因解决方案
编译报错“未定义接口类型”未在 CMakeLists.txt中添加接口依赖;接口包未编译确保 ament_target_dependencies包含接口包;先编译接口包
服务调用失败“服务不存在”服务名称不匹配;服务端未启动;命名空间错误检查服务名称和命名空间;用 ros2 service list确认服务是否存在
运行时崩溃“空指针访问”请求/响应对象未初始化;订阅者/发布者未正确创建确保 create_service/ create_client返回非空指针;检查消息指针是否有效
并发调用导致数据错乱共享资源未加锁使用 std::mutex保护共享变量

五、总结

C++开发ROS2服务端与客户端的核心流程可概括为:

定义服务接口( .srv)并编译接口包。服务端:继承 rclcpp::Node,创建服务对象,实现请求处理回调函数,通过发布者/订阅者完成业务逻辑。客户端:创建客户端对象,异步发送请求,通过回调处理响应。

相比Python,C++需更多关注内存管理(如智能指针 SharedPtr)和线程安全(如互斥锁),但能提供更高的性能。结合参数动态配置和超时控制,可实现更健壮的服务通信逻辑,满足机器人系统的实时性需求。

  • 全部评论(0)
手机二维码手机访问领取大礼包
返回顶部