4、ROS2核心编程:使用Python和C++编写发布订阅、服务客户端、动作服务器与客户端,掌握rclpy和rclcpp

ROS2的编程模型围绕三大通信原语展开:发布-订阅(Topic)、服务(Service)和动作(Action)。Topic用于连续数据流(如激光雷达点云、车辆速度),Service用于同步请求-响应(如路径规划请求),Action用于长时间运行且可取消的任务(如泊车、变道)。本章将深入剖析rclpy(Python客户端库)与rclcpp(C++客户端库)的实现差异,通过完整代码案例帮助开发者建立跨语言开发能力。理解这三类通信机制的本质区别,是构建可靠自动驾驶系统的前提——错误地使用Service传输高频传感器数据会导致节点阻塞,而用Topic处理需要确认的任务则可能丢失关键指令。

ROS2 三大通信原语架构与rclpy/rclcpp映射 ROS2 节点 (Node) 发布-订阅 (Topic) 异步/连续/一对多 服务 (Service) 同步/请求-响应/一对多 动作 (Action) 异步/可取消/带反馈 rclpy: Publisher/Subscription create_publisher() create_subscription() QoS: depth/durability 回调函数处理 rclcpp: Service/Client create_service() create_client() 同步/异步请求 超时与重试机制 rclpy: ActionServer/Client create_action_server() create_action_client() goal/feedback/result 取消与抢占 关键对比维度 通信模式:Topic异步/Service同步/Action异步+反馈 适用场景:Topic-传感器流/Service-指令查询/Action-复杂任务 可靠性:Topic依赖QoS/Service需超时处理/Action支持取消与重试

上图展示了ROS2中三种通信原语与rclpy/rclcpp的映射关系。在自动驾驶系统中,Topic用于处理摄像头图像流(30Hz)、Service用于路径规划请求(低频)、Action用于车辆控制指令(需要反馈和取消)。开发者需要根据数据频率、实时性、可靠性要求选择正确的通信方式。

4.1 核心原理:通信模型与QoS机制

ROS2的通信基于DDS(Data Distribution Service)中间件,相比ROS1的TCPROS,DDS提供了更丰富的QoS(Quality of Service)策略。QoS决定了消息传递的可靠性、历史数据保留策略和时效性。在自动驾驶场景中,不同数据对QoS的需求差异巨大:激光雷达点云需要"尽力传输"(BEST_EFFORT)以保证低延迟,而车辆控制指令需要"可靠传输"(RELIABLE)确保不丢失。

QoS策略Topic示例Service示例Action示例
可靠性BEST_EFFORT(传感器)RELIABLE(指令)RELIABLE(控制)
历史记录KEEP_LAST(10)KEEP_LAST(1)KEEP_LAST(5)
时效性DEADLINE(100ms)无DEADLINE(500ms)
生命周期VOLATILETRANSIENT_LOCALVOLATILE

rclpy和rclcpp在API设计上保持高度一致,但底层实现不同。rclpy基于C扩展库rcl,通过Python绑定实现;rclcpp直接使用C++模板和智能指针。性能上,rclcpp在千字节级消息处理上比rclpy快约30%,但在开发效率上rclpy更优。实际项目中常采用混合编程:性能关键节点用C++,算法原型和工具脚本用Python。

4.2 发布-订阅编程实战

发布-订阅是ROS2中最常用的通信模式。在自动驾驶中,感知节点发布障碍物列表,规划节点订阅该列表生成轨迹。下面展示一个完整的Python发布订阅示例,包含QoS配置和生命周期管理。

# Python发布者 (rclpy)
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32MultiArray
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy

class ObstaclePublisher(Node):
    def __init__(self):
        super().__init__('obstacle_publisher')
        qos = QoSProfile(
            depth=10,
            reliability=ReliabilityPolicy.BEST_EFFORT,
            history=HistoryPolicy.KEEP_LAST
        )
        self.publisher = self.create_publisher(Float32MultiArray, 'obstacles', qos)
        self.timer = self.create_timer(0.1, self.publish_callback)  # 10Hz
        
    def publish_callback(self):
        msg = Float32MultiArray()
        msg.data = [1.5, 2.3, 0.8]  # 障碍物坐标x,y,z
        self.publisher.publish(msg)
        self.get_logger().info(f'发布障碍物: {msg.data}')

def main():
    rclpy.init()
    node = ObstaclePublisher()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

