目录
[一、为什么还需要 Action 通信?](#一、为什么还需要 Action 通信?)
[1.1 前面三种通信方式分别解决了什么?](#1.1 前面三种通信方式分别解决了什么?)
[(1)Topic 通信](#(1)Topic 通信)
[(2)自定义 msg 通信](#(2)自定义 msg 通信)
[(3)自定义 srv 通信](#(3)自定义 srv 通信)
[1.2 Topic、Service、Action 到底有什么区别?](#1.2 Topic、Service、Action 到底有什么区别?)
[(3)Service 是否一定立马返回?](#(3)Service 是否一定立马返回?)
(4)Action:发送一个目标,过程中持续反馈,最后返回结果
[1.3 Service 与 Action](#1.3 Service 与 Action)
[(1)Service 的局限性](#(1)Service 的局限性)
[(2)Action 适合什么场景?](#(2)Action 适合什么场景?)
[二、自定义 Action 接口文件](#二、自定义 Action 接口文件)
[2.1 Action 文件放在哪里?](#2.1 Action 文件放在哪里?)
[2.2 LimoAction.action 文件内容](#2.2 LimoAction.action 文件内容)
[三、修改 limo_msgs 接口包配置](#三、修改 limo_msgs 接口包配置)
[3.1 修改 CMakeLists.txt](#3.1 修改 CMakeLists.txt)
[3.2 修改 package.xml](#3.2 修改 package.xml)
[3.3 编译接口包](#3.3 编译接口包)
[四、Action 服务端代码](#四、Action 服务端代码)
[4.1 Action 服务端的作用](#4.1 Action 服务端的作用)
[4.2 新建服务端文件](#4.2 新建服务端文件)
[五、Action 客户端代码](#五、Action 客户端代码)
[5.1 Action 客户端的作用](#5.1 Action 客户端的作用)
[5.2 新建客户端文件](#5.2 新建客户端文件)
[六、修改 limo_learning 功能包](#六、修改 limo_learning 功能包)
[6.1 正常功能包目录:](#6.1 正常功能包目录:)
[6.2 修改 limo_learning 的 CMakeLists.txt](#6.2 修改 limo_learning 的 CMakeLists.txt)
[(1)添加 action 依赖](#(1)添加 action 依赖)
[6.3 修改 package.xml](#6.3 修改 package.xml)
[7.1 编译功能包](#7.1 编译功能包)
[7.2 先启动 Action 服务端](#7.2 先启动 Action 服务端)
[7.3 再启动 Action 客户端](#7.3 再启动 Action 客户端)
[7.4 查看 Action 列表](#7.4 查看 Action 列表)
[7.5 查看 Action 信息](#7.5 查看 Action 信息)
[7.6 使用命令行发送 Action 目标](#7.6 使用命令行发送 Action 目标)
[八、Topic、Service、Action 对比总结](#八、Topic、Service、Action 对比总结)
[8.1 Topic 通信](#8.1 Topic 通信)
[8.2 Service 通信](#8.2 Service 通信)
[8.3 Action 通信](#8.3 Action 通信)
[8.4 三者对比表](#8.4 三者对比表)
摘要
前面几篇文章已经分别详细讲解了 ROS2 C++ 小车控制中的几种常见通信方式:
第一篇主要讲解:Topic + 官方标准 msg:
ROS2 C++ 小车控制完整实战(一):一文搞懂 Topic 发布订阅、Twist 消息与 /cmd_vel 速度控制-CSDN博客
https://blog.csdn.net/m0_58954356/article/details/161829804?spm=1001.2014.3001.5502也就是使用ROS2 官方已经定义好的消息类型,例如:
geometry_msgs/msg/Twist
通过 /cmd_vel 话题发布速度指令,实现对小车前进、后退、转向等基础运动控制。
第二篇主要讲解:Topic + 自定义 msg:
ROS2 C++ 小车控制完整实战(二):自定义 msg 消息发布与订阅保姆级教程-CSDN博客
https://blog.csdn.net/m0_58954356/article/details/161834874?spm=1001.2014.3001.5502我们自己定义了 LimoStatus.msg,用来发布小车状态信息,例如:
电池电压、车辆状态、控制模式、运动模式、错误码等。
第三篇主要讲解:Service + 自定义 srv:
ROS2 C++ 小车控制完整实战(三):自定义 srv 服务通信保姆级教程-CSDN博客
https://blog.csdn.net/m0_58954356/article/details/161834909?spm=1001.2014.3001.5502也就是客户端发送一次请求,服务端处理后返回一次响应。例如客户端发送 x、y、z 速度参数,服务端收到后控制小车运动,并返回 success 表示是否执行成功。
但是 Service 更适合"短时间、一次性、有明确返回结果"的任务。
如果任务执行时间比较长,比如:
- 让小车持续运动一段时间
- 让小车执行导航任务
- 让小车执行巡检任务
- 让小车执行一个可以反馈进度的控制任务
这时候就更适合使用 Action 通信。
本篇文章就继续在前面 Topic、msg、srv 的基础上,讲解 ROS2 中更适合长时间任务的通信方式:
自定义 Action 通信。
需要说明的是:
本篇重点先放在 Action 通信的整体流程和工程配置上,让大家先知道 Action 是什么、Action 文件怎么写、功能包怎么配置、代码怎么编译运行、终端命令怎么测试。
至于 Action 服务端和客户端代码中每一行到底是什么意思,例如 create_server、create_client、GoalHandle、handle_goal、handle_cancel、handle_accepted、execute、feedback_callback、result_callback 等核心内容,会放到下一篇文章中单独进行详细拆解。
功能包完整目录:
bash
ros2_ws/
└── src
├── limo_msgs
│ ├── msg
│ │ └── LimoStatus.msg
│ ├── srv
│ │ └── LimoSrv.srv
│ ├── action
│ │ └── LimoAction.action
│ ├── CMakeLists.txt
│ └── package.xml
│
└── limo_learning
├── src
│ ├── limo_topic_cmd.cpp
│ ├── limo_topic_sub.cpp
│ ├── limo_srv_server.cpp
│ ├── limo_srv_client.cpp
│ ├── limo_action_server.cpp
│ └── limo_action_client.cpp
├── CMakeLists.txt
└── package.xml
这里有两个包:
| 功能包 | 作用 |
|---|---|
limo_msgs |
定义自定义接口:msg、srv、action |
limo_learning |
编写 C++ 节点代码:Topic、Service、Action 服务端和客户端 |
一、为什么还需要 Action 通信?

1.1 前面三种通信方式分别解决了什么?
在正式写 Action 代码之前,我们先回顾一下前面几篇文章中已经使用过的 ROS2 通信方式。
前面主要讲了三部分内容:
- Topic 通信
- 自定义 msg 通信
- 自定义 srv 通信
这三种通信方式各自解决的问题并不一样。
(1)Topic 通信
Topic 通信适合持续不断地发布数据。
例如:
小车速度控制:
/cmd_vel
小车状态发布:
/limo_status
雷达数据发布:
/scan
Topic 的特点是:
- 发布者只管发布,订阅者只管接收。
- 发布者不关心订阅者有没有收到,也不等待返回结果。
所以 Topic 适合:
- 持续发布速度
- 持续发布状态
- 持续发布传感器数据
Topic 就像广播。
(2)自定义 msg 通信
自定义 msg 的作用是:
当 ROS2 官方提供的消息类型不够用时,我们可以自己定义消息结构。
例如前面我们定义过:
LimoStatus.msg
里面包含:
std_msgs/Header header
uint8 vehicle_state
uint8 control_mode
uint8 motion_mode
float64 battery_voltage
uint16 error_code
这样就可以把小车状态封装成一个完整的自定义消息,然后通过 Topic 发布出去。
所以:
msg 解决的是"传什么数据"的问题。
(3)自定义 srv 通信
Service 通信适合一次请求、一次响应。
例如前面我们定义过:
LimoSrv.srv
内容类似:
float32 x
float32 y
float32 z
---
bool success
客户端发送请求:
x = 0.2
y = 0.0
z = 0.0
服务端收到请求后,转换成 Twist 速度指令发布到 /cmd_vel,然后返回:
success = true
所以 Service 适合:
- 客户端请求一次
- 服务端处理一次
- 服务端返回一次结果
Service 就像"问一句,答一句"。
1.2 Topic、Service、Action 到底有什么区别?

在正式讲 Action 之前,一定要先把 ROS2 中最常见的三种通信方式区分清楚:
- Topic
- Service
- Action
这三种通信方式都可以让 ROS2 节点之间进行数据交互,但是它们适合的场景完全不一样。
可以先用一张表简单对比:
| 通信方式 | 是否持续通信 | 是否有反馈 / 结果 | 适合场景 |
|---|---|---|---|
| Topic | 持续发布 / 持续接收 | 没有返回结果 | 速度、雷达、状态、里程计 |
| Service | 一次请求 / 一次响应 | 有最终响应 | 查询、开关、短时间任务 |
| Action | 一个目标 / 持续反馈 / 最终结果 | 有过程反馈,也有最终结果 | 导航、巡检、长时间任务 |
简单来说:
- Topic 适合持续数据流。
- Service 适合一次性请求。
- Action 适合长时间任务。
(1)Topic:一直持续,但是没有反馈
- Topic 通信就像广播。
- 发布者 一直向某个话题发布数据,订阅者只要订阅了这个话题,就可以持续接收数据。
例如:
发布者 → /cmd_vel → 订阅者
发布者只管发布,订阅者只管接收。
Topic 的特点是:
- 持续通信
- 异步通信
- 没有返回结果
比如在小车控制中:
/cmd_vel 持续发送速度指令
/scan 持续发送雷达数据
/odom 持续发送里程计数据
/limo_status 持续发送小车状态
这些数据都适合用 Topic。
因为它们不是问答关系,而是持续不断地发布和接收。
一句话理解:
Topic 就是一直发、一直收,但是不返回结果。
(2)Service:一次请求,一次处理,一次返回结果
- Service 通信更像去窗口办业务。
- 客户端先提出一个请求,服务端收到请求后进行处理,处理完成后再返回响应结果。
整体流程是:
客户端发送请求
↓
服务端收到请求
↓
服务端处理请求
↓
服务端返回响应结果
↓
客户端拿到结果
所以 Service 不是一直通信,而是:
请求一次,处理一次,返回一次。
例如前面自定义 srv 通信中,客户端发送:
x = 0.2
y = 0.0
z = 0.0
服务端收到请求后,把它转换成 Twist 速度指令,然后发布到:
/cmd_vel
最后返回:
success = true
这就是典型的 Service 通信。
一句话理解:
Service 就是客户端问一次,服务端处理一次,然后返回一次结果。
(3)Service 是否一定立马返回?
这里是很多初学者最容易混淆的地方。
Service 不是一定"立马返回",而是:
服务端处理完这一次请求后,才会返回结果。
也就是说,Service 返回快还是慢,取决于服务端回调函数里面到底做了多少事情。
比如客户端请求:
让小车前进 1 秒。
如果服务端代码里面这样写:
收到请求
发布速度
等待 1 秒
停止小车
返回 success = true
那么客户端就需要等服务端这一整套流程执行完,才能拿到返回结果。
但是如果服务端代码这样写:
收到请求
发布一次速度
立刻返回 success = true
那么客户端就会很快收到响应结果。
所以关键不在于 Service 本身,而在于:
服务端回调函数里面到底执行了多长时间。
可以这样理解:
Service 返回的时机 = 服务端处理完这一次请求并设置 response 的时机
- 因此,Service 更适合短时间任务。
- 如果任务执行时间很长,客户端一直等待响应就不太合适了。
(4)Action:发送一个目标,过程中持续反馈,最后返回结果
Action 通信适合长时间任务。
它不是简单的"一问一答",而是:
客户端发送目标 Goal
↓
服务端开始执行任务
↓
执行过程中不断发送 Feedback
↓
任务完成后返回 Result
比如导航任务:
目标:去 A 点
反馈:当前完成 20%
反馈:当前完成 50%
反馈:当前完成 80%
结果:到达成功
Action 和 Service 最大的区别。
- Service 只有最终响应。
- Action 不仅有最终结果,还可以在执行过程中持续反馈状态。
Action 的特点是:
- 可以执行长时间任务
- 中途可以持续反馈
- 最后有最终结果
- 任务中途还可以取消
Action 就是发送一个目标,服务端慢慢执行,执行过程中不断反馈,最后返回最终结果。
(5)更严谨地理解三种通信方式
- Topic 是异步持续通信,发布者和订阅者互不等待,也没有返回结果。
- Service 通常用于一次性请求-响应通信,客户端发送请求后,等待服务端处理完成并返回响应结果,适合短时间任务。
- Action 是异步的目标任务通信,客户端发送目标后,服务端可以在执行过程中持续返回反馈,任务完成后再返回最终结果,适合长时间任务。
结合小车控制来理解:
Topic:持续发送 /cmd_vel 速度控制小车
Service:请求小车执行一次简单动作,然后返回是否成功
Action:请求小车执行一个长时间任务,过程中反馈状态,完成后返回最终结果
到这里就可以自然引出本篇文章的重点:
当我们希望小车执行一个持续一段时间的任务,并且希望在执行过程中不断收到状态反馈时,就应该使用 Action 通信。
1.3 Service 与 Action
(1)Service 的局限性
Service 虽然很适合一次性请求,但是它不太适合长时间任务。
比如现在有一个需求:
让小车向前运动 10 秒,并且执行过程中不断反馈当前执行状态。
如果使用 Service,就会出现一个问题:
客户端发送请求后,只能等待服务端最终返回结果。
在任务执行过程中,客户端很难持续知道:
- 当前执行到哪一步了
- 小车是否还在运动
- 任务有没有中途失败
- 任务进度是多少
也就是说,Service 只适合短任务,不适合长任务。
(2)Action 适合什么场景?
Action 通信就是为长时间任务设计的。
Action 比 Service 多了一个非常重要的能力:
可以在任务执行过程中持续反馈状态。
Action 通信一般包含三部分:
- 目标 Goal
- 反馈 Feedback
- 结果 Result
可以理解为:
- 客户端先发送一个任务目标
- 服务端开始执行任务
- 执行过程中不断返回反馈信息
- 任务结束后返回最终结果
例如:
客户端发送目标:
让小车以 x=0.2 的速度前进
服务端执行过程中持续反馈:
- 当前状态 status = 1
- 当前状态 status = 2
- 当前状态 status = 3
任务执行完成后返回结果:
success = true
所以 Action 更适合:
- 导航任务
- 巡检任务
- 机械臂执行任务
- 小车长时间运动控制
- 需要中途反馈进度的任务
一句话总结:
Action 适合"执行时间较长,并且需要反馈过程状态"的任务。
二、自定义 Action 接口文件

2.1 Action 文件放在哪里?
前面我们已经创建过接口功能包:
limo_msgs
这个功能包中可以放:
msg
srv
action
对应三种自定义接口:
cpp
limo_msgs/
├── msg
│ └── LimoStatus.msg
├── srv
│ └── LimoSrv.srv
├── action
│ └── LimoAction.action
├── CMakeLists.txt
└── package.xml
本篇要新增的就是:
action/LimoAction.action
2.2 LimoAction.action 文件内容
创建文件:
limo_msgs/action/LimoAction.action
写入内容:
float32 x
float32 y
float32 z
---
bool success
---
uint32 status
这个 action 文件中有三个部分,中间用两个 --- 分隔。
(1)Goal:目标部分
第一部分是 Goal,也就是客户端发送给服务端的目标。
float32 x
float32 y
float32 z
这里的含义是:
x:小车线速度 x 方向
y:小车线速度 y 方向
z:小车角速度 z 方向
后面服务端会把它们转换成:
geometry_msgs::msg::Twist
对应关系是:
twist.linear.x = goal->x;
twist.linear.y = goal->y;
twist.angular.z = goal->z;
也就是说:
客户端通过 Action 发送目标速度,服务端根据目标速度控制小车运动。
(2)Result:结果部分
第二部分是 Result,也就是任务执行完成后,服务端返回给客户端的最终结果。
bool success
这里表示任务是否执行成功。
如果任务正常完成:
success = true
如果任务执行失败:
success = false
(3)Feedback:反馈部分
第三部分是 Feedback,也就是任务执行过程中,服务端持续反馈给客户端的状态。
uint32 status
这里可以表示当前执行状态。
例如:
status = 1
status = 2
status = 3
status = 4
status = 5
表示任务正在一步步执行。
当然,在真实项目中,status 也可以设计成更具体的含义,例如:
- 0:空闲
- 1:正在执行
- 2:任务暂停
- 3:任务完成
- 4:任务失败
本篇为了方便理解,先用 status 表示执行进度。
三、修改 limo_msgs 接口包配置
3.1 修改 CMakeLists.txt
前面自定义 msg 和 srv 时,已经配置过 rosidl_generate_interfaces。
现在需要把 action 文件也加入进去。
打开:
limo_msgs/CMakeLists.txt
找到:
cpp
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/LimoStatus.msg"
"srv/LimoSrv.srv"
DEPENDENCIES std_msgs
)
修改为:
cpp
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/LimoStatus.msg"
"srv/LimoSrv.srv"
"action/LimoAction.action"
DEPENDENCIES std_msgs
)
这样编译时,ROS2 才会根据 LimoAction.action 自动生成 C++ 代码。
3.2 修改 package.xml
打开:
limo_msgs/package.xml
确认里面有下面这些依赖:
XML
<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>
如果前面自定义 msg 和 srv 已经配置过,那么这里一般不用重复修改。
3.3 编译接口包
回到工作空间根目录:
cd ~/agilex_open_class_ws
编译:
colcon build --packages-select limo_msgs
编译完成后刷新环境变量:
source install/setup.bash
查看 action 接口是否生成成功:
ros2 interface show limo_msgs/action/LimoAction
正常情况下会看到:
float32 x
float32 y
float32 z
---
bool success
---
uint32 status
说明自定义 Action 接口已经生成成功。
四、Action 服务端代码
4.1 Action 服务端的作用
Action 服务端负责:
- 接收客户端发送的目标
- 根据目标控制小车运动
- 执行过程中持续发布反馈
- 任务结束后返回最终结果
在本篇案例中,Action 服务端的核心功能是:
- 客户端发送 x、y、z 速度目标,服务端把目标转换成 Twist
- 服务端向 /cmd_vel 发布速度
- 服务端循环反馈 status
- 最后返回 success
也就是说:
Action 服务端是真正执行任务的一方。
4.2 新建服务端文件
在 limo_learning 功能包下创建:
limo_learning/src/limo_action_server.cpp
写入代码:
cpp
// 引入智能指针相关头文件
// std::shared_ptr、std::make_shared 会用到
#include <memory>
// 引入线程相关头文件
// std::thread 会用到
#include <thread>
// 引入 ROS2 C++ 客户端库
#include "rclcpp/rclcpp.hpp"
// 引入 ROS2 Action 通信相关头文件
#include "rclcpp_action/rclcpp_action.hpp"
// 引入 Twist 速度消息类型
// 用于向 /cmd_vel 发布速度指令
#include "geometry_msgs/msg/twist.hpp"
// 引入自定义 Action 接口
// 对应 limo_msgs/action/LimoAction.action
#include "limo_msgs/action/limo_action.hpp"
// 自定义 Action 服务端节点类
// 继承 rclcpp::Node,说明它是一个 ROS2 节点
class LimoActionServer : public rclcpp::Node
{
public:
// 给自定义 Action 类型起一个别名,方便后面书写
// limo_msgs::action::LimoAction 对应 LimoAction.action 生成的 C++ 类型
using LimoAction = limo_msgs::action::LimoAction;
// 给 Action 目标句柄类型起一个别名
// GoalHandle 用来表示客户端发送过来的一个目标任务
using GoalHandleLimoAction = rclcpp_action::ServerGoalHandle<LimoAction>;
// 构造函数
// Node("limo_action_server") 表示当前节点名称为 limo_action_server
LimoActionServer() : Node("limo_action_server")
{
// 创建 /cmd_vel 速度发布者
// 发布 geometry_msgs::msg::Twist 类型消息
// 队列长度为 10
cmd_pub_ = this->create_publisher<geometry_msgs::msg::Twist>(
"/cmd_vel",
10
);
// 创建 Action 服务端
// Action 类型:LimoAction
// Action 名称:/limo_action
// handle_goal:收到目标请求时调用
// handle_cancel:收到取消请求时调用
// handle_accepted:目标被接受后调用
action_server_ = rclcpp_action::create_server<LimoAction>(
this,
"/limo_action",
std::bind(
&LimoActionServer::handle_goal,
this,
std::placeholders::_1,
std::placeholders::_2
),
std::bind(
&LimoActionServer::handle_cancel,
this,
std::placeholders::_1
),
std::bind(
&LimoActionServer::handle_accepted,
this,
std::placeholders::_1
)
);
// 打印节点启动日志
RCLCPP_INFO(this->get_logger(), "limo_action_server node has started.");
}
private:
// /cmd_vel 速度发布者对象
// 用于控制小车运动
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_pub_;
// Action 服务端对象
// 用于提供 /limo_action 动作服务
rclcpp_action::Server<LimoAction>::SharedPtr action_server_;
// 处理客户端发送过来的 Goal 目标请求
// 当客户端发送目标时,会先进入这个函数
rclcpp_action::GoalResponse handle_goal(
const rclcpp_action::GoalUUID & uuid,
std::shared_ptr<const LimoAction::Goal> goal)
{
// 当前代码中没有使用 uuid
// 写 (void)uuid 是为了避免编译器提示"变量未使用"
(void)uuid;
// 打印客户端发送过来的目标参数
// goal->x 表示目标中的 x 方向速度
// goal->y 表示目标中的 y 方向速度
// goal->z 表示目标中的 z 方向角速度
RCLCPP_INFO(
this->get_logger(),
"Received goal: x=%.2f, y=%.2f, z=%.2f",
goal->x,
goal->y,
goal->z
);
// 接受并立即执行该目标
// ACCEPT_AND_EXECUTE 表示服务端同意执行客户端发来的任务
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}
// 处理客户端发送的取消请求
// 当客户端想取消正在执行的 Action 任务时,会进入这个函数
rclcpp_action::CancelResponse handle_cancel(
const std::shared_ptr<GoalHandleLimoAction> goal_handle)
{
// 当前代码中没有直接使用 goal_handle
// 写 (void)goal_handle 是为了避免编译器提示"变量未使用"
(void)goal_handle;
// 打印收到取消请求的日志
RCLCPP_INFO(this->get_logger(), "Received request to cancel goal.");
// 接受客户端的取消请求
return rclcpp_action::CancelResponse::ACCEPT;
}
// 当目标被接受后,会进入这个函数
// 这里不直接执行任务,而是开启一个新线程执行 execute()
void handle_accepted(
const std::shared_ptr<GoalHandleLimoAction> goal_handle)
{
// 创建一个新线程执行 execute 函数
// 这样做的目的:
// 避免长时间任务阻塞 ROS2 主线程
//
// detach() 表示线程独立运行
// 主线程不会等待这个线程结束
std::thread{
std::bind(
&LimoActionServer::execute,
this,
std::placeholders::_1
),
goal_handle
}.detach();
}
// 真正执行 Action 任务的函数
// 目标被接受后,会在新线程中执行这个函数
void execute(
const std::shared_ptr<GoalHandleLimoAction> goal_handle)
{
// 打印开始执行任务的日志
RCLCPP_INFO(this->get_logger(), "Executing goal...");
// 获取客户端发送过来的目标数据
const auto goal = goal_handle->get_goal();
// 创建反馈对象
// Feedback 对应 LimoAction.action 中第二个 --- 后面的反馈部分
auto feedback = std::make_shared<LimoAction::Feedback>();
// 创建结果对象
// Result 对应 LimoAction.action 中第一个 --- 后面的结果部分
auto result = std::make_shared<LimoAction::Result>();
// 设置循环频率为 1Hz
// 也就是每秒执行一次循环
rclcpp::Rate loop_rate(1);
// 创建 Twist 速度消息对象
geometry_msgs::msg::Twist cmd_vel;
// 将 Action 目标中的 x、y、z 转换成 Twist 速度指令
// goal->x 对应 linear.x,表示前后方向速度
// goal->y 对应 linear.y,表示左右方向速度
// goal->z 对应 angular.z,表示绕 z 轴旋转角速度
cmd_vel.linear.x = goal->x;
cmd_vel.linear.y = goal->y;
cmd_vel.angular.z = goal->z;
// 循环执行 5 次
// 因为 loop_rate 是 1Hz,所以这里大约执行 5 秒
for (uint32_t i = 1; i <= 5; i++)
{
// 判断当前目标是否正在被取消
// 如果客户端发送了取消请求,并且服务端接受取消,就会进入这里
if (goal_handle->is_canceling())
{
// 创建停止速度指令
// Twist 默认值都是 0,所以表示停止小车
geometry_msgs::msg::Twist stop_cmd;
// 发布停止指令到 /cmd_vel
cmd_pub_->publish(stop_cmd);
// 设置最终结果为失败
result->success = false;
// 通知客户端:目标已经取消
goal_handle->canceled(result);
// 打印取消日志
RCLCPP_INFO(this->get_logger(), "Goal canceled.");
// 结束 execute 函数
return;
}
// 发布速度指令,让小车按照目标速度运动
cmd_pub_->publish(cmd_vel);
// 设置反馈状态
// 这里用 i 表示当前执行进度
// 第 1 秒 status=1,第 2 秒 status=2,以此类推
feedback->status = i;
// 向客户端发布反馈信息
// 客户端可以在任务执行过程中不断收到 feedback
goal_handle->publish_feedback(feedback);
// 打印当前反馈状态
RCLCPP_INFO(
this->get_logger(),
"Publishing feedback: status=%d",
feedback->status
);
// 按照 1Hz 频率休眠
// 也就是每次循环间隔约 1 秒
loop_rate.sleep();
}
// 任务执行完成后,发布停止指令
// 防止小车继续运动
geometry_msgs::msg::Twist stop_cmd;
cmd_pub_->publish(stop_cmd);
// 设置最终结果为成功
result->success = true;
// 通知客户端:目标执行成功
goal_handle->succeed(result);
// 打印任务成功日志
RCLCPP_INFO(this->get_logger(), "Goal succeeded.");
}
};
// main 函数,程序入口
int main(int argc, char ** argv)
{
// 初始化 ROS2
rclcpp::init(argc, argv);
// 创建 LimoActionServer 节点对象
auto node = std::make_shared<LimoActionServer>();
// 运行节点
// spin 会让节点持续运行,等待 Action 请求、取消请求等回调触发
rclcpp::spin(node);
// 关闭 ROS2
rclcpp::shutdown();
return 0;
}
```
五、Action 客户端代码
5.1 Action 客户端的作用
Action 客户端负责:
- 等待 Action 服务端启动
- 发送目标给服务端
- 接收服务端反馈
- 接收最终执行结果
在本篇案例中,客户端发送:
x = 0.2
y = 0.0
z = 0.0
表示让小车向前运动。
服务端执行过程中,客户端会不断收到 feedback。
最后收到 result,判断任务是否执行成功。
5.2 新建客户端文件
在 limo_learning 功能包下创建:
limo_learning/src/limo_action_client.cpp
写入代码:
cpp
// 引入智能指针相关头文件
// std::shared_ptr、std::make_shared 会用到
#include <memory>
// ROS2 C++ 客户端库头文件
#include "rclcpp/rclcpp.hpp"
// ROS2 Action 通信相关头文件
#include "rclcpp_action/rclcpp_action.hpp"
// 引入自定义 Action 接口头文件
// 对应 limo_msgs/action/LimoAction.action
#include "limo_msgs/action/limo_action.hpp"
// 自定义 Action 客户端节点类
class LimoActionClient : public rclcpp::Node
{
public:
// 给自定义 Action 类型起一个别名,方便后面使用
using LimoAction = limo_msgs::action::LimoAction;
// 给 Action 客户端目标句柄类型起一个别名
// ClientGoalHandle<LimoAction> 表示客户端这边用于管理目标任务的句柄
using GoalHandleLimoAction = rclcpp_action::ClientGoalHandle<LimoAction>;
// 构造函数
// 创建节点时,节点名称为 limo_action_client
LimoActionClient() : Node("limo_action_client")
{
// 创建 Action 客户端对象
// LimoAction 表示使用的 Action 接口类型
// "/limo_action" 表示要连接的 Action 服务名称
action_client_ = rclcpp_action::create_client<LimoAction>(
this,
"/limo_action"
);
// 打印日志,提示 Action 客户端节点已经启动
RCLCPP_INFO(this->get_logger(), "limo_action_client node has started.");
}
// 发送 Action 目标任务的函数
void send_goal()
{
// 等待 Action 服务端启动
// 最多等待 5 秒
// 如果 5 秒内没有发现 Action 服务端,则直接返回
if (!action_client_->wait_for_action_server(std::chrono::seconds(5)))
{
RCLCPP_ERROR(this->get_logger(), "Action server not available.");
return;
}
// 创建一个 Action 目标消息
// LimoAction::Goal 对应 .action 文件中第一段 Goal 部分
auto goal_msg = LimoAction::Goal();
// 设置目标参数
// 这里表示希望小车按照 x、y、z 指定的速度执行运动
goal_msg.x = 0.2;
goal_msg.y = 0.0;
goal_msg.z = 0.0;
// 打印日志,提示正在发送目标
RCLCPP_INFO(this->get_logger(), "Sending goal...");
// 创建发送目标时的选项对象
// 里面可以绑定三个回调函数:
// 1. goal_response_callback:目标是否被服务端接受
// 2. feedback_callback:执行过程中接收反馈
// 3. result_callback:执行结束后接收最终结果
auto send_goal_options = rclcpp_action::Client<LimoAction>::SendGoalOptions();
// 绑定目标响应回调函数
// 当服务端接受或拒绝目标时,会自动调用 goal_response_callback
send_goal_options.goal_response_callback =
std::bind(
&LimoActionClient::goal_response_callback,
this,
std::placeholders::_1
);
// 绑定反馈回调函数
// 当服务端执行过程中发布 feedback 时,会自动调用 feedback_callback
send_goal_options.feedback_callback =
std::bind(
&LimoActionClient::feedback_callback,
this,
std::placeholders::_1,
std::placeholders::_2
);
// 绑定结果回调函数
// 当 Action 任务执行完成、取消或失败时,会自动调用 result_callback
send_goal_options.result_callback =
std::bind(
&LimoActionClient::result_callback,
this,
std::placeholders::_1
);
// 异步发送目标任务
// goal_msg 是目标内容
// send_goal_options 中包含三个回调函数
action_client_->async_send_goal(goal_msg, send_goal_options);
}
private:
// Action 客户端对象
// 用于连接 /limo_action,并向 Action 服务端发送目标任务
rclcpp_action::Client<LimoAction>::SharedPtr action_client_;
// 目标响应回调函数
// 当客户端发送目标后,服务端会决定是否接受该目标
void goal_response_callback(
const GoalHandleLimoAction::SharedPtr & goal_handle)
{
// 如果 goal_handle 为空,说明服务端拒绝了目标
if (!goal_handle)
{
RCLCPP_ERROR(this->get_logger(), "Goal was rejected by server.");
}
// 如果 goal_handle 不为空,说明服务端接受了目标
else
{
RCLCPP_INFO(this->get_logger(), "Goal accepted by server.");
}
}
// 反馈回调函数
// 服务端执行任务过程中,可以不断向客户端发送 feedback
void feedback_callback(
GoalHandleLimoAction::SharedPtr,
const std::shared_ptr<const LimoAction::Feedback> feedback)
{
// 打印服务端返回的反馈状态
// feedback->status 对应 .action 文件中 Feedback 部分的 status 字段
RCLCPP_INFO(
this->get_logger(),
"Received feedback: status=%d",
feedback->status
);
}
// 最终结果回调函数
// 当 Action 任务结束后,会进入这个函数
void result_callback(
const GoalHandleLimoAction::WrappedResult & result)
{
// 根据 result.code 判断任务最终状态
switch (result.code)
{
// 任务成功完成
case rclcpp_action::ResultCode::SUCCEEDED:
RCLCPP_INFO(
this->get_logger(),
"Goal succeeded, success=%d",
result.result->success
);
break;
// 任务执行失败,被服务端中止
case rclcpp_action::ResultCode::ABORTED:
RCLCPP_ERROR(this->get_logger(), "Goal was aborted.");
break;
// 任务被取消
case rclcpp_action::ResultCode::CANCELED:
RCLCPP_WARN(this->get_logger(), "Goal was canceled.");
break;
// 其他未知状态
default:
RCLCPP_ERROR(this->get_logger(), "Unknown result code.");
break;
}
// 收到最终结果后关闭 ROS2 节点
rclcpp::shutdown();
}
};
int main(int argc, char ** argv)
{
// 初始化 ROS2
rclcpp::init(argc, argv);
// 创建 Action 客户端节点对象
auto node = std::make_shared<LimoActionClient>();
// 调用 send_goal(),向 Action 服务端发送目标任务
node->send_goal();
// 进入 ROS2 事件循环
// 用于等待并处理 Action 回调函数
// 包括目标响应、过程反馈、最终结果
rclcpp::spin(node);
// 关闭 ROS2
rclcpp::shutdown();
return 0;
}
六、修改 limo_learning 功能包
6.1 正常功能包目录:
bash
ros2_ws/
└── src
├── limo_msgs
│ ├── msg
│ │ └── LimoStatus.msg
│ ├── srv
│ │ └── LimoSrv.srv
│ ├── action
│ │ └── LimoAction.action
│ ├── CMakeLists.txt
│ └── package.xml
│
└── limo_learning
├── src
│ ├── limo_topic_cmd.cpp
│ ├── limo_topic_sub.cpp
│ ├── limo_srv_server.cpp
│ ├── limo_srv_client.cpp
│ ├── limo_action_server.cpp
│ └── limo_action_client.cpp
├── CMakeLists.txt
└── package.xml
6.2 修改 limo_learning 的 CMakeLists.txt
(1)添加 action 依赖
因为 Action 代码中用到了:
rclcpp_action
所以需要在 CMakeLists.txt 中增加依赖。
打开:
limo_learning/CMakeLists.txt
添加:
cpp
find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(limo_msgs REQUIRED)
如果前面已经有 rclcpp、geometry_msgs、limo_msgs,就只需要补充:
find_package(rclcpp_action REQUIRED)
(2)添加可执行文件
继续添加:
cpp
add_executable(limo_action_server src/limo_action_server.cpp)
ament_target_dependencies(
limo_action_server
rclcpp
rclcpp_action
geometry_msgs
limo_msgs
)
add_executable(limo_action_client src/limo_action_client.cpp)
ament_target_dependencies(
limo_action_client
rclcpp
rclcpp_action
limo_msgs
)
(3)添加安装配置
在 install 中加入:
cpp
install(TARGETS
limo_action_server
limo_action_client
DESTINATION lib/${PROJECT_NAME}
)
如果前面已经有其他可执行文件,可以统一写成:
cpp
install(TARGETS
limo_topic_cmd
limo_topic_sub
limo_status_pub
limo_status_sub
limo_srv_server
limo_srv_client
limo_action_server
limo_action_client
DESTINATION lib/${PROJECT_NAME}
)
6.3 修改 package.xml
打开:
limo_learning/package.xml
添加依赖:
XML
<depend>rclcpp</depend>
<depend>rclcpp_action</depend>
<depend>geometry_msgs</depend>
<depend>limo_msgs</depend>
其中:
XML
<depend>rclcpp_action</depend>
是本篇 Action 通信需要新增的重点依赖。
七、编译与运行

7.1 编译功能包
回到工作空间根目录:
cd ~/agilex_open_class_ws
编译:
colcon build
如果只想编译相关功能包,也可以:
colcon build --packages-select limo_msgs limo_learning
编译完成后刷新环境变量:
source install/setup.bash
7.2 先启动 Action 服务端
打开第一个终端:
cd ~/agilex_open_class_ws
source install/setup.bash
ros2 run limo_learning limo_action_server
正常输出类似:
[INFO] [limo_action_server]: limo_action_server node has started.
说明 Action 服务端已经启动。
7.3 再启动 Action 客户端
打开第二个终端:
cd ~/agilex_open_class_ws
source install/setup.bash
ros2 run limo_learning limo_action_client
客户端输出类似:
[INFO] [limo_action_client]: limo_action_client node has started.
[INFO] [limo_action_client]: Sending goal...
[INFO] [limo_action_client]: Goal accepted by server.
[INFO] [limo_action_client]: Received feedback: status=1
[INFO] [limo_action_client]: Received feedback: status=2
[INFO] [limo_action_client]: Received feedback: status=3
[INFO] [limo_action_client]: Received feedback: status=4
[INFO] [limo_action_client]: Received feedback: status=5
[INFO] [limo_action_client]: Goal succeeded, success=1
服务端输出类似:
[INFO] [limo_action_server]: Received goal: x=0.20, y=0.00, z=0.00
[INFO] [limo_action_server]: Executing goal...
[INFO] [limo_action_server]: Publishing feedback: status=1
[INFO] [limo_action_server]: Publishing feedback: status=2
[INFO] [limo_action_server]: Publishing feedback: status=3
[INFO] [limo_action_server]: Publishing feedback: status=4
[INFO] [limo_action_server]: Publishing feedback: status=5
[INFO] [limo_action_server]: Goal succeeded.
7.4 查看 Action 列表
可以使用命令查看当前系统中的 Action:
ros2 action list
正常情况下可以看到:
/limo_action
7.5 查看 Action 信息
ros2 action info /limo_action
可以看到 Action 的服务端和客户端数量。
7.6 使用命令行发送 Action 目标
除了写客户端代码,也可以直接用终端命令测试 Action。
ros2 action send_goal /limo_action limo_msgs/action/LimoAction "{x: 0.2, y: 0.0, z: 0.0}"
如果想看到反馈信息,可以加上:
--feedback
完整命令:
ros2 action send_goal /limo_action limo_msgs/action/LimoAction "{x: 0.2, y: 0.0, z: 0.0}" --feedback
这样终端就会显示服务端不断返回的 feedback。
ros2 action send_goal命令可以直接在终端中向 Action 服务端发送目标任务,不需要额外编写客户端代码。- 其中
/limo_action表示 Action 名称,limo_msgs/action/LimoAction表示自定义 Action 接口类型,"{x: 0.2, y: 0.0, z: 0.0}"表示发送给服务端的 goal 目标内容。- 如果在命令末尾加上
--feedback,终端会实时显示服务端执行过程中返回的 feedback 信息,方便调试 Action 通信流程。
八、Topic、Service、Action 对比总结
8.1 Topic 通信
Topic 适合持续发送数据。
例如:
- 速度控制
- 状态发布
- 雷达数据
- IMU 数据
- 里程计数据
特点是:
- 发布者不断发布
- 订阅者不断接收
- 没有请求和响应
- 没有最终结果返回
可以理解为:
Topic 是广播型通信。
8.2 Service 通信
Service 适合一次请求、一次响应。
例如:
- 打开某个功能
- 切换某种模式
- 发送一次控制请求
- 查询一次状态
特点是:
- 客户端发送请求
- 服务端处理请求
- 服务端返回结果
- 过程一般比较短
可以理解为:
Service 是问答型通信。
8.3 Action 通信
Action 适合长时间任务。
例如:
- 导航到目标点
- 执行巡检任务
- 执行机械臂动作
- 小车持续运动一段时间
- 需要反馈执行进度的任务
特点是:
- 客户端发送目标
- 服务端执行任务
- 执行过程中持续反馈
- 结束后返回最终结果
- 任务中途还可以取消
可以理解为:
Action 是任务型通信。
8.4 三者对比表
| 通信方式 | 核心特点 | 是否持续通信 | 是否有返回结果 | 是否有过程反馈 | 适合场景 |
|---|---|---|---|---|---|
| Topic | 发布/订阅 | 是 | 否 | 否 | 速度、状态、传感器数据 |
| Service | 请求/响应 | 否 | 是 | 否 | 短时间一次性任务 |
| Action | 目标/反馈/结果 | 是 | 是 | 是 | 长时间任务、导航、巡检 |
九、本篇总结

本篇文章在前面 Topic、msg、srv 的基础上,继续讲解了 ROS2 中非常重要的 Action 通信机制。
前面我们已经知道:
- Topic 可以持续发布速度和状态
- 自定义 msg 可以封装自己的数据结构
- Service 可以实现一次请求和一次响应
但是当任务执行时间比较长,并且需要在执行过程中持续反馈状态时,Service 就不太适合了。
这时候就需要 Action。
本篇通过自定义:
LimoAction.action
实现了一个小车 Action 控制案例。
其中:
- Goal 负责接收客户端发送的目标速度
- Feedback 负责在执行过程中反馈当前状态
- Result 负责在任务结束后返回最终结果
- 服务端收到目标后,将 x、y、z 转换成 Twist 速度指令,并发布到 /cmd_vel 控制小车运动。
- 客户端发送目标后,可以持续接收服务端反馈,并在任务结束后获取 success 结果。
同时,本篇还完成了 limo_msgs 接口功能包和 limo_learning 代码功能包的配置。
其中:
- limo_msgs 负责定义自定义接口,包括 msg、srv、action。
- limo_learning 负责存放 C++ 节点代码,包括 Topic、Service、Action 的服务端和客户端。
至此,ROS2 中最核心的三种通信方式已经串起来了:
- Topic:持续发布和订阅数据
- Service:一次请求和一次响应
- Action:长时间任务、过程反馈、最终结果
下一篇文章会专门进入 Action 服务端和客户端代码解析,重点讲清楚:
- create_server 是如何创建 Action 服务端的。
- create_client 是如何创建 Action 客户端的。
- GoalHandle 到底是什么。
- handle_goal、handle_cancel、handle_accepted 分别什么时候执行。
- execute 函数为什么要单独开线程。
- publish_feedback、succeed、canceled、abort 分别有什么作用。
- 客户端的 goal_response_callback、feedback_callback、result_callback 分别负责什么。
本篇先把 Action 通信完整跑通。
下一篇再把 Action 服务端和客户端代码彻底讲明白。
掌握这三种通信方式后,再去看 ROS2 机器人项目中的导航、巡检、底盘控制、机械臂控制等代码,就会清晰很多。