news 2026/10/15 16:54:50

ROS service通信实战:从service_demo理解服务端/客户端与srv定义

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ROS service通信实战:从service_demo理解服务端/客户端与srv定义

写在前面:service_demo这个项目,是我在ROS学习路上第五个上手练的示例工程。上一阶段还在用topic做频繁通信,到了这个demo,才算把“一问一答”的通信模式真正吃透。

如果你也正处于“刚看完小海龟,准备去碰自定义消息和服务”的阶段,这篇内容应该能帮你少走不少弯路。我会从设计思路、srv定义、服务端/客户端代码、编译运行、问题排查这几个角度,完整复盘service_demo的实操全过程,并把那些文档里不会写、只有自己踩过坑才知道的细节一并放出来。

1. service_demo项目到底是什么:找准通信方式的“适用边界”

带编号的H2说明,是否应该写为“## 1. service_demo项目到底是什么:找准通信方式的‘适用边界’”这样?好的。

1.1 一次请求一次应答,topic换成service

先用一个场景说清楚我在写service_demo之前遇到的困惑。当时我已经可以用publisher和subscriber做一个简单的雷达数据回传demo,节点A每100毫秒发一次数据,节点B持续收。这个模式用topic一点问题没有,因为数据是“流式”的,发完也不关心对方有没有处理、处理结果是什么。

但真实需求很快变了:我希望节点A发送一个“加多少度数”,节点B负责把电机转过去,然后告诉节点A“已经到位”。这个动作不是周期性的,也不是单向的——它需要发起方等待结果,而且结果必须和这次请求绑定。再用topic来做,就得自己设计“请求ID + 回复ID + 回调配对”的机制,复杂且容易出错。

ROS里的service通信正是干这个的:客户端发起请求,服务端执行完反馈一个响应,一对多、多对一都不合适,它是典型的同步远程调用。service_demo这个项目的核心价值,就是把这个机制拆成最简可运行的形式:一个服务端,一个客户端,一个自定义的消息类型。理解它之后,后面再去碰action、rosbridge、plugin那些东西,基础会牢很多。

1.2 demo的总体设计:服务端、客户端、服务消息

service_demo从宏观上看,由三个部分组成。

一是服务端节点,负责服务注册和业务处理。我会在节点里命名一个服务,比如/deal_service,然后在一个回调函数里接收请求、计算、返回响应。

二是客户端节点,负责发起调用。它会主动等待/deal_service上线,然后向它发送一个请求,并阻塞等待结果。

三是自定义srv消息。这是topic和service一个明显区别点:topic可以用std_msgs/String这样的现成类型,但service很多时候需要携带“请求要什么字段、响应回什么字段”的完整描述。service_demo里我定义了一个四则运算器功能作为例子,请求传两个数字和一个操作符,响应返回运算结果。

这个设计在工程中非常常见:节点A提交任务,节点B执行任务,节点A获得执行结果。学会service_demo之后,再去理解真正的机器人项目里的导航目标切换、机械臂规划请求、图像处理异步请求,思路都是相通的。

1.3 service_demo里“client/server”的真实角色划分

很多初学者看完service的官方教程,会对“服务端是不是就是机器人本体、客户端是不是就是上位机”产生误解。其实service的client/server和ROS机器人硬件没有任何必然绑定关系。

服务端是“提供能力的人”,它把自己的处理能力注册到ROS Master上。

客户端是“使用能力的人”,它按照名字去查找服务,然后发起调用。

在service_demo里,client和server既可以都写在同一台电脑上跑,也可以分布在两台运行了ROS的机器上。只要ROS_MASTER_URI一致,服务发现就靠Master完成。这个模型和实际的HTTP服务有点类似,但区别在于ROS service不直接用IP+端口访问,而是通过Master做了一次名字转发。

我实际写demo的时候,是先在一个普通工作空间里把两个节点都写好,再用roslaunch同时拉起。这就足够演示完整流程了。搞清楚这一层之后,再去拆工业场景里“导航服务端部署在机器人,客户端部署在远程监控机”的架构,就会觉得非常自然。

2. 写代码前必须搞懂的三个核心文件