C++订阅者实现如下,注意使用智能指针和回调组(CallbackGroup)来避免回调阻塞。

// C++订阅者 (rclcpp)
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/float32_multi_array.hpp>
#include <rclcpp/qos.hpp>

class ObstacleSubscriber : public rclcpp::Node {
public:
    ObstacleSubscriber() : Node("obstacle_subscriber") {
        auto qos = rclcpp::QoS(10).best_effort();
        subscription_ = this->create_subscription<Float32MultiArray>(
            "obstacles", qos,
            [this](const Float32MultiArray::SharedPtr msg) {
                RCLCPP_INFO(this->get_logger(), "收到障碍物: [%.2f, %.2f, %.2f]",
                    msg->data[0], msg->data[1], msg->data[2]);
            });
    }
private:
    rclcpp::Subscription<Float32MultiArray>::SharedPtr subscription_;
};

int main(int argc, char** argv) {
    rclcpp::init(argc, argv);
    auto node = std::make_shared<ObstacleSubscriber>();
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}
经验提醒:在Python中,回调函数默认在单线程执行器中运行,耗时操作会阻塞其他回调。建议使用MultiThreadedExecutor并配置回调组。C++中默认使用单线程执行器,但可以通过rclcpp::executors::MultiThreadedExecutor实现并行处理。

4.3 服务通信与客户端实现

服务通信适用于请求-响应模式,如路径规划请求、地图加载指令。在自动驾驶中,服务常用于低频控制指令(如启动/停止)和参数查询。服务分为服务器(Server)和客户端(Client)两端,客户端发送请求后阻塞等待响应,直到超时或收到响应。

# Python服务端 (rclpy)
from example_interfaces.srv import AddTwoInts

class PathPlannerService(Node):
    def __init__(self):
        super().__init__('path_planner_service')
        self.srv = self.create_service(AddTwoInts, 'plan_path', self.plan_callback)
        
    def plan_callback(self, request, response):
        self.get_logger().info(f'收到规划请求: {request.a} + {request.b}')
        response.sum = request.a + request.b  # 简化示例
        return response
// C++客户端 (rclcpp)
#include <rclcpp/rclcpp.hpp>
#include <example_interfaces/srv/add_two_ints.hpp>

class PathPlannerClient : public rclcpp::Node {
public:
    PathPlannerClient() : Node("path_planner_client") {
        client_ = this->create_client<AddTwoInts>("plan_path");
    }
    
    void send_request(int a, int b) {
        auto request = std::make_shared<AddTwoInts::Request>();
        request->a = a;
        request->b = b;
        
        while (!client_->wait_for_service(std::chrono::seconds(1))) {
            RCLCPP_WARN(this->get_logger(), "等待服务上线...");
        }
        
        auto future = client_->async_send_request(request);
        if (rclcpp::spin_until_future_complete(this->get_node_base_interface(), future) == 
            rclcpp::FutureReturnCode::SUCCESS) {
            RCLCPP_INFO(this->get_logger(), "规划结果: %ld", future.get()->sum);
        } else {
            RCLCPP_ERROR(this->get_logger(), "服务调用失败");
        }
    }
private:
    rclcpp::Client<AddTwoInts>::SharedPtr client_;
};
风险提示:服务通信是同步阻塞的,如果服务端处理时间过长(如复杂路径规划),客户端会一直等待。在自动驾驶中,建议将耗时服务改为Action模式,以便提供进度反馈和取消能力。同时,客户端必须设置合理的超时时间,避免无限等待导致节点挂起。

4.4 动作服务器与客户端开发

动作(Action)是ROS2中最复杂的通信模式,适用于需要长时间执行、可取消、带反馈的任务。在自动驾驶中,动作典型应用包括:自动泊车(需要反馈进度)、车道变更(可取消)、紧急制动(需要确认完成)。动作由三部分组成:Goal(目标)、Feedback(反馈)、Result(结果)。

# Python动作服务器 (rclpy)
from action_tutorials_interfaces.action import Fibonacci

