一、ROS2通信机制概述
ROS 2 的三种核心通信机制(话题 Topic 、服务 Service 、动作 Action)是基于不同的底层层次结构与应用场景设计的。
ROS 2 三大通信机制核心对比:
| 维度 | 话题 (Topic) | 服务 (Service) | 动作 (Action) |
| 通信模式 | 异步发布/订阅 (Pub/Sub) | 同步/异步 请求-响应 (Req/Resp) | 异步 目标-反馈-结果 (Goal/Feedback/Result) |
| 基准关系 | 一对多 / 多对多 | 一对一 / 多对一(多个 Client 发往同一个 Server) | 一对一 / 多对一 |
| 底层实现 | DDS 主题发布 | DDS 双向 RPC 调用 | 由3 个 Topic + 2 个 Service 组合封装实现 |
| 数据流向 | 单向传输(Publisher \\to Subscriber) | 双向传输(Client Server
Client) | 双向多通道传输(包含目标发送、状态反馈、最终结果) |
| 典型适用场景 | 传感器数据高频连续流(如激光雷达 /scan、相机图像、底盘里程计) |
简短、即时完成的逻辑查询或状态切换(如开灯、参数查询、状态复位) | 耗时长、可打断、需要过程反馈的任务(如导航到目标点、机械臂抓取) |
|---|
1. 话题 (Topic)
-
同步/异步 :非阻塞异步通信。Publisher 只管推送数据到 DDS 网络,无需等待 Subscriber 确认接收,不会阻塞主线程。
-
拓扑结构 :支持多对多。同一话题可以存在多个 Publisher 和多个 Subscriber。
2. 服务 (Service)
-
同步/异步:
-
客户端(Client) :在 C++ 中通过
async_send_request()默认实现异步非阻塞发送(推荐,配合 Future/Callback 操作);也支持等待结果的同步阻塞调用。 -
服务端(Server):接收请求并处理完成后返回响应。
-
-
拓扑结构 :支持 多对一。多个 Client 可以向同一个 Server 发送请求,但每一次具体的请求-响应交互都是一对一独立的。
3. 动作 (Action)
-
同步/异步 :全异步通信。Client 发送 Goal 后不会阻塞,可以一边执行其他逻辑,一边接收 Server 实时发回的 Feedback;支持随时发送 Cancel 请求中断任务。
-
底层构成(重要概念):动作是一种应用层的通信机制,其底层是基于话题和服务来实现的。
-
Goal Service:发送任务目标并返回是否接受目标。
-
Cancel Service:发送取消任务指令。
-
Result Service:任务结束时获取最终执行结果。
-
Feedback Topic:连续广播任务过程中的中间状态数据。
-
Status Topic:广播 Action 服务端的当前状态机状态。
-
-
Action 状态机:ROS 2 Action 定义的标准状态转移包含以下几类核心状态:
| 状态类型 | 状态名称 | 含义说明 |
|---|---|---|
| 初始状态 | STATUS_UNKNOWN |
状态未定义/未初始化。 |
| 目标处理 | STATUS_ACCEPTED |
Server 已接收目标,等待开始执行。 |
| 执行中 | STATUS_EXECUTING |
Server 正在后台执行长时任务。 |
| 取消流程 | STATUS_CANCELING |
Client 提出了取消请求,Server 正在清理资源并准备打断任务。 |
| 终止状态 | STATUS_SUCCEEDED |
任务成功完成(终态)。 |
| 终止状态 | STATUS_CANCELED |
任务已被成功取消(终态)。 |
| 终止状态 | STATUS_ABORTED |
执行过程中发生错误,Server 主动终止了任务(终态)。 |
二、ROS2 通信接口与接口文件定义