service_demo不是一两百行就能写完的,但也不是特别复杂。真正决定项目能不能跑通的关键,在于三个文件:自定义srv、服务端节点、客户端节点。先聊清楚这三个文件的职责和关键点,后面代码才好一次成型。

2.1 自定义srv消息格式与字段匹配

在ROS里,service的通信格式必须由一个.srv文件描述。它和.msg非常相似,但结构多了一层:用---把请求和响应分隔开。请求部分写在分割线上面,响应部分写在分割线下面。

我用的srv文件是DealSrv.srv,字段如下:

float64 num_a float64 num_b string op --- float64 result

这个设计里,client发来的是一次运算的参数(两个数字、一个操作符),server则返回最终计算结果。

需要注意的地方是字段类型匹配。写Python或C++代码时,生成的类名和字段名会直接从srv文件映射过来,比如请求部分是req.num_a、req.num_b、req.op,响应是res.result。类型不对会在编译或运行时报错,而且报错信息往往不够直接,我自己就栽过一次,后面第4节会详细说。

另外建议养成一个习惯:srv文件里不要用无意义的单字母变量名。因为srv一旦生成,会被多个节点依赖,命名混乱会搞得后续调试非常痛苦。哪怕是demo,num_a/num_b这种语义化命名也很必要。

2.2 服务端节点:阻塞与多线程的选择

服务端的核心是定义一个回调函数,然后把它绑定到rospy.Service或ros::ServiceServer上。当client调用时,这个回调函数会执行,返回值会作为响应发给客户端。

回调函数的写法很关键。使用Python时,它接收一个请求对象,返回一个响应对象。

def handle_deal(req): rospy.loginfo("Get request: %.2f %s %.2f" % (req.num_a, req.op, req.num_b)) if req.op == "+": result = req.num_a + req.num_b elif req.op == "-": result = req.num_a - req.num_b elif req.op == "*": result = req.num_a * req.num_b elif req.op == "/": if req.num_b == 0: rospy.logwarn("Divisor cannot be 0!") result = 0.0 else: result = req.num_a / req.num_b else: rospy.logwarn("Unsupported op: %s" % req.op) result = 0.0 return DealSrvResponse(result)

这里的DealSrvResponse是srv文件自动生成的响应类,可以直接返回。如果只写return result,类型就对不上,会出问题。

如果业务逻辑比较复杂、client会并发请求,服务端就需要考虑多线程模型。rospy.Service默认的队列方式会让多个请求排队等待处理。如果某个请求处理耗时很久,后来的请求就会卡住。这时候可以用rospy.Service(..., callback, buff_size),并考虑在回调里用线程池处理耗时任务,或者改用rospy.Service的args传递共享资源。但在service_demo这个层级,同步阻塞是没问题的,我也没有为了演示而强行加复杂机制。

2.3 客户端节点:等到服务再调用

客户端的常见错误是启动就让节点立刻发请求,结果服务端还没就绪,调用失败或超时。正确姿势是启动后先等服务,等到服务真正上线再调用。

rospy.wait_for_service('/deal_service') dealer = rospy.ServiceProxy('/deal_service', DealSrv)

rospy.wait_for_service会阻塞,直到服务可用或超时。它本质上是不断尝试查询Master和service server的状态,机制可靠。

rospy.ServiceProxy是客户端的代理对象,创建时需要两个参数:服务名和srv类型。调用时传入请求对象,比如dealer(5.0, 3.0, '+'),它会返回响应对象,通过resp.result取得结果。

这里要提醒一点:ServiceProxy调用是同步的,程序会一直等到服务端恢复才往下走。如果服务端处理异常或长时间不响应,客户端就一直卡住,且没有默认超时兜底。所以实际工程里,建议将调用包在try/except中,并且给rospy.wait_for_service传一个合理的超时时间,避免项目卡死。

这就是我在service_demo里反复强调的“等到服务再调用”原则,简单但极其重要。

3. 手把手跑通service_demo全过程

这一节是整个项目的核心操作部分,我会把它拆成创建功能包、定义srv、编写代码、编译运行、验证五个阶段。每一步都给出可直接复制的命令和代码,环境基于ROS Noetic + Ubuntu 20.04 + Python 3。

3.1 创建功能包和目录结构

