ROS2中分布式通信原理详解

ROS2 的通信原理基于采用分布式架构的 DDS(Data Distribution Service) 中间件,是一种以数据为中心的发布-订阅模型,其核心原理是节点之间通过 共享域(Domain) 实现分布式通信,进行数据交换,无需中心节点(ROS1 的 Master)。

  • 域(Domain) :DDS 通信发生在一个逻辑隔离的域内,只有加入同一个 Domain ID 的 DDS 参与者才能相互通信,相当于 ROS 2 网络的一个逻辑隔离层,所有节点必须加入相同的域(通过域ID标识),才能在同一逻辑空间内通信,域相当于一个全局数据池,节点在此空间内发布或订阅数据 。
  • 数据为中心 :DDS 不关注节点本身,而是关注数据的生成与消费,发布者将数据写入域,订阅者根据主题(Topic)匹配并读取数据,无需直接连接发送方。
  • 去中心化 :节点通过内置的发现机制(RTPS协议)自动发现彼此。

1. QoS 策略(如何保证信息不丢失)

ROS2 节点在同一个域内自动发现彼此,发送方将数据发布到域中,接收方从域中订阅数据,形成类似数据池的通信模型,ROS2 通过 DDS 提供的 QoS 策略配置来保证可靠性,核心策略是 RELIABILITY,默认行为是尽力而为 (BEST_EFFORT),类似 UDP,不保证送达, 要实现不丢失,必须显式配置,关键策略包括:

  • 可靠性策略(ReliabilityQosPolicy) :分为 Reliable(可靠)和 Best-Effort(尽力而为),其中,BEST_EFFORT发送方不关心接收方是否收到,也不重传,可能丢失数据,适用于实时性要求高的场景(如视频流);RELIABLE发送方保证发送的数据最终会被所有匹配的、活着的、兼容的订阅方接收到,是保证不丢失的基础,可靠模式通过确认机制确保消息不丢失,适用于重要数据(如传感器数据)。
  • 历史策略(HistoryQosPolicy):定义了写入器和读取器如何管理发送/接收到的数据样本的历史记录,其中,KEEP_LAST保留最新的 N 个样本 (由 depth 参数指定),旧样本被覆盖;KEEP_ALL则保留所有已发送但未被所有匹配订阅方确认的样本(需要配合 RELIABLE 和 ResourceLimits 使用,资源消耗大)。
  • 资源限制约束(ResourceLimitsQosPolicy):限制读取器和写入器的缓存的历史样本数量,防止资源耗尽。
  • 持久性策略(DurabilityQosPolicy) :定义历史数据对新加入的订阅者的可见性(包括 VOLATILE, TRANSIENT_LOCAL, TRANSIENT, PERSISTENT),对于确保"不丢失"更重要的是在已有订阅者连接期间的数据可靠性,VOLATILE 通常足够(配合 RELIABLE 和合适的 History),若确保新订阅者能接收到订阅前已发布的数据的更高持久性级别,可以设置为 Transient_Local ,节点会保留历史数据供新订阅者获取。
  • 队列大小(Queue Size) :通过设置消息队列长度(缓冲区),避免因处理延迟导致丢包。例如,订阅端处理速度低于发布频率时,队列可暂存多余消息。
  • 可靠传输 :启用 Reliable 策略时,发送方会等待接收方确认消息接收成功,否则重新发送。
  • 队列缓冲 :在发布端和订阅端设置队列大小,防止因处理速度不匹配导致丢包。例如,若消息发布频率为 10Hz,而订阅端处理频率为 5Hz,则队列需至少容纳 2 条消息以避免丢包。
  • 权限策略(OwnershipQosPolicy / OwnershipStrengthQosPolicy): 解决多个发布者发布同一主题时的冲突(独占或共享)。间接影响哪个发布者的数据被最终接收。

2. DCPS 模型(接收方如何知道共享域中有了自己需要的数据)

其发现步骤简单表示为:

  1. 域主体通过 SPDP 相互发现
  2. 通过 SEDP 交换写入器和读取器的详情
  3. 读取器根据话题的类型、QoS 匹配写入器
  4. 建立虚拟连接
  5. 新数据到达读取器缓存
  6. 通过 Listener/WaitSet/Polling 等方法通知应用程序

