ROS动作通信详解:从Service阻塞到Action异步任务控制

发布时间:2026/10/14 16:07:24
ROS动作通信详解:从Service阻塞到Action异步任务控制 简介面向ROS初学者与机器人开发者这份动作服务器与客户端学习源码包以C语言展示了动作通信机制的完整实现帮助解决机器人任务中长耗时操作的发起、执行与监控问题。压缩包共5个文件涵盖2个C源文件服务端与客户端主程序、1个action消息定义、1个txt说明文档以及CMakeLists.txt与package.xml构建配置整体仅8KB精简结构便于快速阅读与复用。源码中使用actionlib库的SimpleActionServer/SimpleActionClient清晰演示了executeCB回调、发送Goal、等待Result等关键步骤并配合ros_tutorials_action目录下的动作定义与include头文件体现一个可编译的catkin工程组织方式。已有227人浏览学习适合对照博客笔记进行实操验证也可作为课堂实验模板在此基础上扩展tf、geometry_msgs等模块完成更复杂的机器人行为控制。总体而言这份资源把理论讲解与可运行代码浓缩在一个小包里对快速理解ROS动作机制很有帮助。1. ROS 动作通信到底解决了什么从“服务只能返回一次”到“进度可见、任务可取消”写 ROS 两三年的人大多卡过这个问题想给机械臂发一个耗时数秒的运动指令写 service 吧服务端在算客户端只能干等中途想取消还没办法换 topic 吧又失去了“请求—响应”的语义根本不知道任务到底结束没有。ROS action 就是为这个场景设计的通信原语。动作通信把一次任务拆成目标、反馈、结果三段客户端能实时看到进度新目标可以抢占旧目标取消请求也能被服务端响应。这份源码笔记拆的正是基于 C 的 action 服务端与客户端完整实现从消息定义到 CMake 配置再到编译运行适合所有被 service 同步阻塞坑过、想上手异步任务控制的 ROS 开发者。2. 三段式消息与状态机动作通信的底层逻辑2.1 .action 文件的语法和代码生成源码包里最容易忽略的是action目录下的.action文件。这套笔记里的动作定义是MoveToGoal.action内容不长但三段式的结构很典型# 目标段Goal geometry_msgs/Pose target_pose --- # 结果段Result bool reached --- # 反馈段Feedback float32 distance_remaining三段用---分隔这就是 action 消息的全部语法。第一段定义客户端要提交的目标比如机械臂末端的目标位姿第二段定义任务结束后服务端返回的最终结果第三段是服务端执行过程中随时吐出来的进度信息。需要记住的是Feedback 可以发布任意多次Result 只能发一次且以最后一次为准。把这文件放进action/目录后CMake 里的配置也有一套固定写法find_package(catkin REQUIRED COMPONENTS roscpp actionlib actionlib_msgs message_generation ) add_action_files( DIRECTORY action FILES MoveToGoal.action ) generate_messages( DEPENDENCIES actionlib_msgs std_msgs geometry_msgs ) catkin_package( CATKIN_DEPENDS roscpp actionlib actionlib_msgs message_runtime )这里有几个容易翻车的位置。add_action_files必须写在generate_messages之前generate_messages的DEPENDENCIES漏掉actionlib_msgs生成的头文件会缺 GoalStatus 相关定义。编译完成后到devel/include/learning_actions/下看一眼能看到MoveToGoalAction.h、MoveToGoalActionGoal.h、MoveToGoalActionResult.h一整组头文件C 代码里包含最外层的MoveToGoalAction.h就够用了。我一般会写一句#include learning_actions/MoveToGoalAction.h剩下的交给编译系统处理。2.2 服务器与客户端的状态机从 PENDING 到 DONE 之间的那些状态动作通信两边各维护一套状态机。客户端发目标后这个目标先进入 PENDING服务器接受后变 ACTIVE执行完毕根据结局变成 SUCCEEDED、PREEMPTED、ABORTED 或 REJECTED。这些状态码和actionlib_msgs/GoalStatus里的定义一一对应调试时直接看数字就能判断当前卡在哪一步。状态名称状态码含义PENDING0目标已发送等待服务器接收ACTIVE1服务器已接收正在执行PREEMPTED2目标被新目标抢占或主动取消SUCCEEDED3执行成功并返回结果ABORTED4执行失败无有效结果REJECTED5目标被服务器直接拒绝RECALLING6客户端请求撤销还没发出去的目标LOST8目标丢失通常因为连接异常这组数字在代码里不会直接出现但后面用rostopic echo /xxx/status查状态时全都会看到。理解状态机最有用的一个场景是排查“客户端为什么没拿到结果”如果状态停在了 ACTIVE 不变说明服务器的执行回调内部卡住了如果变成了 PREEMPTED说明中间有人调用了cancelGoal()或在执行回调里返回了抢占状态。动作通信在底层其实展开成五个话题goal、feedback、result、status、cancel。service 之所以做不到中途取消和进度反馈就是因为它只有一对请求—响应话题。action 把一次任务的三个关键事件拆到不同话题上客户端订阅result时并不会阻塞等待这是异步通信能成立的根本原因。3. 手写动作服务器SimpleActionServer 与 execute 回调的执行边界3.1 服务器构造函数与回调绑定start() 的顺序不是玄学源码包里服务器端用到了actionlib::SimpleActionServer初始化代码不长但要讲顺序#include ros/ros.h #include actionlib/server/simple_action_server.h #include learning_actions/MoveToGoalAction.h class MoveActionServer { protected: ros::NodeHandle nh_; actionlib::SimpleActionServerlearning_actions::MoveToGoalAction as_; std::string action_name_; public: MoveActionServer(std::string name) : as_(nh_, name, boost::bind(MoveActionServer::executeCB, this, _1), false), action_name_(name) { as_.start(); ROS_INFO(action server [%s] started, action_name_.c_str()); } void executeCB(const learning_actions::MoveToGoalGoalConstPtr goal); };构造函数的第三个参数是执行回调用boost::bind把成员函数和this绑在一起第四个参数false表示关闭自动启动。为什么要把 auto_start 设为 false因为如果传 true回调线程会在对象成员还没全部构造完成时就开始跑一旦 executeCB 里访问了尚未初始化的成员就会出现很难复现的段错误。写服务端时我习惯先构造好所有成员、再在最末尾调用as_.start()。3.2 executeCallback 主循环反馈频率、抢占检查与退出条件executeCB 是动作服务器真正干活的函数下面的代码模拟了一个带距离反馈的移动任务void MoveActionServer::executeCB( const learning_actions::MoveToGoalGoalConstPtr goal) { double distance goal-target_pose.position.x; double step 0.05; ros::Rate rate(10); // 10Hz每 100ms 循环一次 learning_actions::MoveToGoalFeedback feedback; learning_actions::MoveToGoalResult result; ROS_INFO(receive goal, target_x %.2f, distance); while (distance 0.01) { // 抢占检查客户端取消或新目标到达必须在这个位置感知 if (as_.isPreemptRequested() || !ros::ok()) { ROS_INFO(goal preempted, distance %.2f, distance); as_.setPreempted(); return; } distance - step; feedback.distance_remaining distance 0.0 ? distance : 0.0; as_.publishFeedback(feedback); rate.sleep(); } result.reached true; as_.setSucceeded(result); }这段代码里有几个值得注意的设计点。as_.isPreemptRequested()必须在每个循环开头检查一次这是服务器响应取消请求的唯一机会。publishFeedback是向 feedback 话题发布消息客户端只要处于 ACTIVE 状态就会持续收到。rate.sleep()控制循环频率10Hz 意味着客户端每秒能看到 10 条进度更新。反馈发布的频率要结合业务场景选择不是越快越好。对机械臂运动这类任务10Hz 足够表达进度对高速视觉跟踪这类场景可以把频率提到 50Hz但每一帧都要做一次序列化和话题传输CPU 占用会明显上升。3.3 三种结局的写法setSucceeded、setAborted、setPreemptedexecuteCB 正常走到循环结束用setSucceeded(result)发布最终结果客户端会收到 SUCCEEDED 状态和 result 里的reached true如果任务中途发现路径规划失败、传感器数据异常这类问题应该调用setAborted(result)如果客户端请求取消调用as_.setPreempted()。三兄弟各自对应不同的状态码和 result 发布行为。一个容易搞混的点是setSucceeded和setAborted都能带 result但setPreempted也可以带 result用来传递“被取消时执行到哪一步”的现场信息。这套源码还在服务器里加了超时保护如果任务超过预设时间在 executeCB 外层包一个定时器超时后强制setAborted。这是一个很实用的习惯因为某个被调度的子进程或底层驱动一旦卡死executeCB 的 while 循环可能永远不会退出外层的定时器是最后的救命稻草。4. 手写动作客户端两种结果等待方式与超时处理4.1 waitForServer启动竞态和超时参数客户端的核心代码集中在怎么和服务器通信上初始化部分如下#include ros/ros.h #include actionlib/client/simple_action_client.h #include learning_actions/MoveToGoalAction.h int main(int argc, char** argv) { ros::init(argc, argv, move_action_client); // 第二个参数 true让客户端自己拉一个线程处理回调 actionlib::SimpleActionClientlearning_actions::MoveToGoalAction ac(move_to_goal, true); ROS_INFO(waiting for action server...); // 等待服务器启动最多等 10 秒 if (!ac.waitForServer(ros::Duration(10.0))) { ROS_ERROR(action server not found within 10s); return -1; } ROS_INFO(action server connected);SimpleActionClient构造函数的第一个参数是 action 名称它和服务器端必须完全一致大小写、命名空间都算。第二个参数true表示开启内部 spin 线程客户端不需要在代码里写额外的ros::spin()就能持续处理服务器发来的状态反馈。waitForServer(ros::Duration(10.0))是带超时的等待返回 false 说明 10 秒内没发现服务器。新手最容易在这里踩坑服务器节点没启动客户端就挂在这个函数上一直等所以正式代码里必须传超时参数。4.2 waitForResult 轮询模式状态、结果与取消服务器连接成功后构造目标并发送learning_actions::MoveToGoalGoal goal; goal.target_pose.position.x 2.5; goal.target_pose.position.y 0.0; ROS_INFO(send goal, x %.2f, goal.target_pose.position.x); ac.sendGoal(goal); // 等待结果返回最多等 30 秒 bool finished ac.waitForResult(ros::Duration(30.0)); if (finished) { actionlib::SimpleClientGoalState state ac.getState(); const learning_actions::MoveToGoalResultConstPtr result ac.getResult(); ROS_INFO(state: %s, reached: %d, state.toString().c_str(), result-reached); } else { ROS_WARN(timeout, cancel goal); ac.cancelGoal(); // 取消之后等一小段时间让状态变成 PREEMPTED ac.waitForResult(ros::Duration(3.0)); } return 0; }waitForResult会阻塞当前线程直到结果话题收到消息、或超过设定的ros::Duration。getState()返回SimpleClientGoalState用toString()可以打出 SUCCEEDED、PREEMPTED 这类可读状态。getResult()拿到最终结果如果任务是 ABORTED 或 PREEMPTEDgetResult()可能返回空指针取值前要先判空。超时分支里的cancelGoal()值得说两句。调用它之后服务器要等到下一次isPreemptRequested()检查才会响应响应完成后状态变成 PREEMPTED客户端才会结束等待。所以取消后要再waitForResult(ros::Duration(3.0))给状态转换留出时间窗口否则立刻退出状态机可能还停留在 RECALLING。4.3 回调驱动模式sendGoal 带三个回调的生产级写法轮询模式适合简单 demo但生产代码里更好用的是回调驱动。sendGoal的重载版本可以传三个回调函数void doneCb(actionlib::SimpleClientGoalState state, const learning_actions::MoveToGoalResultConstPtr result) { ROS_INFO(done, state%s, state.toString().c_str()); if (result) { ROS_INFO(reached%d, result-reached); } } void activeCb() { ROS_INFO(goal just went active); } void feedbackCb( const learning_actions::MoveToGoalFeedbackConstPtr feedback) { ROS_INFO(distance%.2f, feedback-distance_remaining); } ac.sendGoal(goal, boost::bind(doneCb, this, _1, _2), boost::bind(activeCb, this), boost::bind(feedbackCb, this, _1));三个回调分别在目标完成、目标被服务器接收、收到反馈时触发。doneCb里传的是SimpleClientGoalState和结果指针反馈回调每收到一条 feedback 消息就会执行一次。这种模式的好处是客户端主线程完全不被阻塞可以继续处理别的逻辑适合把动作通信嵌进状态机或 UI 框架里。这里要提一个实际生产中常见的补充如果客户端同时处理多个任务最好在 doneCb 里判断goal_id因为服务器可能由于抢占等原因返回多个结果只有匹配当前目标的那个才应该被采纳。5. 动作实战避坑四个能卡住新手一整天的典型问题5.1 客户端一直等待服务器先检查 start() 与 action 名称现象客户端卡在waitForServer超时终端不停打印 “action server not found”。原因最常见的是服务器端没调用as_.start()。构造函数里把 auto_start 设为false后如果忘记手动启动服务器对象虽然创建了但五个底层话题根本没有建立。其次是 action 名称不一致服务器端叫move_to_goal客户端写成moveto_goal或者带了某个命名空间前缀两边永远配对不上。解决先执行rostopic list | grep move_to_goal如果输出为空说明服务器节点没把话题首跳出来去查服务器代码的as_.start()是否被执行如果话题存在但客户端仍然超时把两边的 action 名统一成全局完整名称。5.2 反馈风暴10Hz 足够不要每毫秒 publishFeedback现象rostopic hz /move_to_goal/feedback显示上千赫兹CPU 占用飙升其他节点响应变慢。原因executeCB 里的 while 循环没有rate.sleep()或者循环很快但每轮都无条件publishFeedback。feedback 是 topic 消息发布得越频繁序列化和传输开销越大积压的 feedback 还会撑大订阅端队列。解决给循环加ros::Rate或者在循环里做节流int loop_count 0; while (distance 0.01) { loop_count; if (as_.isPreemptRequested() || !ros::ok()) { as_.setPreempted(); return; } if (loop_count % 5 0) { feedback.distance_remaining distance; as_.publishFeedback(feedback); } rate.sleep(); }loop_count % 5 0意味着每五轮发布一次配合 10Hz 的循环频率就是 2Hz 的反馈更新对大多数控制类任务已经够用。反馈是给客户端“看进度”用的不是控制系统的主反馈通道别把它当成传感器数据一样狂发。5.3 取消不生效执行回调里不检查 isPreemptRequested 就是白搭现象客户端调用cancelGoal()服务器端没有任何反应任务照样执行到最后最终返回 SUCCEEDED。原因executeCB 里是一个长时间运行的 while 循环中间只有rate.sleep()没有调用as_.isPreemptRequested()。SimpleActionServer 无法打断正在执行的 executeCB取消请求到达后必须等当前回调运行结束才会被处理。如果循环要跑 30 秒取消请求就等 30 秒。解决在每个循环周期开头检查抢占状态发现抢占请求后立刻setPreempted()并 return。如果任务里包含多个阶段每个阶段完成、进入下一个阶段前都要检查一次。更细粒度的做法是把任务切成多个子步骤每个子步骤单独检查一次这样取消的响应延迟可以控制在单个步骤的执行时长以内。5.4 头文件生成失败add_action_files 与 generate_messages 的顺序坑现象编译时报fatal error: learning_actions/MoveToGoalAction.h: No such file or directory。原因CMakeLists.txt 里add_action_files写在generate_messages之后或者generate_messages的DEPENDENCIES里漏了actionlib_msgs。另一种可能是一次catkin_make的产物还没生成完整直接再编客户端的代码也找不到头文件。解决把 CMake 配置按前面第 2 章的顺序重写一遍然后在工作空间执行完整的catkin_make不要只编译单个目标。编译结束后去devel/include/learning_actions/目录下ls确认头文件确实生成了再回头编译客户端。这里需要注意的细节是改了.action文件后catkin_make有时不会自动重新生成消息头文件习惯性地删掉devel/和build/里的对应缓存后再编译能省掉很多莫名的半边红。6. 调试动作通信最快的姿势三个终端盯着 status 看状态跳变动作通信调试我最常用的方法不是看日志而是开三个终端直接观察底层话题。先在服务器和客户端都跑起来之前用一个终端执行rostopic list | grep move_to_goal能看到五个标准话题/move_to_goal/cancel /move_to_goal/feedback /move_to_goal/goal /move_to_goal/result /move_to_goal/status五个话题全部出现说明服务器端启动正常。然后一个终端执行rostopic echo /move_to_goal/status一个终端执行rostopic echo /move_to_goal/feedback。status 话题输出的status字段就是第 2 章表格里的状态码发送目标后应该看到它从 1ACTIVE跳到 3SUCCEEDED中间夹杂着 0PENDING的短暂停留。如果状态卡在 1 不动说明执行回调内部阻塞优先检查服务器端的 while 循环和rate.sleep()。feedback 话题同步验证进度更新的频率用rostopic hz /move_to_goal/feedback可以直接测出反馈发布频率如果显示的数字和代码里ros::Rate设置的不一致多半是循环里的其他操作耗时太长或者有多个节点同时向同名 feedback 话题发消息。多机或同节点多机器人场景下我会把 action 名称加上命名空间前缀比如/robot1/move_to_goal、/robot2/move_to_goal五个话题会自动带上对应前缀。这样rostopic list | grep robot1就能一眼看出哪台设备的状态不用从一大堆同名话题里靠时间戳去猜数据属于谁。从那以后我每次调动作通信都会强制自己先开这三个终端把 status、feedback、result 挂出来再干活看到状态码和预想不一致立即回头查代码而不是盲目加日志重编。希望这篇文章能帮你把这套动作服务器与客户端的源码跑通也能少走几个我当年绕过的弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询