ROS2 Service详解
本文由 ZiQingDream 编写,未经允许禁止转载。
本文作者:工程课代表[B站] / ZiQingDream[Github]
在之前的《ROS2 Executor详解》文章中,我们解释了调度器怎么处理回调和回调组。对于ROS2客户端-服务端架构,服务端符合之前所说的回调机制,这里不再赘述。该文将重心放到客户端的上,ROS2的客户端包含两种写法,同步客户端和异步客户端。
阅读本文需要你有一定ROS2基本理解,至少要有关于客户端-服务端基础知识。
ROS2 服务机制
ROS2官网中关于Service的可视化图,整个处理流程如下(Understanding services — ROS 2 Documentation: Humble documentation):。

ROS2 Service类似于网络通信中“服务端-客户端”架构,客户端发送请求给服务端,服务端(Server)接收到请求,会执行预先注册回调函数。ROS2 Service服务端符合《ROS2 Executor详解》中的回调机制,这里不在赘述。Service模式属于”N-1”模式,多个客户端(Client)发送请求给服务端,服务端每次处理一个请求,并返回相应。(这也同样受回调组的影响)
客户端代码流程一般如下图。在客户端代码中,等待结果返回方式有两种模式:同步模式和异步回调模式。无论哪一种模式,请求的发送都是异步发送的。也就是说在发送完请求后,可以不必等待,去做其他工作,在之后合适的时机去等待、检查结果返回,并对结果做进一步处理。

同步Service-Client模式
实验
服务端
1 | |
同步客户端
1 | |
同步客户端分析
如之前所述,请求发送是异步的,通过调用async_send_request()函数,请求被通过DDS发送给服务端:
1 | |
async_send_request()异步发送的代码如下:
1 | |
async_send_request()首先建立promise并获取future(注意这里的future带有一个请求ID),以便未来能够通过promise通知结果已经准备好。(如果对这部分不清楚,可以参考std::promise)。消息发送的流程,交给了async_send_request_impl()函数。
1 | |
async_send_request_impl()通过调用RCL层rcl_send_request()发送请求,并返回请求ID。然后以这个ID为键,将请求保留到pending_requests_,表明该请求在处理,之后收到服务端的反馈后,程序会pending_requests_中找到对应的请求Promise,然后通知客户端,结果已经返回。另外如果客户端不想接受反馈(例如出错了),则需要将请求从pending_requests_移除,防止回调被调用,这也很重要。
RCL底层细节暂不关注,请求发出后,返回的future就是等待任务完成的手段,程序通过spin_until_future_complete()等待结果返回:
1 | |
这里有一个问题:为什么不能直接调用future.get()?
要回答这个问题,我们需要深入spin_until_future_complete()函数,看下有什么额外的操作:
1 | |
在spin_until_future_complete()中建立一个单线程执行器,然后将传入的节点加入到单线程执行器,从而提供了与ROS2底层通信交互的能力,该执行器负责驱动底层获取由服务端反馈的消息。具体细节如下:
1 | |
由于节点被加入到临时的单线程执行器中,并且执行器通过节点与ROS2底层进行交互,所以对节点有以下要求:
- 该节点必须是创建客户端的节点,否则会阻塞但不会返回;
- 该节点在调用前不能加入到其他执行器中,否则就会抛出异常,导致程序终止。
紧接着控制权就交给了SingleThreadedExecutor的spin_until_future_complete()函数,该函数代码如下:
1 | |
等待包含三种状态:
- FutureReturnCode::SUCCESS:一切正常,拿到了从服务端反馈的消息
- FutureReturnCode::TIMEOUT:超时了,需要继续等待或做错误处理
- FutureReturnCode::INTERRUPTED:操作被终端,有可能是ROS2底层出了问题
在返回不为FutureReturnCode::SUCCESS,要处理好pending_requests_中滞留的请求(移除或继续等待)
有了之前文章中调度器概念后,整个过程并不复杂,就是通过执行器处理底层消息,当服务结果有反馈时,spin_once_impl()的execute_any_executable()触发客户端回调,future对应promise就会被设置,从而future.wait_for就会获取到”有结果到来”的通知:
1 | |
从上方代码基本可以回答之前的问题了:
由于等待过程涉及到底层服务反馈到来后,才会触发promise.set_value,必须使用spin获取底层的消息,不能直接通过future.get()类函数完成等待,否则就会卡死。
异步Service-Client模式
实验
异步客户端
1 | |
异步客户端分析
以上代码仍然是ros2 example中异步客户端样例,核心在于:
- 主线程(其他线程)负责注册callback并发送请求
- 启动独立线程负责执行器spin(),以便在主线程执行其他操作,spin线程完成ROS2消息处理
异步发送请求代码如下(注意这部分在主线程中完成):
1 | |
在发送请求代码中async_send_request()提供回调函数response_received_callback(),用于在接收到服务消息反馈时,触发回调函数调用。
1 | |
具体触发回调的位置,我们之前在同步客户端中已经说明了,具体细节:
1 | |
当从服务端接收到回应时,handle_response()被调用,代码通过promise.set_value通知客户端,消息已经被回应了。之后调用callback(),这就是用户注册回调函数,可对结果进一步地处理。另外值得注意的是,用户的回调是在执行器线程中被调用的,自定义的功能代码要做好共享变量的数据同步和保护。