简介面向ROS初学者的源码包《学习笔记七源码-动作服务器与客户端.zip》聚焦动作服务器与动作客户端的C实现通过完整示例帮助理解actionlib库的通信机制与开发流程。包内共五个文件包含两个C源文件、一个动作消息定义文件、一个编译配置文件和一个说明文档整体仅八KB轻量易读适合快速上手该资源已有两百二十七人学习经实践验证。包内源码可直接对照学习动作目标、结果与反馈三要素掌握简单动作服务器与简单动作客户端的创建、回调执行、结果等待等关键操作对于正在学习机器人操作系统编程、希望从理论过渡到实际代码的开发者它是一份简洁实用的入门素材也为后续构建复杂任务控制系统打下基础。1. 动作服务器与客户端源码包到底在解决什么问题某个服务接口刚发出去机器人底盘已经动了但服务端还在等一个持续十秒的执行结果。用同步请求去扛这种活调用方会卡到怀疑人生中途想取消也没地方下手。这就是动作通信Action存在的理由它专治耗时、可反馈、可取消的机器人任务。手里这份学习笔记7源码-动作服务器与客户端.zip如果你翻过目录会发现里面是一个完整的 ROS 动作编程示例编译之后能跑通一个带反馈状态机的任务链路。它解决的核心问题就是把「发一个目标出去然后等一个最终结果」的笨办法升级成「目标、反馈、结果、取消」四件事都能独立处理的异步模型。这份笔记适合谁刚学完 ROS 基础话题与服务的同学想把手头项目从一问一答升级成任务协作的开发者以及被导航、机械臂这类长流程任务折磨到想换技术路线的人。作者把第七章的源码单独打包意图很明显动作服务器和客户端是 ROS 里最容易被跳过、但实际工程又绕不开的一环。下面按拆分目录、编译运行、源码逐段、参数调整和踩坑记录的顺序把它一次讲透。2. 从 service 到 action为什么重活要交给动作通信2.1 service 和 action 的本质差异话题Topic是持续广播服务Service是一次性问答。前者不给结果后者给结果但整个过程像一次 HTTP 请求发出去就等响应。做动作编程时你面对的真实需求往往是告诉机器人一个目标位置这一走就是十几秒期间要不断上报进度还要允许人随时喊停。Service 的结构天然不适合这种场景——服务端处理中客户端只能干等中途没有反馈通道取消只能靠超时来兜底。动作通信在思路上借鉴了任务队列客户端发一个 Goal目标服务器端执行过程中不断发 Feedback反馈完成时给一个 Result结果任意时刻有一方请求取消双方都能感知。底层实现上actionlib 用一个 Action 消息类型运行时自动拆成五个话题goal、cancel、status 各一进一出feedback 与 result 单独一路调度关系由动作服务器统一管理。写代码时你能明显感觉到差异。Service 的响应数据模型是死的请求和响应各一段Action 则有「目标段 / 结果段 / 反馈段」三段结构。这个三段结构直接决定了服务器里的回调函数形态接收目标一个回调、发布反馈另外处理、丢弃目标又是一种回调。很多第一次接触动作编程的人会把 Action 当作大号 Service 来写服务器端收到 goal 就闷头执行完全不发 feedback——结果客户端那边 status 卡在「执行中」界面看起来像死机这是我对这份源码最想先泼冷水的地方。2.2 读懂一份动作源码包的三个文件任何一份动作服务器与客户端的源码包无论笔记顺序怎么排最终都会围绕着三个文件转Action 定义文件、服务器端实现、客户端实现。动作定义文件后缀是 .action它决定了编译后生成的消息类型全家桶。一行目标段、一行结果段、一行反馈段看似简单其实类型设计在这里就得想清楚因为后续所有回调签名都由它决定。服务器端实现负责的是执行循环。核心是一个回调函数被动作服务器在收到目标时触发你在里面做真正的任务逻辑循环、判断、发反馈、最后给结果。客户端实现则在另一边处理状态切换发目标、等反馈、处理取消、收结果。源码包里常见的做法是三个文件平铺在 src 目录下然后 CMakeLists.txt 里加一行 add_action_filespackage.xml 里补上 actionlib_msgs 依赖。这中间有一个人最容易踩的观念坑Action 定义里的反馈字段不要写太多。反馈是高频通道频繁发一个塞满点云数据的反馈会让整条链路拥塞。这份源码里的反馈字段通常只放一个进度数字或一段简短状态文本工程上这是对的。消息设计的朴素程度决定了你后边调参时是顺风顺水还是到处救火。2.3 action 消息的五个话题是怎么跑起来的编译一个 .action 文件的直观效果是消息系统里多了几个类型运行时则表现为五个话题。以名为 do_dishes 的动作服务器为例发布之后 rostopic list 会看到 do_dishes/goal、do_dishes/cancel、do_dishes/status、do_dishes/feedback、do_dishes/result 五个条目。goal 和 cancel 由客户端发出、服务器接收status 由服务器发出、客户端接收feedback 与 result 同理。这套话题分工带来一个重要推论动作服务器与客户端并没有建立什么专用的长连接它仍然是基于话题通信的服务器不发布话题时客户端发送目标也会得到「无服务器应答」的表现。这也就是为什么写客户端代码时几乎所有教程都会先调用 wait_for_server 而不是直接 send_goal——这一步的本质是确认 status 话题背后有一个活着的执行者。源码里如果注释了链路图通常也是按这五个话题画的。理解这套话题分工后边排查问题的思路就清晰了反馈没收到先看 feedback 话题有没有消息流取消没生效就去翻 cancel 话题而不是一头扎进代码里空想。3. 把源码包跑起来从编译到第一次看到 feedback3.1 放进工作空间并处理依赖拿到这份 zip 源码第一步是解压后放到 catkin 工作空间的 src 目录下。注意包名不要带着源码包文件名里的序号比如 learning_notes7 这种带数字的名字在 catkin 里合法但容易让人分不清包名和文件夹名。我一般会找一下 package.xml 里的 标签把它作为真正的包名。cd ~/catkin_ws/src unzip ../学习笔记7源码-动作服务器与客户端.zip -d action_notes # 解压后先看包名再决定是否重命名目录 cat action_notes/package.xml | grep name # 检查依赖是否齐全缺少 actionlib 时会编译报错 rospack depends action_notes命令里的动作拆开看unzip 解压到指定目录避免把文件散在工作空间根目录grep 是确认包名rospack depends 则是编译前最值得花十秒做的一次体检。如果输出里缺了 actionlib、actionlib_msgs就需要在 package.xml 里补上对应行并在 CMakeLists.txt 的 find_package 里加上这两个依赖。补依赖后建议直接编译一次而不是急着打开源码看逻辑。编译能过至少证明动作消息类型生成成功这是后边能跑通的前提。cd ~/catkin_ws catkin_make source devel/setup.bash # 验证动作消息类型已经生成 rosmsg show action_notes/DoDishesActionGoalrosmsg show 这一步相当关键。如果消息类型能完整展开出 goal、result、feedback 三段字段说明 .action 文件被正确解析如果卡在这一步报找不到消息说明 CMakeLists 里 add_action_files 或 generate_messages 的依赖顺序有问题。看到完整字段定义再往下走后面的源码阅读会顺畅很多。3.2 自己写一个最小 action 定义文件源码包里通常会带一个现成的 .action 文件但想真正读懂我建议照着写一个最小的把三段结构刻进脑子里。下面这个 Countdown.action 是一个倒计时任务输入目标秒数执行过程上报剩余秒数结束时统一返回一个标志。# 目标要倒计时的秒数 int32 target --- # 结果完成状态0 表示正常结束 int32 finished --- # 反馈当前剩余秒数 int32 remaining三段之间用---分隔顺序固定目标、结果、反馈。字段名和类型定下来后编译会生成一组消息类型其中 CountdownAction 是总入口CountdownActionGoal、CountdownActionResult、CountdownActionFeedback 分别对应三段。自定义字段名时要注意不要用 feedback、result 这类保留词做字段名编译不会报错但生成代码后回调里容易跟框架自带字段混淆。文件写好后放到包里的 action 目录并在 CMakeLists.txt 里添加构建配置add_action_files( FILES Countdown.action ) generate_messages( DEPENDENCIES actionlib_msgs std_msgs )这里 generate_messages 的 DEPENDENCIES 会直接关系到编译成败。如果漏掉 actionlib_msgs编译器会报找不到 GoalID 这类类型。生成的是消息类型不是可执行代码所以 add_action_files 必须在 add_executable 之前声明。常见的翻车是把这两段写反了编译时一脸懵。3.3 先写服务器端执行循环与反馈节奏动作服务器端的核心是一个回调函数它接收目标、干活、发反馈、给结果。以 Countdown 为例服务器端的完整写法如下#!/usr/bin/env python import rospy import actionlib from action_notes.msg import CountdownAction, CountdownFeedback, CountdownResult class CountdownServer: def __init__(self): self.server actionlib.SimpleActionServer( countdown, CountdownAction, execute_cbself.execute_cb, auto_startFalse ) self.server.start() def execute_cb(self, goal): rate rospy.Rate(5) feedback CountdownFeedback() result CountdownResult() remaining goal.target while remaining 0: # 每条循环里检查是否有取消请求这是可取消任务的关键 if self.server.is_preempt_requested(): self.server.set_preempted() rospy.loginfo(任务被取消) return feedback.remaining remaining self.server.publish_feedback(feedback) remaining - 1 rate.sleep() result.finished 0 self.server.set_succeeded(result) if __name__ __main__: rospy.init_node(countdown_server) server CountdownServer() rospy.spin()逻辑说明execute_cb 是执行回调每收到一个目标就触发一次循环内先用 is_preempt_requested 判断客户端有没有发取消指令有就调用 set_preempted 标记任务被抢占。反馈通过 publish_feedback 发出结果用 set_succeeded 收尾。auto_startFalse 是为了先注册回调再启动服务器避免启动瞬间目标丢失。参数说明rate 是反馈频率的节奏器设为 5 表示每秒最多发 5 条反馈。注意循环里的 rate.sleep() 会让发布频率稳定在一个量级如果去掉它反馈会被系统以最快速度空转发送占用带宽还是小事严重的会把下游客户端反馈回调压垮。goal.target 是读取目标字段的标准姿势CountdownFeedback / CountdownResult 则分别是反馈与结果的实例。服务器端写完后可以用 rostopic echo 验证反馈话题有数据在流动rosrun action_notes countdown_server.py # 另开终端 rostopic echo /countdown/feedback rostopic echo /countdown/status4. 客户端怎么接住这些状态回调设计与会话参数4.1 客户端的最小连接与状态切换客户端要做的事比服务器端更琐碎连接服务器、发目标、收反馈、收结果还要应对服务器没起、任务被抢、连接超时这些异常。用 SimpleActionClient 写一份最小客户端是理解动作编程另一半关键逻辑的最好起点。#!/usr/bin/env python import rospy import actionlib from action_notes.msg import CountdownAction, CountdownGoal rospy.init_node(countdown_client) client actionlib.SimpleActionClient(countdown, CountdownAction) rospy.loginfo(等待动作服务器...) client.wait_for_server() goal CountdownGoal() goal.target 10 client.send_goal(goal) client.wait_for_result() result client.get_result() rospy.loginfo(最终结果: %d, result.finished)逻辑说明wait_for_server 会阻塞直到服务器上线这个动作必须在 send_goal 之前。它检查的本质是 status 话题有没有发布者所以服务器没启动时客户端会一直等这也是新手踩坑最多的地方。send_goal 发送目标后立刻返回wait_for_result 则进入阻塞等待直到服务器给出最终状态。参数说明wait_for_server 可以传超时参数比如 wait_for_server(rospy.Duration(5)) 表示最多等 5 秒超时返回 False这样客户端不会无限卡死。get_result 返回的是 Result 对象直接读字段即可。还有一个细节send_goal 之后不调用 wait_for_result程序也会继续往下走所以想做异步流程可以跳过等待自己用状态机管理后续逻辑。4.2 关键参数超时、反馈频率、goal 生命周期动作通信的调参重点不在 PID而在三个地方连接超时、反馈频率、目标生命周期。这三者决定了你写出来的程序在真实运行环境下是稳定干活还是时不时抽风。参数典型默认值调整方向wait_for_server 超时无限等待改成 3-5 秒避免节点挂死反馈发布频率不设置按任务耗时设 1-10 Hzgoal 生命周期一直有效按业务设置取消策略cancel 响应延迟由执行循环决定循环里越早检查越好反馈频率这栏值得多说一句。很多教程不写 publish_feedback 的频率控制导致反馈像洪水一样灌出去客户端那边一秒钟收到几百条回调界面 UI 直接被刷爆。我在实际工程里的做法是耗时超过 10 秒的任务反馈频率控制在 1Hz短任务可以到 5Hz但不要让每条反馈都携带冗余数据。目标生命周期涉及 preempt 与 cancel 的配合。客户端 send_goal 后如果又发了新目标服务器端会自动对旧目标发起抢占取消。这种机制在导航任务里很有用但在某些设备控制场景里会造成误触发比如一个机械臂正在执行抛光动作突然来了个新目标旧动作被抢占后设备会停在半路。处理方式是在服务器端执行循环里对抢占请求做二次确认不是所有抢占都直接响应。4.3 真实任务里我会改哪几个默认值源码包里的示例通常是跑通就完但落进真实设备时有几个默认值我一定会动。第一个是 wait_for_server 的无限阻塞改成带超时的版本并且超时后给出明确错误日志而不是让整个节点像睡着了一样。第二个是反馈字段的内容示例里往往只放一个进度数值真实任务里我会额外加一个状态枚举让下游能区分「执行中」「暂停中」「收尾中」不同阶段。第三个必改项是 set_succeeded 的结果字段。示例里通常返回一个简单的 success 标志但真实任务里我还要回传执行耗时、最终误差、异常码这样客户端拿到结果后能直接决定下一步动作。修改方式是在 .action 定义的结果段里加字段然后服务器端 set_succeeded 时赋值客户端 get_result 后按字段分支处理。第四个是取消动作的粒度。示例代码里 is_preempt_requested 通常放在主循环顶部一旦任务里有耗时子过程就会响应不及时。我会把检查点插到关键子过程的边界处既保证能及时响应取消又不会让任务做到一半被不明不白掐断。5. 动作编程避坑五个高频翻车现场与排查方法5.1 编译报错找不到动作消息类型现象catkin_make 时报找不到.../CountdownAction头文件或 Python 模块rosmsg show 输出为空。原因绝大多数是 add_action_files 没有被声明或者 generate_messages 的依赖列表里漏了 actionlib_msgs。另一个隐蔽原因是 .action 文件放错了目录放在了 src 下而不是 action 目录下。解决把 .action 文件移到包根目录下的 action/ 文件夹里确认 CMakeLists.txt 中先写 add_action_files 再写 generate_messages。依赖顺序上actionlib_msgs 必须出现在 generate_messages 的 DEPENDENCIES 里并且 find_package 里也要有它。改完后重新 catkin_make再用 rosmsg show 验证。5.2 feedback 频率和预期差一个量级现象服务器端明明写了 rate rospy.Rate(10)客户端收到的反馈一秒钟只有一两条甚至时断时续。原因通常是 publish_feedback 与 rate.sleep 之间的代码耗时不稳定比如里面有日志输出、文件写入这类阻塞操作把循环周期拉长。还有一种情况是大反馈消息占了太多带宽调度被降级。解决把耗时操作移出反馈循环日志输出频率也降下来。若反馈消息里包含数组或字符串压缩字段内容。想验证真实频率用rostopic hz /countdown/feedback看实际发布频率而不是靠感觉。5.3 send_goal 之后 wait_for_result 卡到天荒地老现象客户端启动后没有任何报错但一直阻塞在 wait_for_result服务器端好像完全没收到目标。原因先排除服务器没启动再看话题名称是否一致——动作服务器与客户端通信靠的是同一个话题前缀比如服务器端写 countdown客户端却写成了 count_down两个节点各自安好但永远碰不上。解决用rostopic list | grep countdown看实际话题名再对比两端代码。还查不出时把 wait_for_server 加上超时参数失败时打印服务器状态。某个开发者在改需求时把服务器话题改成了复数客户端没同步排查了两小时才在话题列表里发现端倪。5.4 cancel 请求到了服务器却假装没听见现象客户端调用 cancel_goal服务器端回调没触发任务继续执行到结束。原因大概率是服务器端执行循环里没有检查 is_preempt_requested或者检查点距离太远。还有一种可能是用了 SimpleActionServer 但没启动单独的执行线程目标处理会串行卡住cancel 消息排队等待。解决在 execute_cb 的每条循环路径上都加抢占检查并调用 set_preempted 通知客户端。若任务里包含阻塞调用把阻塞操作放到子线程主循环保持活跃。验证手段是在客户端取消后观察 status 话题是否出现 PREEMPTED 状态。5.5 客户端重启后连不上服务器现象服务器一直在跑客户端 kill 掉重启后偶尔出现 wait_for_server 一直超时或者第一次 send_goal 无响应。原因常见于上一次运行的客户端节点没有完全退出同名话题的发布者残留导致连接状态混乱。另外 SimpleActionClient 在连接建立后如果服务器先崩了客户端侧的状态不会自动清理。解决重启客户端前rosnode cleanup清一次或给节点名加时间戳后缀避免同名冲突。客户端代码里在 wait_for_server 失败后主动调用client.close()并重新创建实例不要复用一个连接状态异常的对象。6. 进阶用 rostopic 验证动作链路给目标加超时与重试动作通信跑通只是第一步真正让它在工程里稳定服役还要靠两个习惯链路可视化和超时重试。先说链路验证。rosmsg 与 rostopic 是你手上的两把螺丝刀rosmsg show 看消息结构对不对rostopic list 看话题有没有起来rostopic echo 看具体数据流。排查问题时我一般会在四个时间点抓数据节点启动后、send_goal 前、feedback 流动中、result 返回后。给客户端加超时重试是源码示例里最常缺席的一段逻辑。示例跑完就退真实业务里必须考虑服务器瞬断、网络拥堵、目标被抢占这些意外。下面是一个带了重试机制的客户端片段import rospy import actionlib from action_notes.msg import CountdownAction, CountdownGoal def send_goal_with_retry(client, target, retries3): goal CountdownGoal() goal.target target for attempt in range(retries): client.send_goal(goal) # 等待结果最长阻塞 15 秒 if client.wait_for_result(rospy.Duration(15)): return client.get_result() rospy.logwarn(第 %d 次发送超时重试中, attempt 1) rospy.logerr(多次重试仍失败放弃本次目标) return None client actionlib.SimpleActionClient(countdown, CountdownAction) if not client.wait_for_server(rospy.Duration(5)): rospy.logerr(服务器不存在请检查话题) exit(1) result send_goal_with_retry(client, 10)参数说明wait_for_result 传入 rospy.Duration(15) 是把无限等待改成上限 15 秒返回 False 说明超时。重试次数设 3 是平衡业务及时性与系统负担。退出条件里 wait_for_server 先判空用 rospy.logerr 给出明确信号而不是静默退出。最后说一个我自己养成的习惯动作服务器与客户端运行期间不要太依赖 print 日志观察状态。开着rostopic echo /countdown/status能看到每个目标的生命周期迁移PENDING 到 ACTIVE 再到 SUCCEEDED任何一步卡住都一目了然。某次我排查一个「结果没收到但服务端显示完成」的问题就是靠观察 status 话题发现结果返回前多执行了一轮空转浪费了几个小时才定位到。在那之后我的所有动作项目都会固化一个监控脚本专门抓五个话题的状态变化。这套动作通信方案只要把消息三段结构吃透、五话题链路看清、抢占处理做好它就能从「另一个通信机制」变成你手里的任务调度工具。源码包里的示例代码只是一个起点真正值钱的部分是你在运行中积累的那套排查套路。希望帮到你。本文还有配套的精品资源点击获取