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 模型(接收方如何知道共享域中有了自己需要的数据)
其发现步骤简单表示为:
- 域主体通过 SPDP 相互发现
- 通过 SEDP 交换写入器和读取器的详情
- 读取器根据话题的类型、QoS 匹配写入器
- 建立虚拟连接
- 新数据到达读取器缓存
- 通过 Listener/WaitSet/Polling 等方法通知应用程序
DDS 的核心是 DCPS (Data-Centric Publish-Subscribe) 模型,包含以下关键实体:
- 域主体(Domain Participant): 代表加入特定域的一个应用程序,它是创建其他 DDS 实体的工厂,在 ROS 2 中,通常一个 ROS 2 节点对应一个或多个 Domain Participant。
- 主题(Topic): 定义通信的数据类型和主题名称,关联一个数据类型 (在 DDS 中由 IDL 定义,在 ROS 2 中由 .msg/.srv/.action 文件定义) 和一个字符串名称 (如 "/chatter"),Topic 是发布者和订阅者匹配的桥梁。
- 发布者(Publisher): 由域主体创建,用于发布数据,一个 Publisher 可以管理多个 写入器。
- 写入器(Data Writer): 由 发布者创建,绑定到一个特定的 主题,应用程序通过写入器的 write() 方法将数据发送到网络中。
- 订阅者(Subscriber): 由域主体创建,用于接收数据,一个 订阅者可以管理多个 读取器。
- 读取器(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 模式的保证机制
- 缓存: 写入器发送数据时,会先将数据样本缓存在本地(根据 History 策略,如保留最新的 N 个)。
- 序列号: 每个数据样本都被赋予一个唯一的、递增的序列号。
- 确认 (ACKs) / 否定确认 (NACKs): 读取器收到数据后,会向对应的 写入器发送确认消息 (ACK),告知它哪些序列号的数据已成功接收。
- 心跳 (Heartbeat): 写入器会定期向读取器发送心跳消息,包含它已发送的最新序列号范围。
- 检测丢失:
如果 读取器在预期时间内没有收到某个序列号的数据(通过心跳得知它应该存在),它会向 写入器发送一个 NACK 消息,请求重传丢失的样本。
如果 写入器在一定时间内没有收到某个序列号样本的 ACK(可能是 ACK 丢失或数据丢失),它会主动重传该样本(基于本地缓存)。 - 重传: 收到 NACK 或检测到 ACK 缺失时,写入器从本地缓存中取出对应的样本并重新发送。
- 流量控制: 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 米",赋予序列号 #101,并缓存它(在最新的 10 个指令缓存中)。
- 同时发送心跳,告知读取器它已发送到序列号 #101。
- 如果没收到序列号 #101 的 ACK(可能网络延迟或包丢失),接收方 (读取器) 通过心跳发现它没收到 #101(它只收到到 #100),于是发送 NACK 请求 #101;或者,发送方心跳计时器超时未收到 #101 的 ACK。
- 发送方收到 NACK 或检测到 ACK 超时,从缓存中取出 #101 指令,重新发送。
- 接收方收到重传的 #101 指令,处理它(让机器人前进),并发送 ACK 确认 #101。
- 发送方收到 #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 流量) |
适用场景 | 高频状态更新 (传感器读数、图像帧、非关键状态)、实时性要求高 | 关键命令/控制 (运动指令、配置更新、任务指令)、必须保证送达 |