想象你走进一家机器人餐厅:大厅广播“午市套餐上新”,这是 Topic——发出去就完事,没人应答;你按下呼叫铃问“现在几点了?”服务员秒回,这是 Service——一问一答,立等可取;你点了一份炭烤战斧牛排,下单后厨房开火、腌制、翻烤、装盘,全程有人给你报进度,你可以催单也能叫停,这是 Action——为长时间任务而生。ROS2 作为机器人的“操作系统语言”,把后厨里这些沟通方式标准化成了通信机制。本文就用这家餐厅的运营故事,把 Service 与 Action 的接口定义、QoS 配置、回调处理一次讲透。
理解服务与 Action 是掌握 ROS2 分布式协作的基石。学会它,你就能为机器人设计“即问即答”与“长程可控”的协作能力,这也是机器人系统工程师的核心功底。
可视化演示
对比ROS2 Service同步请求-响应与Action异步目标-反馈-结果通信机制,展示QoS可靠性配置对数据传输的影响。
本节配有交互动效(进入视口后自动加载,约 3KB)。
核心概念定位
餐厅后厨与前厅之间,有些问题急如星火。你按服务铃问“果汁机有空吗?”,服务员抬头就能答——这种“一问一答、立等可取”的沟通,就是 ROS2 中的 Service。而有些任务是马拉松,比如“为一桌二十人准备晚宴”,下单后要备菜、烹饪、上菜,中途还得汇报进度、允许顾客改主意——这种“长期执行、分步反馈、可取消”的协作,就是 ROS2 中的 Action。
技术上看,Service 采用同步请求-应答模型:客户端发出请求后,服务端处理完并返回响应,整个过程短促、双向、有始有终。Action 则在更长的时间尺度上工作,它定义了目标(Goal)、过程反馈(Feedback)和最终结果(Result)三段式消息流,客户端可以在任意时刻取消任务。两者的选择标准很简单:短时、一次性、不需要中间反馈 用 Service;长时、需要进度、可中途取消 用 Action。
所以进店后先想清楚:你只是要“一壶水的答案”,还是要“一整套餐的交付”?选对机制,机器人协作才不会乱套。
- Service 的 Client → Server客户端把一次请求发给服务端,就像顾客按下服务铃。
- Service 的 Server → Client服务端处理完立刻把结果返回,一问一答画上句号。
- Action 的 Client → Server客户端先下达目标,比如“把牛排做到七分熟”。
- Action 的双向箭头服务端在执行中持续回传反馈,客户端可随时下达取消指令,任务结束后再返回最终结果,沟通是往复的。
接口定义
餐厅刚开业时菜单上只写菜名,顾客点“随便”后厨就懵了。后来菜单有了标准化编号和规格说明。ROS2 也是同样的道理:通信双方必须先约定“消息长什么样”。srv 文件就像菜单上的“编号+价格”——短、平、快;action 文件则像一本菜品说明书——目标、过程、成品全都写清楚。
srv 接口把一次通信分成两段:请求段(request)和响应段(response),中间用 --- 隔开。action 接口则分成三段:goal(目标)、result(最终结果)和 feedback(过程反馈)。看两个实际的接口定义:
规格写清楚,系统才不会出现“我要牛排你却端来沙拉”的乌龙。定义接口文件,就是在为机器人的协作立“菜单规范”。
- srv 的请求字段花括号里的 $r_i$ 是调用方传入的参数,比如加法的两个加数。
- srv 的响应字段$s_j$ 是服务端算好后回传的数据,比如它们的和。
- action 的目标 G启动一项长期任务所需的指令文字,比如目的地坐标。
- 反馈序列 $F_i$执行过程中向客户端通报的进度快照,比如“已走完 30%”。
- 结果 R任务收尾时给出的最终交付物,比如“到达终点,耗时 3 分钟”。
# 加法器服务:int64 a / int64 b 作为请求,int64 sum 作为响应int64 aint64 b---int64 sum
# 倒计时动作:target 是目标,finished 是最终结果,progress 是过程反馈int64 target---bool finished---float32 progress
服务通信实现
顾客按服务铃,服务员走到桌边听清需求,转身处理,再回来答复。如果顾客不急,可以回座位等水送过来——这时他说“水好了叫我一声”,这就是异步。ROS2 的 Service 通信同样有这两种姿态。
实现上,Service 由服务端(Server)和客户端(Client)组成。服务端用 create_service 创建并注册回调,客户端用 create_client 建立连接。调用的关键区别在于:同步调用会阻塞当前线程直到拿到响应,适合主流程必须先拿到数据再往下走的场景;异步调用立即返回一个 future 凭证,等响应到达后由 executor 触发回调,主线程不阻塞。
使用异步时,客户端可以一边等服务端计算,一边继续发布话题或处理其他回调。但要注意:服务端只有在其节点被 spin 时才会处理请求,因此两端都必须有正在转动的 executor,否则就会“服务员在休息,没人接你的铃”。
聪明的服务员会灵活选择“立等可取”与“稍后通知”。写代码时多想想:这一问,主线程能不能等?
- send(req) 与 recv(rsp)一次远程过程调用的两端,客户端发请求,服务端收请求。
- RPC 箭头请求穿越 ROS2 中间件到达服务端的过程,相当于信号从铃铛传到服务员耳朵。
- blocking(同步)调用后线程挂起等待结果,像顾客站在前台直到水壶递到手。
- non-blocking(异步)调用立即返回一个 future,后续回调伺机执行,像顾客回桌等水。
Action机制
点牛排时,顾客不想站在明火旁干等。厨房发一个取餐牌(Goal 已接收),之后每完成一步就广播进度(腌制完成、翻面、装盘),最后上桌(Result)。顾客也可以中途大喊“不要了”(Cancel)。这正是 Action 的完整生命周期。
实现上,Action 通信由 Action Server 与 Action Client 组成。服务端接收 goal、调度执行体、周期性发布 feedback、最终发布 result,并响应取消请求;客户端发送 goal、注册 feedback 回调、接收 result、可随时发起 cancel。底层虽然由多组 Topic 与 Service 组合实现,但对用户暴露的是统一的三段式 API。
与“手动写 Service + 轮询”相比,Action 的价值在于把进度和取消都标准化了:你不需要自己造轮子去维护“任务状态机”,框架替你完成了大部分调度和错误处理。
Action 就是给机器人“点菜”的完整闭环:长期任务永远有进度条,也永远有紧急刹车。
- G(Goal)目标消息,标明任务内容和参数,比如“去充电桩,速度 0.5 m/s”。
- \{F_i\} 反馈流每完成一步就推送一条进度消息,客户端可以实时更新 UI。
- R(Result)最终结果,包含成功与否与最终数据,比如“已到达,耗时 2 分 30 秒”。
- cancel 取消信号客户端可随时下达取消命令,服务端收到后中止执行并返回取消状态。
QoS配置
有的菜必须趁热送到,比如滚烫的砂锅汤;有的菜可以慢慢送,比如水果拼盘。餐厅对不同菜品选择不同配送方式。ROS2 通信也一样,用 QoS(Quality of Service,服务质量)策略决定消息“配送”的保障等级。
QoS 是一组策略参数的集合,核心包括:Reliability(可靠性)、History(历史策略)、Depth(队列深度) 和 Durability(持久性)。对 Service 与 Action 这种需要双向确认的通信,RELIABLE(可靠投递)是默认且常见的选择——请求不能丢,丢了要重发。而传感器流常用 BEST_EFFORT(尽力而为),丢几帧无所谓。
配置 QoS 时最关键的一点是兼容性:通信双方的 QoS 参数必须兼容才能建立连接。比如服务端用 RELIABLE,客户端用 BEST_EFFORT,就可能出现握手失败或收不到消息的情况。这就像顾客说“必须闪送”,外卖平台却说“只能平邮”——这单根本接不了。
给 Service 与 Action 配上合适的 QoS,就像在菜单上注明“此菜需趁热食用”——配送方式定对了,沟通才不凉。
- Reliability可靠投递(RELIABLE)还是尽力而为(BEST_EFFORT),对应闪送和平邮。
- HistoryKEEP_LAST 只保留最近若干条,KEEP_ALL 全部保留,像冰柜里只留最新水果盘还是全留着。
- Depth队列能塞多少条消息,深度越小越容易丢旧数据。
- Durability是否让后来加入的订阅者也能拿到旧值,像“新来的服务员也能看到昨天交接班记录”。
回调处理
餐厅忙时,一个服务员要同时处理 A 桌加菜、B 桌结账、C 桌投诉。他必须有个工作节奏:先记下 A 桌需求,转身给 B 桌递账单,再安抚 C 桌。ROS2 的节点也是如此——多个事件同时在催你,需要执行器(Executor)统一调度回调函数。
ROS2 节点处理事件靠回调函数:收到 Service 请求会调用服务回调,收到 Action 反馈会调用反馈回调。SingleThreadedExecutor 单线程挨个执行回调,像只有一位服务员跑堂;MultiThreadedExecutor 用线程池并行处理,像多人分工接待。选哪种取决于回调是否耗时:如果某个回调里有等待硬件 IO 的阻塞操作,单线程执行器会被卡住,其他回调全部排队。
Action 的 feedback 回调触发频率通常很高,必须写得轻量——不要在反馈回调里做重计算或磁盘写入。一句话:回调要短小精悍,就像服务员不会站在一桌旁边聊十分钟。
管理好回调,就是管理好接待节奏。多线程不是万能药——一个不讲武德的阻塞回调照样会让线程池的线程饿死。
- $T_{\text{cb}_i}$ 每个回调耗时每一个小任务的处理时间,比如递账单要 5 秒。
- 单线程求和所有回调排队相加,一个卡住全店停摆。
- 多线程取最大回调并行执行,总周期由最慢的那个决定。
- 设计原则把重活丢到独立线程池,回调里只做轻量登记。
工程实践
成熟的餐厅会记账:排班表、进货单、顾客反馈。机器人开发也离不开这些幕后功夫——调试工具、生命周期、性能监控就是 ROS2 的“运营台账”。
开发时最常用的是命令行工具:ros2 service list 查看当前有哪些服务,ros2 service call 手动调用服务测试;ros2 action list 查看 Action,ros2 action send_goal 手动下发目标。这些命令能快速确认“服务员有没有在岗、接不接单”。此外要留意生命周期管理:Lifecycle Node 提供了 unconfigured→inactive→active→finalized 的状态机,适合有启动顺序要求的复杂机器人系统;性能监控则关注消息频率、回调耗时和队列深度,排查“哪一环拖慢了整条链路”。
常见故障排查口诀:服务端没 spin,请求无人理;QoS 不兼容,握手直接断;回调里死等,全节点卡死。理解了整条链路的时延构成,问题定位就不难。
台账越清晰,餐厅越不容易翻车。把这条时延公式刻在脑子里,排查性能问题就有章可循。
- 序列化与反序列化消息在发送前打包、接收后解包,像把食材装进保鲜盒再打开。
- 传输时延消息在网络中飞行的时间,像外卖骑手在路上跑。
- 排队时延消息到达后等待执行器调度的时间,像高峰期等位。
- 处理时延服务端真正执行业务逻辑的耗时,像后厨炒菜。
AI 应用场景
AGV 物流机器人的长程配送
阶段:任务调度与执行
解决的问题:传统运动控制用 Topic 广播目标点,既拿不到进度也没法取消;改用 Action 后,配送机器人接收 Goal,沿途不断反馈位置与电量,调度系统随时可取消改派,任务执行透明可控。
具体案例:某电商仓库中,多台 AGV 接收“将料箱从 A 区送至 B 区”的 Action 目标。调度中枢看到每台车的实时反馈,当一台 AGV 发生拥堵时,系统下发取消指令并重新分配任务,整体吞吐量提升约 30%。
巡检机器人的状态自检
阶段:状态查询与健康诊断
解决的问题:传统方式需人工 SSH 连接机器人或轮询话题,信息不即时且浪费带宽;用 Service 封装“查询电量/温度/自检结果”接口,中控大厅一键呼叫即问即答,像按服务铃一样简单。
具体案例:物业巡检机器人每 10 分钟被中控调用一次“电量查询”服务,若返回电量低于 20%,中控立即下发“回充站导航”的 Action 目标,实现全自动补能闭环。
机械臂协作装配
阶段:多环节协同控制
解决的问题:装配任务既有“查状态”的短请求,也有“执行轨迹”的长任务。单独用 Service 扛不住长时间执行,单独用 Action 又不够轻快。组合使用:先 Service 查询夹爪开合状态,再 Action 下达搬运装配目标,两者配合恰到好处。
具体案例:3C 产线上的协作机械臂接到“抓取螺丝并拧入孔位”的 Action 任务,每完成一个动作就反馈进度;在切换工件前,调度系统用 Service 快速确认吸盘真空度是否达标,保障节拍稳定。
Python 演示
import timeimport threading class RoboRestaurant: """机器人餐厅: 演示 Service 与 Action 两种通信模式""" def __init__(self): self._cancel_events = {} self._serial = 0 # ---- Service: 一问一答 ---- def query_water(self, table): """按服务铃: 请求一次, 立即返回响应""" return {"table": table, "status": "water served", "cost_ms": 50} # ---- Action: 从下单到上桌 ---- def place_order(self, meal, steps=3): """点大餐: 逐阶段执行, 支持反馈与取消""" self._serial += 1 goal_id = self._serial cancel_event = threading.Event() self._cancel_events[goal_id] = cancel_event def _cook(): for i in range(1, steps + 1): if cancel_event.is_set(): print("[{}] cancelled: {}".format(goal_id, meal)) return "cancelled" time.sleep(0.2) print("[{}] feedback {}/{}: cooking {}".format( goal_id, i, steps, meal)) print("[{}] result: {} served".format(goal_id, meal)) del self._cancel_events[goal_id] threading.Thread(target=_cook, daemon=True).start() return goal_id def cancel_order(self, goal_id): event = self._cancel_events.get(goal_id) if event: event.set() return True return False if __name__ == "__main__": restaurant = RoboRestaurant() print(restaurant.query_water(3)) # Service 同步秒回 gid = restaurant.place_order("steak", 3) # Action 长期执行 time.sleep(0.5) restaurant.cancel_order(gid) # 中途取消 time.sleep(0.5)
def query_water(self, table):Service 处理函数:一次请求立即算出并返回结果,对应服务端回调。return {"table": table, "status": ...}构造响应体,相当于 srv 文件的 response 段。cancel_event = threading.Event()为这次任务配备一个“取消信号旗”,对应 Action 的取消机制。if cancel_event.is_set():执行循环中检查取消标志,收到信号就终止任务。time.sleep(0.2)模拟耗时的烹饪步骤,现实中对应电机转动或路径规划。print("[{}] feedback {}/{}...")每阶段输出进度反馈,对应 action 的 feedback 消息。threading.Thread(target=_cook, daemon=True).start()后台线程执行长任务,主线程不被阻塞。restaurant.cancel_order(gid)客户端发起取消请求,对应 Action 的 cancel goal。
常见误区
误区“我的任务要跑 5 分钟,但我不想学 Action,直接用 Service 不就行了?我把请求发过去,服务端慢慢算,最后返回结果。”
正解Service 是一问一答的短时模型,客户端同步等待时线程会被阻塞,期间无法收到任何进度反馈,也不能中途取消。如果服务端处理超过数秒,用户体验极差,系统还容易触发超时。长时间、需要反馈、可能取消的任务应该交给 Action,正如点牛排不该按服务铃解决。
为何常见:初学者刚接触 Service 时觉得“能返回结果就行”,忽略了长任务需要的反馈和取消能力,误把 Service 当成通用 RPC。
误区“Action 底层不就是两个 Topic 加一个 Service 吗?我自己写回调也行,没必要用 Action 框架。”
正解Action 虽然在底层封装了 goal/cancel/result 等若干 Topic 与 Service,但它提供了统一的三段式 API、完整的状态机、取消语义和内置的反馈/结果处理。手动拼装极易遗漏边界情况,比如“任务执行到一半取消时,反馈线程怎么退出”。用框架是走成熟路线,不是偷懒。
为何常见:深入源码后看到底层实现,容易产生“不过如此”的错觉,低估了应用层状态管理的复杂度。
误区“我在代码里给 Service 配了 QoS,RELIABLE 和 KEEP_LAST,应该就没问题了吧?”
正解QoS 是通信双方协商的结果,不是单方面的事。如果服务端用 RELIABLE、客户端用 BEST_EFFORT,二者不匹配可能导致连接建立失败或消息静默丢失。配置 QoS 时必须同时检查发布端和订阅端(或服务端与客户端)的策略是否兼容,就像外卖员和顾客必须约定同一个配送方式。
为何常见:QoS 参数较多且文档分散,初学者容易只配置一端,忽视“双方需兼容”这一核心约束。
误区“我的回调里要等电机转到位,写个 while 循环不断读传感器不就行了?反正回调会执行完。”
正解在默认的单线程执行器里,一个回调阻塞,整个节点的所有订阅、定时器和回调都会停摆,就像服务员站在一桌前打持久战,其他桌全晾着。回调必须短小精悍,耗时操作应放入独立线程池或改用多线程执行器,并配合线程安全机制。
为何常见:写普通 Python 脚本时,while 循环很常见;但节点模型是事件驱动的,阻塞回调等于堵死事件循环,这个转变初学者往往意识不到。
误区“服务端收到 goal 之后,execute 函数应该立刻跑起来,任务就在执行了吧?”
正解Action 服务端收到 goal 后,需要先经过 accept 确认,之后执行器才会在 spin 过程中调度 execute 回调。若执行器没在转,goal 只是躺在队列里。此外,accept 和 execute 是解耦的,可以在 accept 阶段做权限校验,拒绝不合理的目标。
为何常见:把 Action 的 goal 机制想成了普通 Service 的“请求即处理”,忽略了 action 服务端的确认环节和调度时序。
面试题
Q1. 以下哪个场景最适合使用 Service 通信?
- A.导航至指定目标点并返回
- B.查询电池当前电量数值← 正确答案
- C.持续广播激光雷达数据
- D.周期上报超声波距离值
答案:B。Service 适合短时请求-响应,查询电量是一问一答;导航要长时间执行且需进度,适合 Action;激光雷达和超声波数据是持续流,适合 Topic。
Q2. Action 消息模型中,用于报告任务执行进度的部分是?
- A.目标消息 Goal
- B.反馈消息 Feedback← 正确答案
- C.结果消息 Result
- D.取消请求 Cancel
答案:B。Action 三段式中,Goal 是目标,Result 是最终结果,Feedback 是执行过程中的进度报告。Cancel 不属于三段式消息,而是独立的取消机制。
Q3. srv 文件中用 --- 分隔符分割的是什么?
- A.请求段与响应段← 正确答案
- B.消息头与消息负载
- C.注释与有效字段
- D.消息版本与类型名
答案:A。srv 接口文件把 request 字段写在 --- 上方,response 字段写在下方,这个分隔符是服务接口的语法标志。
Q4. ROS2 中服务端用 RELIABLE、客户端用 BEST_EFFORT 配置 QoS,可能发生什么?
- A.数据全部丢失且无法恢复
- B.连接建立失败或消息丢失← 正确答案
- C.消息发送频率自动提升
- D.客户端自动升级为可靠模式
答案:B。QoS 必须双方兼容。RELIABLE 与 BEST_EFFORT 在可靠性维度不匹配,会导致握手失败或收发异常。系统不会自动降级或升级,必须人工对齐。
Q5. Action 的 message 模型中包含哪三部分?
- A.目标消息 Goal← 正确答案
- B.反馈消息 Feedback← 正确答案
- C.结果消息 Result← 正确答案
- D.心跳消息 Heartbeat
答案:ABC。Action 接口由 Goal、Feedback、Result 三段消息组成,分别对应任务目标、过程进度和最终结果。Heartbeat 是通用的连接保活机制,不属于 Action 消息模型。
Q6. 关于 Executor 执行器,下列说法正确的是?
- A.单线程执行器串行处理回调← 正确答案
- B.多线程执行器可并行处理回调← 正确答案
- C.执行器负责调度等待中的回调← 正确答案
- D.每个回调必须单独占一个线程
答案:ABC。SingleThreadedExecutor 逐个执行回调,MultiThreadedExecutor 用线程池并行执行,Executor 的职责就是调度就绪回调。D 错误,多线程执行器共享线程池,并非每个回调独占线程。
Q7. 客户端想中途停止一个正在执行的 Action 任务,应该怎么做?
- A.发送取消目标请求← 正确答案
- B.重启全部网络节点
- C.删除 Action 源文件
- D.关闭机器人电源
答案:A。Action 内置了取消语义,客户端发送 cancel goal 请求,服务端收到后中止执行并返回取消状态。重启节点、删文件、断电都属于粗暴且错误的方式。
Q8. 下列哪些命令行工具用于 Action 调试?
- A.ros2 action list← 正确答案
- B.ros2 action send_goal← 正确答案
- C.ros2 pkg executables
- D.ros2 daemon stop
答案:AB。ros2 action list 查看当前 action,ros2 action send_goal 手动下发目标,是调试 action 的常用命令。ros2 pkg executables 查看可执行文件,ros2 daemon stop 控制后台守护进程,均与 action 调试无关。
Q9. 下列哪种场景更适合用 Service 而非 Action?
- A.需要持续反馈执行进度
- B.需要同步获取明确响应← 正确答案
- C.需要中途取消任务
- D.需要周期广播传感器流
答案:B。Service 的核心特征是同步请求-响应,适合“问一句、答一句”的短交互。需要反馈或取消应选 Action,周期广播应选 Topic。
Q10. ROS2 中 Action 接口文件的扩展名是什么?
- A..srv
- B..msg
- C..action← 正确答案
- D..yaml
答案:C。Action 接口文件使用 .action 后缀,Service 用 .srv,普通消息用 .msg,配置文件常用 .yaml。三者分别是不同通信机制的接口定义格式。