先假设你已经有了一个ROS工作空间,比如~/catkin_ws,里面有src目录。进入src后创建功能包:

cd ~/catkin_ws/src catkin_create_pkg service_demo rospy std_msgs message_generation message_runtime

这个命令会生成service_demo功能包,同时把rospy、std_msgs、message_generation、message_runtime写进package.xml,并在CMakeLists里自动加上相关配置。如果你之后还会用C++写节点,可以把roscpp和rospy都加上。

创建完功能包后,手动建一个srv目录:

cd ~/catkin_ws/src/service_demo mkdir srv

最终目录大概是这样:

service_demo/ ├── CMakeLists.txt ├── package.xml ├── srv/ │ └── DealSrv.srv ├── scripts/ │ ├── deal_server.py │ └── deal_client.py └── launch/ └── service_demo.launch

scripts目录和launch目录都需要手动创建,catkin_create_pkg不会帮我们建好。把Python脚本放进scripts后,记得要赋可执行权限:

chmod +x scripts/deal_server.py scripts/deal_client.py

3.2 srv文件生成与CMakeLists配置

先把DealSrv.srv写进srv目录,内容就是上一节给的那个定义。然后编辑功能包根目录的CMakeLists.txt,这一步比较容易漏。

需要修改的地方有三个。

第一,find_package中必须包含message_generation:

find_package(catkin REQUIRED COMPONENTS rospy std_msgs message_generation )

第二,在add_service_files中指定srv文件:

add_service_files( FILES DealSrv.srv )

第三,generate_dynamic_reconfigure_options不用管,但要在catkin_package里加上message_runtime:

catkin_package( CATKIN_DEPENDS rospy std_msgs message_runtime )

这三处改完后,回到工作空间根目录编译:

cd ~/catkin_ws catkin_make source devel/setup.bash

如果编译通过,说明srv已经生成了Python和C++对应的模块。可以通过以下命令确认服务消息是否注册成功:

rossrv list | grep service_demo

如果能看到service_demo/DealSrv,说明srv定义和编译环节都正常。

3.3 服务端和客户端代码实现

在scripts目录下新建deal_server.py,完整代码如下:

#!/usr/bin/env python3 import rospy from service_demo.srv import DealSrv, DealSrvResponse def deal_callback(req): rospy.loginfo("Receive request: %.2f %s %.2f" % (req.num_a, req.op, req.num_b)) if req.op == "+": result = req.num_a + req.num_b elif req.op == "-": result = req.num_a - req.num_b elif req.op == "*": result = req.num_a * req.num_b elif req.op == "/": if abs(req.num_b) < 1e-9: rospy.logwarn("Divisor is zero, set result to 0") result = 0.0 else: result = req.num_a / req.num_b else: rospy.logwarn("Unsupported op: %s" % req.op) result = 0.0 return DealSrvResponse(result) if __name__ == "__main__": rospy.init_node("deal_server") srv = rospy.Service("/deal_service", DealSrv, deal_callback) rospy.loginfo("Service /deal_service is ready.") rospy.spin()

第15行判断除数为0时,我用了abs(req.num_b) < 1e-9而不是req.num_b == 0,这是为了规避浮点数误差,常规开发里更稳妥。

客户端deal_client.py完整代码如下:

#!/usr/bin/env python3 import rospy from service_demo.srv import DealSrv def call_deal_service(): rospy.init_node("deal_client") rospy.wait_for_service("/deal_service") dealer = rospy.ServiceProxy("/deal_service", DealSrv) try: resp = dealer(5.0, 3.0, "+") rospy.loginfo("5.0 + 3.0 = %.2f" % resp.result) resp = dealer(6.0, 2.0, "*") rospy.loginfo("6.0 * 2.0 = %.2f" % resp.result) resp = dealer(8.0, 0.0, "/") rospy.loginfo("8.0 / 0.0 = %.2f" % resp.result) except rospy.ServiceException as e: rospy.logerr("Service call failed: %s" % e) if __name__ == "__main__": call_deal_service()

这里要注意客户端只发起三次调用就退出了,所以它不是一个常驻节点,跑完就没有了。这在一些自动化测试脚本里很常见,但如果想让客户端一直发送请求做压力测试,需要加循环和延时。service_demo保持简单即可,我会在最后一节聊聊如何扩展。