ROS 2 使用强类型的接口定义文件,在编译时会自动生成对应语言(C++ / Python)的数据结构和序列化/反序列化代码。
<ros_package>/
├── msg/
│ └── SensorRead.msg # 话题接口文件
├── srv/
│ └── TriggerTask.srv # 服务接口文件
└── action/
└── NavigateTo.action # 动作接口文件
1. 话题接口文件 (.msg)
仅包含传输的数据字段定义。
# SensorRead.msg
std_msgs/Header header
float64 temperature
float64[3] position
2. 服务接口文件 (.srv)
由 --- 分割为请求(Request)与响应(Response)两部分。
# TriggerTask.srv
string task_name # Request: 客户端请求的数据
---
bool success # Response: 服务端返回的结果
string message
3. 动作接口文件 (.action)
由两个 --- 分割为 目标(Goal) 、结果(Result) 与 反馈(Feedback) 三部分。
# NavigateTo.action
geometry_msgs/PoseStamped target_pose # 1. Goal: 导航目标点
---
geometry_msgs/PoseStamped final_pose # 2. Result: 最终到达位置与耗时
float32 total_time
---
geometry_msgs/PoseStamped current_pose # 3. Feedback: 实时当前位置与剩余距离
float32 distance_remaining
4. 编译接口文件
完成定义后,还需要在功能包的CMakeLists.txt中配置编译选项,让编译器在编译过程中,根据接口定义,自动生成不同语言的代码:
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/GetObjectPosition.srv"
)
功能包的package.xml文件中也需要添加代码生成的功能依赖:
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
设计选型决策法则
在机器人系统设计时,选择通信接口的标准逻辑:
-
数据是连续高频发出的吗?
Topic(如传感器数据数据流)。
-
需要执行一个快速动作并立马知道成功/失败吗?
Service(如查询传感器状态、切换硬件模式)。
-
任务需要执行几秒甚至更久,中途需要知道进度,或者随时可能被用户取消吗?
Action(如自主导航、轨迹规划执行)。
三、话题、服务、动作的创建
1. 创建发布者代码流程如下:
-
编程接口初始化
-
创建节点并初始化
-
创建发布者对象
-
创建并填充话题消息
-
发布话题消息
-
销毁节点并关闭接口
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from std_msgs.msg import String # 字符串消息类型"""
创建一个发布者节点
"""
class PublisherNode(Node):def __init__(self, name): super().__init__(name) # ROS2节点父类初始化 self.pub = self.create_publisher(String, "chatter", 10) # 创建发布者对象(消息类型、话题名、队列长度) self.timer = self.create_timer(0.5, self.timer_callback) # 创建一个定时器(单位为秒的周期,定时执行的回调函数) def timer_callback(self): # 创建定时器周期执行的回调函数 msg = String() # 创建一个String类型的消息对象 msg.data = 'Hello World' # 填充消息对象中的消息数据 self.pub.publish(msg) # 发布话题消息 self.get_logger().info('Publishing: "%s"' % msg.data) # 输出日志信息,提示已经完成话题发布def main(args=None): # ROS2节点主入口main函数
rclpy.init(args=args) # ROS2 Python接口初始化
node = PublisherNode("topic_helloworld_pub") # 创建ROS2节点对象并进行初始化
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
2. 创建订阅者代码流程如下:
-
编程接口初始化
-
创建节点并初始化
-
创建订阅者对象
-
回调函数处理话题数据
-
销毁节点并关闭接口
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from std_msgs.msg import String # ROS2标准定义的String消息"""
创建一个订阅者节点
"""
class SubscriberNode(Node):def __init__(self, name): super().__init__(name) # ROS2节点父类初始化 self.sub = self.create_subscription(\ String, "chatter", self.listener_callback, 10) # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度) def listener_callback(self, msg): # 创建回调函数,执行收到话题消息后对数据的处理 self.get_logger().info('I heard: "%s"' % msg.data) # 输出日志信息,提示订阅收到的话题消息def main(args=None): # ROS2节点主入口main函数
rclpy.init(args=args) # ROS2 Python接口初始化
node = SubscriberNode("topic_helloworld_sub") # 创建ROS2节点对象并进行初始化
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
3. 创建服务客户端代码流程如下
-
编程接口初始化
-
创建节点并初始化
-
创建客户端对象
-
创建并发送请求数据
-
等待服务器端应答数据
-
销毁节点并关闭接口
import sys
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from learning_interface.srv import AddTwoInts # 自定义的服务接口class adderClient(Node):
def init(self, name):
super().init(name) # ROS2节点父类初始化
self.client = self.create_client(AddTwoInts, 'add_two_ints') # 创建服务客户端对象(服务接口类型,服务名)
while not self.client.wait_for_service(timeout_sec=1.0): # 循环等待服务器端成功启动
self.get_logger().info('service not available, waiting again...')
self.request = AddTwoInts.Request() # 创建服务请求的数据对象def send_request(self): # 创建一个发送服务请求的函数 self.request.a = int(sys.argv[1]) self.request.b = int(sys.argv[2]) self.future = self.client.call_async(self.request) # 异步方式发送服务请求def main(args=None):
rclpy.init(args=args) # ROS2 Python接口初始化
node = adderClient("service_adder_client") # 创建ROS2节点对象并进行初始化
node.send_request() # 发送服务请求while rclpy.ok(): # ROS2系统正常运行 rclpy.spin_once(node) # 循环执行一次节点 if node.future.done(): # 数据是否处理完成 try: response = node.future.result() # 接收服务器端的反馈数据 except Exception as e: node.get_logger().info( 'Service call failed %r' % (e,)) else: node.get_logger().info( # 将收到的反馈信息打印输出 'Result of add_two_ints: for %d + %d = %d' % (node.request.a, node.request.b, response.sum)) break node.destroy_node() # 销毁节点对象 rclpy.shutdown() # 关闭ROS2 Python接口
4. 创建服务服务端代码流程如下
-
编程接口初始化
-
创建节点并初始化
-
创建服务器端对象
-
通过回调函数处进行服务
-
向客户端反馈应答结果
-
销毁节点并关闭接口
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from learning_interface.srv import AddTwoInts # 自定义的服务接口class adderServer(Node):
def init(self, name):
super().init(name) # ROS2节点父类初始化
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.adder_callback) # 创建服务器对象(接口类型、服务名、服务器回调函数)def adder_callback(self, request, response): # 创建回调函数,执行收到请求后对数据的处理 response.sum = request.a + request.b # 完成加法求和计算,将结果放到反馈的数据中 self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b)) # 输出日志信息,提示已经完成加法求和计算 return response # 反馈应答信息def main(args=None): # ROS2节点主入口main函数
rclpy.init(args=args) # ROS2 Python接口初始化
node = adderServer("service_adder_server") # 创建ROS2节点对象并进行初始化
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
5. 创建动作服务端
import time
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from rclpy.action import ActionServer # ROS2 动作服务器类
from learning_interface.action import MoveCircle # 自定义的圆周运动接口
class MoveCircleActionServer(Node):
def __init__(self, name):
super().__init__(name) # ROS2节点父类初始化
self._action_server = ActionServer( # 创建动作服务器(接口类型、动作名、回调函数)
self,
MoveCircle,
'move_circle',
self.execute_callback)
def execute_callback(self, goal_handle): # 执行收到动作目标之后的处理函数
self.get_logger().info('Moving circle...')
feedback_msg = MoveCircle.Feedback() # 创建一个动作反馈信息的消息
for i in range(0, 360, 30): # 从0到360度,执行圆周运动,并周期反馈信息
feedback_msg.state = i # 创建反馈信息,表示当前执行到的角度
self.get_logger().info('Publishing feedback: %d' % feedback_msg.state)
goal_handle.publish_feedback(feedback_msg) # 发布反馈信息
time.sleep(0.5)
goal_handle.succeed() # 动作执行成功
result = MoveCircle.Result() # 创建结果消息
result.finish = True
return result # 反馈最终动作执行的结果
def main(args=None): # ROS2节点主入口main函数
rclpy.init(args=args) # ROS2 Python接口初始化
node = MoveCircleActionServer("action_move_server") # 创建ROS2节点对象并进行初始化
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
6. 创建动作客户端
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from rclpy.action import ActionClient # ROS2 动作客户端类
from learning_interface.action import MoveCircle # 自定义的圆周运动接口
class MoveCircleActionClient(Node):
def __init__(self, name):
super().__init__(name) # ROS2节点父类初始化
self._action_client = ActionClient( # 创建动作客户端(接口类型、动作名)
self, MoveCircle, 'move_circle')
def send_goal(self, enable): # 创建一个发送动作目标的函数
goal_msg = MoveCircle.Goal() # 创建一个动作目标的消息
goal_msg.enable = enable # 设置动作目标为使能,希望机器人开始运动
self._action_client.wait_for_server() # 等待动作的服务器端启动
self._send_goal_future = self._action_client.send_goal_async( # 异步方式发送动作的目标
goal_msg, # 动作目标
feedback_callback=self.feedback_callback) # 处理周期反馈消息的回调函数
self._send_goal_future.add_done_callback(self.goal_response_callback) # 设置一个服务器收到目标之后反馈时的回调函数
def goal_response_callback(self, future): # 创建一个服务器收到目标之后反馈时的回调函数
goal_handle = future.result() # 接收动作的结果
if not goal_handle.accepted: # 如果动作被拒绝执行
self.get_logger().info('Goal rejected :(')
return
self.get_logger().info('Goal accepted :)') # 动作被顺利执行
self._get_result_future = goal_handle.get_result_async() # 异步获取动作最终执行的结果反馈
self._get_result_future.add_done_callback(self.get_result_callback) # 设置一个收到最终结果的回调函数
def get_result_callback(self, future): # 创建一个收到最终结果的回调函数
result = future.result().result # 读取动作执行的结果
self.get_logger().info('Result: {%d}' % result.finish) # 日志输出执行结果
def feedback_callback(self, feedback_msg): # 创建处理周期反馈消息的回调函数
feedback = feedback_msg.feedback # 读取反馈的数据
self.get_logger().info('Received feedback: {%d}' % feedback.state)
def main(args=None): # ROS2节点主入口main函数
rclpy.init(args=args) # ROS2 Python接口初始化
node = MoveCircleActionClient("action_move_client") # 创建ROS2节点对象并进行初始化
node.send_goal(True) # 发送动作目标
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
四、ROS 2 调度器
单线程执行器(SingleThreadedExecutor) 的底层的运行机制使得回调函数默认不由新线程运行,而是由 rclpy.spin(node) 所在的唯一主线程运行。因此读写共享变量时,完全不需要加锁,也不存在数据竞争。
rclpy.spin(node) 死循环
│
▼
┌──────────────────────────────────┐
│ 1. 轮询 DDS 底层事件网络通知 │
└─────────────────┬────────────────┘
│
┌───────────────────┴───────────────────┐
▼ ▼
收到 Image 话题数据 收到 GetObjectPosition 服务请求
│ │
▼ ▼
将 listener_callback 压入队列 将 object_position_callback 压入队列
│ │
└───────────────────┬───────────────────┘
│
▼
┌──────────────────────────────────┐
│ 2. 从执行队列中按 FIFO 取出任务 │
└─────────────────┬────────────────┘
│
┌──────────────────┴──────────────────┐
│ 逐个串行执行(单线程) │
└──────────────────┬────────────────┘
│
┌───────────────────────────┴───────────────────────────┐
▼ ▼
[执行 listener_callback] [执行 object_position_callback]
├─ 图像转换 ├─ 读取 self.objectX / Y
├─ object_detect() ├─ 构造 response 消息
└─ 写 self.objectX = 120 (完成) └─ return response (完成)
│ │
└───────────────────────────┬───────────────────────────┘
│
▼
(等待并处理下一个事件)
什么时候 ROS 2 节点才"必须加锁?
场景 1:显式改用了多线程执行器(MultiThreadedExecutor)
from rclpy.executors import MultiThreadedExecutor
def main(args=None):
rclpy.init(args=args)
node = ImageSubscriber("service_object_server")
# 显式开启多线程执行器(例如分配 4 个线程)
executor = MultiThreadedExecutor(num_threads=4)
executor.add_node(node)
# 此时,两个回调函数可能分别被 线程A 和 线程B 同时拉起执行!
executor.spin()
场景 2:自己在节点里手动开辟了 Python 原生子线程
如果在节点内部用 threading.Thread(target=...) 启动了一个独立的后台线程去跑耗时算法或串口通信,这个子线程与 rclpy.spin() 的主线程是并发运行的,此时读写共享变量必须加锁。