DDS 的核心是 DCPS (Data-Centric Publish-Subscribe) 模型,包含以下关键实体:

  1. 域主体(Domain Participant): 代表加入特定域的一个应用程序,它是创建其他 DDS 实体的工厂,在 ROS 2 中,通常一个 ROS 2 节点对应一个或多个 Domain Participant。
  2. 主题(Topic): 定义通信的数据类型和主题名称,关联一个数据类型 (在 DDS 中由 IDL 定义,在 ROS 2 中由 .msg/.srv/.action 文件定义) 和一个字符串名称 (如 "/chatter"),Topic 是发布者和订阅者匹配的桥梁。
  3. 发布者(Publisher): 由域主体创建,用于发布数据,一个 Publisher 可以管理多个 写入器。
  4. 写入器(Data Writer): 由 发布者创建,绑定到一个特定的 主题,应用程序通过写入器的 write() 方法将数据发送到网络中。
  5. 订阅者(Subscriber): 由域主体创建,用于接收数据,一个 订阅者可以管理多个 读取器。
  6. 读取器(Data Reader): 由订阅者创建,绑定到一个特定的 主题,应用程序通过读取器读取或监听接收到的数据。

读取器 (DataReader) 不是被动等待广播通知,而是通过 DDS 的自动发现机制来动态感知和匹配写入器 (DataWriter),这个过程是完全去中心化的,没有主节点协调。核心步骤如下:

2.1 SPDP (Simple Participant Discovery Protocol)参与者发现协议:

当域主体加入一个域时,它会周期性地在网络上广播 (通常是组播) 一条 SPDP 消息,宣告自己的存在,这条消息包含域主体的全局唯一标识符 (GUID) 和它所支持的 QoS 策略等基本信息,同时,它也会监听来自同一域中其他 域主体的 SPDP 消息,最终,域内所有域主体相互发现并建立起彼此存在的认知。

2.2 SEDP (Simple Endpoint Discovery Protocol)终端发现协议:

一旦两个域主体 (A 和 B) 通过 SPDP协议发现了彼此,它们之间就会建立 SEDP 通信通道。域主体A 会通过 SEDP协议 向 域主体B 发送消息,详细描述 A 创建的所有写入器和读取器的信息(包括它们绑定的话题名称、数据类型、GUID、详细的 QoS 设置);同样,B 也会向 A 发送自己的端点信息。当读取器接收到一个远端写入器 的 SEDP 通告时,它会进行严格的匹配检查:

  • Topic 名称: 必须完全一致。
  • 数据类型: 必须兼容(在 DDS 中通常要求严格相等,ROS 2 通过类型哈希验证)。
  • QoS 兼容性: 读取器要求的 QoS 和写入器能提供的 QoS 必须兼容。例如,读取器要求 RELIABLE,写入器也必须提供 RELIABLE 才能匹配;读取器要求的 DEADLINE 必须小于等于写入器能提供的期限等,如果不兼容,则不会建立连接。
  • 匹配成功的写入器和读取器建立起直接的虚拟连接。DataReader 现在确切地知道哪些 DataWriter 会发送它感兴趣的数据。

2.3 数据到达感知常见的几种模式:

  • 监听器 (Listener)是最常见的方式,应用程序为读取器注册一个监听器回调函数,当新数据样本到达读取器的本地缓存时,DDS 中间件会自动调用这个回调函数,通知应用程序有新数据可用。这是事件驱动模型,高效且实时性好。

  • 轮询 (Polling): 应用程序可以主动调用读取器的 take() 或 read() 方法去检查是否有新数据到达,take() 会取走数据(缓存中移除),read() 读取数据但保留在缓存中。这种方式效率较低,通常用于特定场景。

  • 等待(WaitSet):应用程序创建一个 WaitSet 对象,将读取器的 ReadCondition(表示有新数据)附加到 WaitSet 上,然后调用 wait() 方法阻塞,直到任何附加的条件(如新数据到达)被触发,这是一种阻塞式的同步等待机制。

3. 机制说明

3.1 RELIABLE 模式的保证机制

  1. 缓存: 写入器发送数据时,会先将数据样本缓存在本地(根据 History 策略,如保留最新的 N 个)。
  2. 序列号: 每个数据样本都被赋予一个唯一的、递增的序列号。
  3. 确认 (ACKs) / 否定确认 (NACKs): 读取器收到数据后,会向对应的 写入器发送确认消息 (ACK),告知它哪些序列号的数据已成功接收。
  4. 心跳 (Heartbeat): 写入器会定期向读取器发送心跳消息,包含它已发送的最新序列号范围。
  5. 检测丢失:
    如果 读取器在预期时间内没有收到某个序列号的数据(通过心跳得知它应该存在),它会向 写入器发送一个 NACK 消息,请求重传丢失的样本。
    如果 写入器在一定时间内没有收到某个序列号样本的 ACK(可能是 ACK 丢失或数据丢失),它会主动重传该样本(基于本地缓存)。
  6. 重传: 收到 NACK 或检测到 ACK 缺失时,写入器从本地缓存中取出对应的样本并重新发送。
  7. 流量控制: DDS 内部有流量控制机制(如基于令牌桶),防止发送方压垮接收方或网络。当接收方处理不过来时,会减慢 ACK 的发送速度或请求发送方降低速率,避免因接收方缓存溢出导致数据丢失。