3.4 编译、启动与手动验证

写一个launch文件会很方便。launch/service_demo.launch内容:

<launch> <node name="deal_server" pkg="service_demo" type="deal_server.py" output="screen" /> <node name="deal_client" pkg="service_demo" type="deal_client.py" output="screen" /> </launch>

然后启动:

roslaunch service_demo service_demo.launch

如果一切正常,终端里会先出现服务端打印的“Service /deal_service is ready.”,随后客户端连续发起三次调用,两边的log会交替出现。关键是能看到类似这样的输出:

5.0 + 3.0 = 8.00 6.0 * 2.0 = 12.00 8.0 / 0.0 = 0.00

除了roslaunch,还有两种常见验证方式。

第一种:只启动服务端,然后用命令行工具手动调用服务。

rosservice list rosservice call /deal_service "num_a: 10.0 num_b: 5.0 op: '-'"

这种方式非常适合在调试时快速验证服务端逻辑,不需要写客户端代码。

第二种:运行rqt_graph,查看节点与服务之间的连接关系。如果能看到/deal_client和/deal_server通过/deal_service连接,说明底层通信注册全部成功。

rqt_graph

注意service在rqt_graph里显示为一条边,但和topic的虚线箭头不同,服务调用是有方向的。看到请求指向服务端、响应指回客户端的表达方式,就能理解这一层通信语义了。

4. 调试中容易踩的坑与排查思路

这部分我想系统整理service_demo里最常见的几个问题。它们很多不是demo本身写错,而是ROS环境、消息机制、并发行为之间产生的隐性摩擦。

4.1 请求超时和waitForService的合理使用

我在写客户端第一次版本时,没有加wait_for_service,直接创建ServiceProxy然后调用,结果终端报了一个“connection refused”类的错误,实际上是因为服务端还没完成注册。

后来加上wait_for_service,但如果服务端一直没起来,默认行为又是无限等待,这在自动化脚本里非常危险。更稳的写法是:

try: rospy.wait_for_service("/deal_service", timeout=5.0) except rospy.ROSException: rospy.logerr("Service /deal_service not available within 5s") return

超时之后,由调用方决定是重试还是直接退出。service_demo阶段建议直接退出,避免脚本卡死。

4.2 服务类型不匹配:变量是float,回报却是int

生成srv之后,客户端和服务端都必须引用同一个DealSrv类型。如果客户端从其他功能包引用了一个同名字但不同内容版本的srv,即使结构完全一样,运行时报错也会出现类型不匹配或接口对不上。

这种情况最常发生在多工作空间叠加、~/.bashrc里source了多个setup.bash的时候。检查思路是:

rospack find service_demo

确认当前环境里实际加载的是哪个路径,然后看是不是编译后环境没有刷新。

4.3 编译后头文件或Python模块找不到