class ParkingActionServer(Node):
    def __init__(self):
        super().__init__('parking_action_server')
        self.action_server = self.create_action_server(
            Fibonacci, 'parking', self.execute_callback)
    
    async def execute_callback(self, goal_handle):
        self.get_logger().info(f'开始泊车,目标位置: {goal_handle.request.order}')
        feedback_msg = Fibonacci.Feedback()
        feedback_msg.sequence = [0, 1]
        
        for i in range(1, goal_handle.request.order):
            if goal_handle.is_cancel_requested:
                goal_handle.canceled()
                return Fibonacci.Result()
            feedback_msg.sequence.append(feedback_msg.sequence[-1] + feedback_msg.sequence[-2])
            goal_handle.publish_feedback(feedback_msg)
            await asyncio.sleep(0.5)  # 模拟泊车过程
        
        goal_handle.succeed()
        result = Fibonacci.Result()
        result.sequence = feedback_msg.sequence
        return result
// C++动作客户端 (rclcpp)
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <action_tutorials_interfaces/action/fibonacci.hpp>

class ParkingActionClient : public rclcpp::Node {
public:
    using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
    using GoalHandle = rclcpp_action::ClientGoalHandle<Fibonacci>;
    
    ParkingActionClient() : Node("parking_action_client") {
        client_ = rclcpp_action::create_client<Fibonacci>(this, "parking");
    }
    
    void send_goal(int order) {
        auto goal_msg = Fibonacci::Goal();
        goal_msg.order = order;
        
        auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions();
        send_goal_options.goal_response_callback = 
            [this](GoalHandle::SharedPtr goal_handle) {
                if (goal_handle) {
                    RCLCPP_INFO(this->get_logger(), "目标被接受");
                } else {
                    RCLCPP_ERROR(this->get_logger(), "目标被拒绝");
                }
            };
        send_goal_options.feedback_callback = 
            [this](GoalHandle::SharedPtr, const std::shared_ptr<const Fibonacci::Feedback> feedback) {
                RCLCPP_INFO(this->get_logger(), "反馈: %ld", feedback->sequence.back());
            };
        send_goal_options.result_callback = 
            [this](const GoalHandle::WrappedResult& result) {
                if (result.code == rclcpp_action::ResultCode::SUCCEEDED) {
                    RCLCPP_INFO(this->get_logger(), "泊车成功");
                }
            };
        client_->async_send_goal(goal_msg, send_goal_options);
    }
private:
    rclcpp_action::Client<Fibonacci>::SharedPtr client_;
};

4.5 案例拆解:自动驾驶中的混合编程实践

在某自动驾驶项目开发中,团队需要实现一个"动态避障"功能。系统包含三个节点:感知节点(Python,处理摄像头数据)、规划节点(C++,生成避障轨迹)、控制节点(C++,执行轨迹)。初始方案全部使用Python,但在实际测试中发现规划节点处理延迟高达200ms,无法满足100ms的实时性要求。

性能分析:通过ros2 topic hz /obstacles和ros2 topic delay /trajectory命令检测,发现瓶颈在Python的GIL锁和消息序列化开销。规划节点需要处理大量矩阵运算,Python的NumPy虽然高效,但消息转换和回调调度消耗了过多时间。

优化方案:将规划节点重写为C++,利用Eigen库进行矩阵运算,并使用rclcpp::executors::MultiThreadedExecutor并行处理订阅回调。感知节点保持Python,因为其算法迭代频繁,Python开发效率更高。控制节点使用C++以确保实时性。

实施步骤:1)定义自定义消息ObstacleArray.msg和Trajectory.msg;2)用C++重写规划节点,使用create_subscription订阅障碍物,create_publisher发布轨迹;3)配置QoS,感知节点使用BEST_EFFORT,规划节点使用RELIABLE;4)使用ros2 launch启动所有节点,设置参数executor: multi_threaded。

结果对比:优化后规划节点延迟从200ms降至45ms,系统整体响应时间满足100ms要求。CPU占用率从85%降至60%,内存占用减少30%。

复盘经验:混合编程的关键在于明确性能边界。Python适合算法原型、数据可视化和工具脚本;C++适合实时控制、高频数据处理和性能关键路径。在项目初期就应建立性能测试基准,避免后期大规模重构。

总结复盘:本章覆盖了ROS2三大通信原语的完整实现。开发者应掌握以下核心能力:1)根据数据特征选择正确的通信模式;2)熟练配置QoS策略以满足可靠性要求;3)理解rclpy和rclcpp的API差异,能够进行混合编程;4)使用ros2 topic echo、ros2 service list、ros2 action info等命令行工具进行调试。在自动驾驶项目中,建议采用"Python原型验证 + C++生产部署"的开发流程,兼顾开发效率和运行性能。