3.2 为什么需要 History (特别是 depth):

RELIABLE 依赖重传,重传需要源数据。

KEEP_LAST + depth=N:保证写入器本地至少缓存了最新的 N 个样本,如果丢失发生在最近的 N 个样本内,就能重传。如果 N 太小,且网络延迟大/丢包率高,较早的数据可能在需要重传前就被覆盖掉,导致无法重传而丢失。

KEEP_ALL:理论上缓存所有未确认的样本,确保只要订阅方还活着且需要,就能重传,但资源消耗可能巨大,需要谨慎设置 ResourceLimits。

4. 示例说明

4.1 场景 1:温度传感器数据 (允许偶尔丢失)

  • 需求: 实时显示当前温度,偶尔丢失一两个读数不影响大局,低延迟更重要。
  • ROS 2 建议配置 (DDS QoS):
    • Reliability: BEST_EFFORT
    • History: KEEP_LAST, depth=1 (只需缓存最新值)
    • Durability: VOLATILE (新订阅者不需要历史温度)
  • 发送方行为: 传感器节点 (写入器) 不断发布最新温度值,只发送一次,不缓存历史值(depth=1,新值覆盖旧值),也不关心是否送达,速度快,资源占用少。
  • 接收方行为: 显示节点 (读取器) 收到新温度就显示,它知道可能收不到所有数据,但能显示最新收到的,如果网络抖动丢了一个包,显示就跳过那一次更新,等下个包。
  • 结果: 高效、低延迟,但可能丢失数据。适用于非关键的状态信息。

4.2 场景 2:机器人运动控制指令 (绝对不允许丢失)

  • 需求: 发送给机器人的"前进 1 米"指令必须送达,丢失指令可能导致机器人行为错误或事故。
  • ROS 2 建议配置 (DDS QoS):
    • Reliability: RELIABLE
    • History: KEEP_LAST, depth=10 (或根据指令频率和网络状况合理设置,比如能覆盖几秒内的指令)
    • Durability: VOLATILE (新控制器加入不需要历史指令)
  • 发送方行为:
    1. 控制节点 (写入器) 发送指令 "前进 1 米",赋予序列号 #101,并缓存它(在最新的 10 个指令缓存中)。
    2. 同时发送心跳,告知读取器它已发送到序列号 #101。
    3. 如果没收到序列号 #101 的 ACK(可能网络延迟或包丢失),接收方 (读取器) 通过心跳发现它没收到 #101(它只收到到 #100),于是发送 NACK 请求 #101;或者,发送方心跳计时器超时未收到 #101 的 ACK。
    4. 发送方收到 NACK 或检测到 ACK 超时,从缓存中取出 #101 指令,重新发送。
    5. 接收方收到重传的 #101 指令,处理它(让机器人前进),并发送 ACK 确认 #101。
    6. 发送方收到 #101 的 ACK,知道指令已送达,可以(在缓存策略允许下)释放该缓存的指令。
  • 接收方行为: 监听器收到新指令(如 #101)立即处理,如果检测到序列号不连续(比如收到 #102 但没 #101),发送 NACK 请求缺失的 #101。
  • 结果: 指令最终一定会送达接收方并被处理(只要双方存活且网络最终恢复),代价是潜在的高延迟(重传引入)和更高资源消耗(缓存、ACK/NACK 流量),适用于关键控制命令、配置更新等。

4.3 场景对比

特性 场景 1: 温度传感器 (Best Effort) 场景 2: 运动控制 (Reliable)
可靠性要求 低 (允许丢失) 高 (不允许丢失)
QoS Reliability BEST_EFFORT RELIABLE
QoS History KEEP_LAST, depth=1 KEEP_LAST, depth=N (N > 1,足够覆盖重传需求) / KEEP_ALL
发送方行为 发送一次,不缓存或只缓存最新值,不重传 发送并缓存样本,监听 ACK/NACK,主动或按需重传
接收方行为 只处理收到的数据,不请求缺失数据 处理数据,发送 ACK,检测丢失并发送 NACK 请求重传
数据传输保证 不保证送达 (可能丢失) 保证送达 (只要双方存活且网络最终连通)
延迟 低 (无重传、ACK/NACK 开销) 可能较高 (重传、ACK/NACK 引入延迟)
带宽/资源消耗 较高 (重传流量、缓存占用、ACK/NACK 流量)
适用场景 高频状态更新 (传感器读数、图像帧、非关键状态)、实时性要求高 关键命令/控制 (运动指令、配置更新、任务指令)、必须保证送达