如果是C++版本,编译后找不到头文件的常见原因是catkin_package里没有声明依赖,或者没有source devel/setup.bash。Python版本则要重点检查Python脚本是否加了shebang(#!/usr/bin/env python3)、是否有执行权限。ROS的roslaunch在执行Python节点时会直接调用脚本,没有执行权限就会报Permission denied。

另外,如果使用了Python3和Noetic,不要漏掉#!/usr/bin/env python3。如果写了python2,就会去调一个不存在的解释器。

4.4 一次调用被阻塞,后续请求全部卡住

service_demo里服务端处理很快,感受不到阻塞问题。但真实业务中,服务端回调如果内部有耗时操作,比如请求一个传感器数据、等待一个算法运行完成,后续调用全部会排队。

排查这个问题,可以先观察服务端日志,确认回调有没有进入;再用第二个终端手动调rosservice call,看是否也卡住。如果手动调用也卡,说明是服务端处理逻辑耗时;如果手动调用响应急,但客户端卡住,就要检查客户端的同步等待机制和网络延时。

常见解法有三种:一是把耗时计算挪到回调外部,用线程池和共享状态缓存结果;二是改用异步等待模式,不让主流程彻底阻塞;三是考虑换用action,因为它本身支持进度反馈和耗时任务取消。

4.5 用rosparam、rosservice、rqt_graph做组合排查

调试service_demo时,我习惯按照固定顺序排查:

先rosnode list,看节点是否都活着。

再rosservice list,看服务是否注册成功。

然后rosservice type /deal_service,确认服务类型正确。

如果服务存在但类型不对,rosservice call会提示参数错误,直接定位问题。

最后用rqt_graph看节点连线状态。如果节点都在,但连线缺失,基本可以断定srv模块加载或服务名写错。

这套排查顺序我从service_demo一直用到后面的实际机器人项目,屡试不爽。

4.6 客户端连续调用时的事务边界

有人会在同一个客户端里循环发送大量请求,比如做1000次加法和减法测试。这里最大的坑是:ROS service调用默认是顺序的、同步的,但有连接复用和缓冲机制。如果你观察到的结果不符合预期,先确认是不是客户端设置了多个连接实例,比如每次调用都重新建一个ServiceProxy,会导致握手开销非常大。

好的写法是复用同一个ServiceProxy对象,只改参数去调用。每次重新创建不仅慢,还容易造成连接未完全断开时的异常。

5. 从demo走向真实工程的经验总结

service_demo只是service通信的起步示例。一旦进入真实机器人开发,很快会发现“service够用,但有些地方真不该用service”。这节分享几个我从demo延伸出来的判断准则和扩展方向,帮你少走弯路。

5.1 什么时候该用service,什么时候转用action

service适合请求很快、不需要进度反馈的场景。比如“查询当前电量”、“设置某个参数”、“执行一次拍照”,这些可以在一两秒内完成,并且调用方只需要最终结果,用service非常自然。

但当业务变成“导航到某个点”、“执行机械臂插补”、“做一次建图优化”,它可能耗时几十秒甚至几分钟,且调用方希望得到阶段反馈、也能中途取消。这种情况就不该用service,要转用action。action本质上是一个双向的异步通信模式,它建立在topic之上,但又保留了service那样的“目标和结果”,同时增加了反馈通道。

service_demo里我是用四则运算来模拟service的特性,但在工程上,我见过有人把“长时间导航任务”也强行做成service,结果就是客户端一直阻塞、服务端回调里跑个大循环,整个系统像卡死了一样。现在我的判断标准很简单:耗时超过2~3秒、或需要进度反馈、或要支持中途取消,就别用service了。

5.2 服务超时的量化设计与客户端重试

service本身不强制客户端等待的绝对上限,所以设计客户端时,要给每次调用设定超时和重试策略。

假设一个服务正常响应时间是100毫秒,那么客户端超时可以设为500毫秒,重试3次,每次间隔200毫秒。这个策略放到具体代码里,需要结合rospy.wait_for_service的timeout参数和ServiceProxy调用的异常捕获来做。

在实际项目中,我一般还会在服务端返回一个响应码。比如result字段返回结果,success字段表示是否成功,message字段存放错误说明。这样客户端能区分“网络异常”“业务异常”“参数非法”。service_demo里没做这么细,但如果想练手,可以扩展srv定义,加一个int8 success和一个string message字段。

5.3 多服务的组织与管理

如果一个节点要提供多个服务,比如/robot/get_status、/robot/set_velocity、/robot/start_task,不要把它们全部堆在一个节点文件里。更清晰的做法是每个服务有独立的注册入口,甚至拆成多个节点,节点内按模块去组织回调。

service_demo里只有一个/deal_service,但我在后续项目里做过多服务节点。经验是:即使功能类似,也尽量在服务名上带层级前缀,比如/robot/nav/start、/robot/nav/cancel,这样用rosservice list一目了然,也方便别人接手。

同时要考虑服务的生命周期,服务端节点关闭时,对应的服务会从Master消失。客户端如果持有旧连接,再次调用时可能报错,需要重构连接。

5.4 service_demo后续还能怎么扩展

如果学完demo想进一步提升,我建议沿着三条线走。

第一条线,把四则运算改成真实业务模拟。比如做一个“图像缩放服务”,client传图片路径和缩放比例,服务端读取图片、处理、返回结果路径。这能帮你把service和实际业务结合起来,理解IO密集型和计算密集型的区别。

第二条线,改成C++版本。Python写service非常快,但真实系统往往需要考虑更多性能问题。C++版本虽然要手写很多代码,但对理解srv生成、内存管理、回调机制帮助很大。

第三条线,加上launch文件参数化。通过launch传递命令行参数,让服务端监听的服务名、前缀、甚至srv类型都可以配置,这样同一个服务端代码可以在多机多场景下复用。

我个人在实际操作中的体会是,service_demo真正值得花时间的地方不在代码本身,而在于把“同步请求-响应”的思维模型建立起来。搞懂了这个模型,再去理解action、ROS服务调用链、甚至外部接口封装,都会觉得顺畅很多。不要急着追求代码量,先把一个srv、一个server、一个client跑通,亲手体验一次“请求被处理、结果回到手上”的闭环,这比看十遍教程都有用。

如果你也正在学习ROS,建议把service、topic、action三种通信方式放在一起对比理解:topic适合持续不断的数据流,service适合一问一答的短任务,action适合长耗时且需要进度反馈的任务。service_demo就是中间这个环节最关键的一次实践。后面我会继续拆解更多基于service的工程案例,到时候再和大家细聊。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 16:54:48

Delphi 10.2 Tokyo 集成 TMS Component Pack v8.3.4.0 实战指南

简介&#xff1a;本资源为Delphi XE10.2 Tokyo平台专用的TMS Component Pack v8.3.4.0完整开发组件包&#xff0c;面向中高级Windows桌面及跨平台&#xff08;Android/iOS&#xff09;Delphi开发者&#xff0c;解决UI构建、数据绑定、图表可视化、数据库导航等高频开发需求。压…

作者头像 李华
网站建设 2026/10/15 16:53:32

SSM+Vue构建数据库原理教学平台:可审计事务与可视化执行计划

简介&#xff1a;本资源是一份面向计算机专业本科生的毕业设计论文&#xff0c;聚焦《数据库系统原理》课程平台的全流程开发实践&#xff0c;适用于毕业设计选题参考、SSMVue全栈项目学习及数据库应用能力提升。论文完整覆盖课题背景、SSM框架与Vue.js技术选型依据、角色驱动的…

作者头像 李华
网站建设 2026/10/15 16:53:11

WorkBuddy 搭建教程:从零开始配置你的 AI 工作助手

1. 引言 WorkBuddy 是一款面向开发者和团队的开源 AI 工作助手,它通过自然语言交互帮助用户完成代码生成、任务编排、文档处理等日常开发工作。本文将从环境准备、安装部署到基础配置,带你一步步完成 WorkBuddy 的本地搭建。 2. 环境准备 在开始安装之前,请确保你的机器满…

作者头像 李华
网站建设 2026/10/15 16:52:28

STM32F4驱动W5500硬协议栈芯片:SPI配置与TCP回环测试详解

简介&#xff1a;面向STM32F4开发者与嵌入式产品工程师的W5500以太网控制器测试压缩包&#xff0c;主要解决芯片驱动、SPI通信与全硬件TCP/IP协议栈的快速集成问题&#xff0c;适用于物联网、工业控制及数据采集设备的原型验证与二次开发。包体共有1256个文件、约10.86MB&#…

作者头像 李华
网站建设 2026/10/15 16:52:27

AT24C02进阶实战:I2C时序、页写回卷与掉电保存避坑指南

1. 这个存储器为什么让很多人翻车做蓝桥杯嵌入式的同学&#xff0c;基本都会在某个晚上跟AT24C02杠上。这个芯片看着不起眼&#xff0c;价格便宜、容量也只有256字节&#xff0c;但它在竞赛里的出镜率极高&#xff0c;尤其到了“像掉电保存、参数设置、运行状态记录”这类题目要…

作者头像 李华
网站建设 2026/10/15 16:52:26

从裸数据到驾驶舱:MQTT+WebSocket遥测可视化系统实战

1. 一个副驾驶视角&#xff1a;Apollo-11想解决的真实问题能做 Apollo-11 这个项目&#xff0c;纯粹源于我对飞行仪表盘的执念&#xff1a;那一排排动态跳动的数字、渐变色的燃料条、末端急剧跳变的高度值&#xff0c;画面对我来说有不可抗拒的吸引力。市面上的装饰品很多&…

作者头像 李华