4、ROS2通信机制(二):服务通信:Client与Server编程实战,自定义服务接口
好,咱们接着聊。上一章我们把话题通信聊透了,那是一种「发布-订阅」的广播模式。但实际工程里,你总会遇到这样的场景:我需要一个请求,然后等一个确切的回复。比如,我让机器人去抓一个杯子,它抓到了没有?抓成功了还是失败了?这时候话题通信就不太够用了——它只管发,不管回。
这就引出了我们今天的主角:服务通信(Service)。说白了,它是一种「一问一答」的模式。客户端(Client)发一个请求,服务端(Server)处理完,返回一个响应。非常像我们写程序时调一个函数,传参进去,拿返回值出来。
核心区别一句话总结:话题是「广播」,服务是「远程过程调用(RPC)」。
4.1 服务通信的底层逻辑
ROS2的服务通信底层用的是 请求-响应模型。客户端发送一个请求消息,服务端处理完后,返回一个响应消息。这个过程中,客户端会阻塞等待,直到收到响应或超时。
我在项目中遇到过一个问题:刚开始用服务时,总觉得它和话题差不多,只是多了个返回值。但实际用起来,服务的实时性要求更高。你想想看,如果服务端处理慢了,客户端那边就一直卡着,整个系统都可能被拖垮。所以,服务一般用于低频、确定性高的操作,比如「启动导航」、「查询状态」、「设置参数」。高频的传感器数据,还是老老实实用话题。
我的习惯:凡是需要「确认结果」的操作,我都用服务。凡是「只管发,不管结果」的,都用话题。这个原则帮我避免了很多坑。
4.2 自定义服务接口(.srv文件)
ROS2的服务接口定义在 .srv 文件中。它的格式很简单:
# 请求部分
---
# 响应部分
中间用三个横线 --- 隔开。上面是请求,下面是响应。
举个例子,我们做一个「计算矩形面积」的服务:
# RectangleArea.srv
float64 width
float64 height
---
float64 area
嗯,这里要注意:字段类型必须是ROS2支持的基本类型,比如 int32、float64、string、bool 等。也可以嵌套其他消息类型,但一般不建议搞得太复杂。
定义好 .srv 文件后,需要在 CMakeLists.txt 或 setup.py 中注册,然后编译。编译完成后,ROS2会自动生成对应的Python或C++代码。你可以在代码里直接 import 或 #include 使用。
我曾经踩过的坑:有一次我改了 .srv 文件,但忘了重新编译,结果客户端和服务端一直对不上号,报了一堆类型不匹配的错误。所以,每次修改接口文件后,一定要重新编译整个包。
4.3 服务端(Server)编程实战
我们以Python为例,写一个计算矩形面积的服务端。
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts # 这里用ROS2自带的示例接口
class AreaServer(Node):
def __init__(self):
super().__init__('area_server')
self.srv = self.create_service(AddTwoInts, 'compute_area', self.compute_area_callback)
self.get_logger().info('服务端已启动,等待请求...')
def compute_area_callback(self, request, response):
# 计算面积:宽 * 高
response.sum = request.a * request.b
self.get_logger().info(f'收到请求: {request.a} x {request.b} = {response.sum}')
return response
def main(args=None):
rclpy.init(args=args)
node = AreaServer()
rclpy.spin(node)
rclpy.shutdown()
if __name__ == '__main__':
main()
这段代码的核心是 create_service 方法。它需要三个参数:
- 服务类型:比如
AddTwoInts,这是ROS2自带的示例接口。 - 服务名称:字符串,比如
'compute_area'。客户端和服务端必须用同一个名字。 - 回调函数:当收到请求时,ROS2会调用这个函数。函数的参数是
request和response,你需要在函数里填充response并返回。
我个人习惯:回调函数的名字用 xxx_callback 的格式,这样一看就知道是回调。另外,回调函数里不要做耗时操作,否则会阻塞其他请求。如果确实需要耗时处理,可以开一个线程。
4.4 客户端(Client)编程实战
客户端稍微复杂一点,因为它需要等待服务端上线,然后发送请求,再等待响应。
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
import sys
class AreaClient(Node):
def __init__(self):
super().__init__('area_client')
self.cli = self.create_client(AddTwoInts, 'compute_area')
# 等待服务端上线
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().warn('等待服务端上线...')
self.req = AddTwoInts.Request()
def send_request(self, a, b):
self.req.a = a
self.req.b = b
# 异步发送请求,返回一个Future对象
self.future = self.cli.call_async(self.req)
rclpy.spin_until_future_complete(self, self.future)
return self.future.result()
def main(args=None):
rclpy.init(args=args)
node = AreaClient()
# 从命令行参数获取宽和高
a = int(sys.argv[1])
b = int(sys.argv[2])
response = node.send_request(a, b)
node.get_logger().info(f'计算结果: {a} x {b} = {response.sum}')
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
这里有几个关键点:
wait_for_service:客户端启动时,服务端可能还没起来。这个函数会阻塞等待,直到服务端上线。超时时间可以自己设,我一般设1秒。call_async:ROS2的服务调用是异步的。它返回一个Future对象,你可以用spin_until_future_complete等待它完成。这样不会阻塞整个节点。spin_until_future_complete:这个函数会一直旋转节点,直到Future完成。说白了,就是「等结果」。
我曾经踩过的坑:有一次我忘了调用 wait_for_service,直接发请求,结果程序崩溃了。因为服务端还没启动,客户端找不到服务。所以,发送请求前一定要确认服务端在线。
4.5 服务通信的完整流程
为了让你更直观地理解,我画了一张流程图:
你看,流程其实很简单。客户端发请求,服务端处理,然后返回结果。但实际工程中,服务通信的可靠性比话题高,因为它有明确的请求-响应配对。如果服务端挂了,客户端会收到超时错误,你可以做相应的处理。
4.6 服务通信 vs 话题通信
我整理了一个对比表,方便你快速区分:
| 特性 | 话题通信 | 服务通信 |
|---|---|---|
| 通信模式 | 发布-订阅(广播) | 请求-响应(RPC) |
| 数据流向 | 单向,发布者→订阅者 | 双向,客户端→服务端→客户端 |
| 是否阻塞 | 不阻塞,异步 | 客户端会阻塞等待响应 |
| 适用场景 | 高频数据流(传感器、状态) | 低频操作(查询、设置、触发) |
| 可靠性 | 尽力而为(QoS可调) | 高,有明确的成功/失败反馈 |
| 接口定义 | .msg 文件 | .srv 文件 |
一句话总结:话题是「喊一嗓子,谁爱听谁听」;服务是「我问你答,没答就再问」。
4.7 实战中的避坑指南
最后,分享几个我在项目中积累的经验:
- 服务名不要重复:同一个ROS2网络中,服务名必须唯一。如果两个节点注册了同一个服务名,后注册的会覆盖先注册的,容易出问题。
- 回调函数要快:服务端的回调函数是在ROS2的线程池里执行的。如果回调函数耗时太长,会阻塞其他服务的处理。我一般把耗时操作放到单独的线程里。
- 超时处理:客户端调用服务时,一定要设置超时。否则服务端挂了,客户端会一直卡着。ROS2的
wait_for_service和spin_until_future_complete都支持超时参数。 - 接口版本管理:如果你改了 .srv 文件,记得更新版本号。否则旧客户端和新服务端可能不兼容。我习惯在接口文件里加一个注释,记录修改日期和原因。
我的一个小技巧:调试服务通信时,可以用 ros2 service list 查看所有服务,用 ros2 service call 手动调用服务。这样不用写代码就能测试服务端是否正常。
好了,服务通信就聊到这里。你想想看,有了话题和服务这两种通信方式,ROS2里大部分的场景都能覆盖了。下一章我们聊聊另一种更灵活的通信方式——动作(Action),它结合了话题和服务的优点,特别适合长时间的任务。