news 2026/5/30 7:44:21

揭秘 DDS原理:无中心、自发现、实时可靠的“分布式神经“

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
揭秘 DDS原理:无中心、自发现、实时可靠的“分布式神经“

在分布式系统的世界里,我们习惯了 HTTP 的请求/响应模式,也熟悉了 Kafka 和 RabbitMQ 那种中心化的消息队列。但在自动驾驶、工业机器人、航空航天等高实时性领域,有一个名字频繁出现——DDS(Data Distribution Service)。

很多人初识 DDS 会觉得它很"神秘":没有 Broker,数据却能在毫秒级送达;程序随便启动,却能自动发现彼此建立连接。这背后的原理究竟是什么?本文将从架构到协议,为你彻底拆解 DDS 的实现原理,并穿插介绍如何用 eProsima 的工具直观观察这些原理的运行。


一、核心哲学:以数据为中心

要理解 DDS,首先要理解它最根本的设计哲学——“以数据为中心”(Data-Centric),这与传统的消息中间件有本质区别。

1.1 两种思维模式的对决

维度消息中心 (Message-Centric)数据中心 (Data-Centric)
代表Kafka, RabbitMQ, MQTTDDS
架构中心化 Broker去中心化 P2P
发送方视角“我把这条消息发给 Topic A”“我把这个值更新到数据空间的 X 位置”
接收方视角“我从 Topic A 消费消息”“我看到数据空间 X 位置的值变了”
数据语义消息流过即消失数据是"有状态的",一直存在于空间中
是否关心对方关心(发到 Broker 即可)不关心(只关心数据本身)

1.2 最恰当的类比:共享白板

邮件系统(MQTT/Kafka) 共享白板(DDS) ───────────────────────── ──────────────────────── 你写好一封信 你走到白板前 ↓ ↓ 交给邮局(Broker) 擦掉旧值,写上你的新状态 ↓ ↓ 邮局通知收信人来取 路过的人看一眼白板 ↓ ↓ 收信人拿到信,看完就扔 就知道最新情况,不用等人通知

在共享白板模式下:

  • 你不需要知道谁在看白板——你只管写
  • 看白板的人不需要知道谁写的——只管看最新值
  • 白板上的信息是持久存在的——随时路过随时看
  • 完全不需要中介——大家直接围在白板前

1.3 DDS 的"白板"叫什么?

DDS 构建了一个抽象的全局数据空间(Global Data Space, GDS)。每个"白板位置"就是一个Topic。发布者把数据更新到 Topic 上,订阅者通过 Topic 名关注这个位置的变化。

关键词:Data-Centric、Global Data Space、Topic


二、四大核心概念

DDS 的一切都围绕 4 个基本概念展开。理解它们,就理解了 DDS 的骨架。

2.1 Domain——逻辑隔离的"微信群"

Domain 0 Domain 1 ┌─────────────────┐ ┌─────────────────┐ │ Participant A │ │ Participant C │ │ Participant B │ │ Participant D │ │ ↑互相可见↑ │ │ ↑互相可见↑ │ │ A 看不到 C, D │ │ C 看不到 A, B │ └─────────────────┘ └─────────────────┘
  • Domain 通过Domain ID(0-232 的整数)区分
  • 不同 Domain 完全隔离,互不干扰
  • 同一个进程可以同时加入多个 Domain
  • 作用:网络分区隔离、多租户、安全性

2.2 Participant——“群成员”

Participant 是 DDS 网络中的一个节点。一个进程可以有一个或多个 Participant。

每个 Participant 持有:

  • GUID(Globally Unique Identifier)— 全球唯一标识
    • 结构:HostId + AppId + ObjectId(共 16 字节)
    • 例如:01.0f.001122334455.0a1b2c3d.0.0.1
    • 相当于 DDS 世界的 MAC 地址 + PID
  • IP 地址和端口— 用于网络通信
  • QoS 声明— 自己支持哪些服务质量

调试技巧:运行fastddsspy.exe后输入participants命令,会列出当前 Domain 中所有 Participant 的 GUID 和名称。你可以看到每个节点的"身份证"。

2.3 Topic——“白板的位置”

Topic 是数据发布和订阅的通道名称。发布者和订阅者通过 Topic 名称来匹配。

Topic 的三要素: ┌─────────────────────────────┐ │ 名称: "Square" │ ← 频道名(必须一致才能通信) │ 类型: "ShapeType" │ ← 数据结构(必须兼容) │ QoS: RELIABLE + KEEP_LAST 5│ ← 行为规则(必须兼容) └─────────────────────────────┘

一个 Topic 可以关联多个 Writer 和 Reader。同名的 Topic 在不同进程间构成一个逻辑数据通道

2.4 Writer & Reader——“真正读写白板的人”

角色类比例子说明
DataWriter在白板上写字的人发布数据到 Topic
DataReader看白板的人从 Topic 订阅数据

关键点:

  • 一个 Publisher 可以有多个 Writer(一个 Topic 一个)
  • 一个 Subscriber 可以有多个 Reader(一个 Topic 一个)
  • Writer 和 Reader通过 Topic 匹配,不直接连接
  • 一个 Writer 可以匹配多个 Reader;一个 Reader 可以匹配多个 Writer
进程 A 进程 B ┌──────────┐ ┌──────────┐ │ Publisher│ │Subscriber│ │ ┌──────┐ │ │ ┌──────┐ │ │ │Writer│─┼─── Topic ────┼─│ Reader│ │ │ └──────┘ │ │ └──────┘ │ └──────────┘ └──────────┘

调试技巧:在fastddsspy.exe中输入endpoints,可以看到当前 Domain 中所有的 Writer 和 Reader 的详细列表,包括它们关联的 Topic 名称和 GUID。


Domain/Participant的概念

• 这两个概念是整个 DDS 的基础中的基础,也是初学者最容易混淆的地方。

———

一句话直击本质

概念 一句话定义
━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Domain DDS 世界的"隔离区"——只有同 Domain 的节点才能看到彼此
───────────── ─────────────────────────────────────────────────────────────────────────────────────────
Participant DDS 世界的"入口/容器"——一个进程需要先创建一个 Participant,才能在这个 Domain 里做任何事

———

先理解 Domain:比作"微信群"

微信里有多个群:

┌─ 技术交流群 (Domain 100) ─┐
│ 张三 │ 李四 │
│ 王五 │ …… │
└───────────────────────────┘

┌─ 家庭群 (Domain 200) ─────┐
│ 张三 │ 张三妈妈 │
│ 张三爸爸 │ …… │
└───────────────────────────┘

张三同时在两个群里。
他在技术群发的消息,家人看不到;
他在家庭群发的消息,同事看不到。

Domain = 微信群。 就是这么回事。

DDS 里的 Domain 通过一个 Domain ID(整数,0-232) 区分。

Participant A (Domain 0) Participant B (Domain 0)
可以互相看到 ✓

Participant A (Domain 0) Participant C (Domain 1)
完全隔离,谁也看不见谁 ✗

关键是:

  • 同一个进程可以加入多个 Domain(像张三同时在两个群)
  • 不同 Domain 完全隔离——没有数据泄漏,没有广播风暴
  • 多租户部署就用不同 Domain 隔开

———

再理解 Participant:比作"手机登录微信"

Participant 可以理解为**“一个人用他的手机登录了微信群”**。

┌─ 微信 App (进程) ──────────┐ │ │ │ 登录账号 A (Participant 1) │ ← 可以加群 │ 登录账号 B (Participant 2) │ ← 可以加群 │ │ └───────────────────────────┘

对应到 DDS:

一个 C++ 进程:
┌──────────────────────────────────┐
│ participant1 = factory->create_participant(0, …) │ ← 加入 Domain 0
│ participant2 = factory->create_participant(1, …) │ ← 加入 Domain 1
│ participant3 = factory->create_participant(0, …) │ ← 又加入 Domain 0
└──────────────────────────────────┘

Participant 就是 DDS 中一切活动的"容器"。在你创建 Participant 之后,你才能在它上面:

participant
├── register_type() ← 注册数据类型
├── create_topic() ← 创建 Topic
├── create_publisher() ← 创建发布者
├── create_subscriber() ← 创建订阅者
└── … ← 所有 DDS 操作都从 Participant 开始

———

用一个真实例子串起来

进程 = 你的电脑上运行的一个 .exe 程序

场景:一个自动驾驶系统

┌─ 电脑 A:感知模块 ──────────────────────────────┐
│ │
│ Participant “Perception” (Domain 0) │
│ ├── Publisher “LidarScan” │
│ │ └── Writer → Topic “scan” │
│ ├── Publisher “CameraImage” │
│ │ └── Writer → Topic “image” │
│ └── Subscriber “ControlCmd” │
│ └── Reader ← Topic “cmd_vel” │
└──────────────────────────────────────────────────┘

┌─ 电脑 B:规划控制模块 ───────────────────────────┐
│ │
│ Participant “Planner” (Domain 0) │
│ ├── Subscriber “LidarHandler” │
│ │ └── Reader ← Topic “scan” │ ← 自动匹配上 A 的 Writer
│ ├── Subscriber “CameraHandler” │
│ │ └── Reader ← Topic “image” │ ← 自动匹配上 A 的 Writer
│ └── Publisher “ControlOutput” │
│ └── Writer → Topic “cmd_vel” │ ← A 自动订阅这个
└──────────────────────────────────────────────────┘

┌─ 电脑 C:调试电脑(仅监控) ─────────────────────┐
│ │
│ Participant “Monitor” (Domain 0) │
│ └── Fast DDS Spy 自动加入 │
│ → 能看到所有 Topics 和数据 │
└──────────────────────────────────────────────────┘

注意:

  • 三台电脑都在 Domain 0 → 可以互相通信
  • 每个电脑启动一个 Participant(名字不同)
  • Participant “Perception” 里有 2 个 Publisher + 1 个 Subscriber
  • Participant “Planner” 里有 2 个 Subscriber + 1 个 Publisher
  • 不需要配置任何 IP 地址——SPDP/SEDP 自动发现匹配

———

容易混淆的 5 个问题

Q1:一个进程可以有几个 Participant?
一个或多个。但通常一个进程只需要一个。

Q2:一个 Participant 可以同时发布和订阅吗?
当然可以。一个 Participant 里同时有 Publisher 和 Subscriber 是很常见的(比如上面的规划模块)。

Q3:不同进程里的 Participant 名字相同会怎样?
没事。名字只是给人看的标签,DDS 内部靠 GUID 区分(所以名字重复没关系)。

Q4:Participant 加入 Domain 后做了什么?
就两件事:

  1. 发多播告诉所有人"我来了"(SPDP:我叫什么、IP 在哪、开了哪些端口)
  2. 监听别人的多播,看还有谁在(SPDP:谁加入了、谁离开了)

Q5:Participant 可以中途换 Domain 吗?
不行。Participant 创建时指定 Domain ID,之后就固定了。想加入另一个 Domain 就再创建一个 Participant。

———

一句话记忆

  • Domain = 微信群(隔离不同业务的数据)
  • Participant = 你(容器/入口,持有 GUID 身份)
  • 创建 Participant 之后,才能在这个 Domain 里做发布/订阅/发现等一切操作

三、自动发现:如何找到彼此

DDS 最令人惊叹的特性之一是零配置自动发现——你不需要配置 IP 地址、不需要启动中心化服务,程序启动后自动找到彼此。这背后是两层发现协议在工作。

3.1 SPDP:参与方发现(“这里有哪些人?”)

SPDP(Simple Participant Discovery Protocol)解决第一个问题:这个网络上还有谁?

工作流程
Participant A(刚启动) Participant B(已运行) │ │ │ ① 多播宣告自己存在 │ │--- DATA(p) ──────────────────→ │ │ 多播地址: 239.255.0.0 │ │ 端口: 7400 + domain*2 │ │ │ │ ② B 收到后回复自己的信息 │ │←── DATA(p) ──────────────────── │ │ │ │ ③ 双方都知道了对方的存在 │ │ 存入本地的 Participant 列表 │ │ 之后 A 不再发多播,改为直接单播 │
DATA§ 报文中包含什么?
DATA(p) 报文 ≈ DDS 世界的"自我介绍名片" ┌─────────────────────────────────┐ │ GUID: 01.0f.xxxx...xx.0.0.1 │ ← 唯一身份 ID │ Vendor ID: eProsima │ ← 哪个厂商的实现 │ Protocol Version: 2.5 │ ← RTPS 版本 │ IP 地址: 192.168.1.100 │ ← 通信地址 │ 端口号: 7410 │ ← 通信端口 │ 存活周期: 30 秒 │ ← "我每隔 30 秒喊一声" │ QoS 能力: │ ← 支持哪些策略 │ - RELIABLE │ │ - TRANSIENT_LOCAL │ │ - ... │ └─────────────────────────────────┘
租约机制(Lease):如何检测节点离开?

DDS 的节点可能随时离开(崩溃、网络断开)。SPDP 通过租约机制检测:

正常情况(每 30 秒一次心跳): A ─── DATA(p) ───→ B ("我还活着") A ─── DATA(p) ───→ B ("我还活着") A ─── DATA(p) ───→ B ("我还活着") 异常情况(A 崩溃了): A ──── ✗ ───────→ B (心跳停了) B 等待 2.5 × 30 秒 = 75 秒 B 判定 A 已离开,从列表中移除

调试技巧:用fastddsspy.exe启动后,participants列出的就是通过 SPDP 发现的所有 Participant。你可以在另一个终端启动或关闭一个 DDS 程序,观察 Spy 中列表的实时变化——这就是 SPDP 租约机制在工作的直接体现。

GUID 结构的秘密
GUID = 16 字节 = HostId(4) + AppId(4) + ObjectId(8) HostId: 通常来自 MAC 地址或随机生成 AppId: 区分同一主机上的不同进程 ObjectId: 区分同一进程内的不同实体 所以两个进程在同一台机器上运行时: GUID 的前 4 字节相同(同一 MAC) GUID 的中间 4 字节不同(不同进程)

3.2 SEDP:端点匹配(“我们能合作吗?”)

知道对方 Participant 存在后,SEDP(Simple Endpoint Discovery Protocol)解决第二个问题:我们能合作吗?

工作流程
Participant A Participant B │ │ │ ① 单播交换端点信息 │ │--- DATA(w) ──────────────────→ │ │ "我发布 Topic 'Square' │ │ 类型: ShapeType │ │ QoS: RELIABLE, KEEP_LAST 5" │ │ │ │←── DATA(r) ──────────────────── │ │ "我订阅 Topic 'Square' │ │ 类型: ShapeType │ │ QoS: RELIABLE, KEEP_ALL" │ │ │ │ ② DDS 内核自动执行匹配算法 │ │ │ │ ③ 匹配成功 → 建立数据通道 │ │══════ RTPS 数据流 ════════════→ │
匹配算法三要素

DDS 内核会逐项比对以下三个条件,全部通过才建立连接

匹配条件判定规则失败例子
Topic 名称必须完全一致Writer 发布 “Square”,Reader 订阅 “Circle” ✗
数据类型必须兼容(类型名一致)Writer 用 ShapeType,Reader 用 ImageType ✗
QoS 兼容性发布端和订阅端 QoS 必须能兼容Writer RELIABLE,Reader BEST_EFFORT ✗

QoS 兼容性矩阵(最常用的 Reliability):

Writer ↓ / Reader → RELIABLE BEST_EFFORT ────────────────────────────────────────────── RELIABLE ✓ 兼容 ✗ 不兼容 BEST_EFFORT ✓ 兼容 ✓ 兼容

简单记忆:更严格的 Writer 要求 Reader 也严格;更宽松的 Writer 兼容一切

调试技巧

  1. fastddsspy.exe中用topics查看当前所有 Topic——你能看到 SEDP 交换后的结果
  2. endpoints可以看到每个 Writer/Reader 的 QoS 设置
  3. 如果两个进程匹配不上,先检查 Topic 名称是否完全一致(包括大小写),再用 Spy 确认两端都能发现对方的存在

3.3 发现协议总结

时间线: Participant 启动 │ ▼ SPDP:发多播 DATA(p) 到 239.255.0.x:7400+ "我来了!这是我的 GUID 和地址" │ ▼ 收到其他 Participant 的 DATA(p) 回复 互相知道"有人在" │ ▼ SEDP:单播交换 DATA(w) 和 DATA(r) "我发布 XXX" / "我订阅 XXX" │ ▼ DDS 内核自动匹配: Topic 名称 ✓ 类型 ✓ QoS ✓ → 建通道 Topic 名称 ✓ 类型 ✓ QoS ✗ → 不建通道 名称或类型不匹配 → 不建通道 │ ▼ 匹配成功 → 开始传输数据(RTPS)

四、RTPS 协议:数据如何高效可靠传输

发现和匹配完成只是第一步。DDS 真正的核心在于 RTPS(Real-Time Publish-Subscribe Protocol)——定义了数据如何在 UDP 这种不可靠的传输层上实现可靠、实时、高效的传输

4.1 History Cache:DDS 的"本地 Git 仓库"

每个 DataWriter 和 DataReader 在内存中维护着一个历史数据缓冲区,叫 History Cache。

┌───────── DataWriter ─────────┐ │ History Cache │ │ ┌───┬───┬───┬───┬───┬───┐ │ │ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ │ ← 最近发送的样本 │ └───┴───┴───┴───┴───┴───┘ │ │ Next SN: 7 │ └──────────────────────────────┘ ┌───────── DataReader ─────────┐ │ History Cache │ │ ┌───┬───┬───┬───┬───┬───┐ │ │ │ 1 │ 2 │ │ 4 │ 5 │ 6 │ │ ← SN=3 丢了! │ └───┴───┴───┴───┴───┴───┘ │ │ Missing: [3] │ └──────────────────────────────┘

每个样本(Sample)都带一个递增的Sequence Number (SN),类似于 TCP 的序列号:

  • SN=1→ 第一个样本
  • SN=2→ 第二个样本
  • SN=k→ 第 k 个样本

Cache 的深度由History QoS控制:

  • KEEP_LAST N→ 只缓存最近 N 个样本
  • KEEP_ALL→ 缓存所有样本(直到内存耗尽)

类比 Git:Writer Cache ≈ 你的本地提交历史,Reader Cache ≈ 别人拉取后的本地仓库。丢了的 commit 就是 missing SN,需要 git fetch(NACK)来补上。

4.2 Heartbeat & ACKNACK:UDP 上的可靠传输

UDP 本身是不可靠的——数据包可能丢失、乱序、重复。RTPS 在 UDP 之上实现了一整套可靠机制。

正常流程(无丢包)
Writer Reader │ │ │── DATA(SN=1, temp=25.5) ──────────────→│ ✓ 收到,存入 Cache │── DATA(SN=2, temp=25.6) ──────────────→│ ✓ 收到 │── DATA(SN=3, temp=25.4) ──────────────→│ ✓ 收到 │ │ │── HEARTBEAT(SN=3) ────────────────────→│ 确认 "最新 SN=3" │←─ ACK(SN=1~3全部收到) ────────────────│ 回复 "全收到了"
丢包处理(重传流程)
Writer Reader │ │ │── DATA(SN=1) ─────────────────────────→│ ✓ │── DATA(SN=2) ─────────────────────────→│ ✗ 网络丢包! │── DATA(SN=3) ─────────────────────────→│ ✓ │ │ │── HEARTBEAT(SN=3) ───────────────────→│ "我最新到 SN=3" │←─ NACK(SN=2) ─────────────────────────│ "SN=2 我没收到,请重发" │ │ │── DATA(SN=2, 重传) ──────────────────→│ ✓ 终于收到了! │ │ │←─ ACK(SN=1~3全部收到) ───────────────│ "齐了"
与 TCP 的对比
特性TCPRTPS (DDS)
连接需要建立连接无连接,基于 UDP
重传触发超时 + 重复 ACKHeartbeat + NACK(主动查询)
选择性重传不支持(只能重传第一个丢失的)支持(可以指定重传任意 SN)
多对多只支持 1 对 1原生支持 1 对 N
发送方状态每个连接一个状态每个匹配的 Reader 一个状态

核心洞察:TCP 是为"两台机器间的一个连接"设计的。RTPS 是为"一台机器上的一个 Writer 与 N 台机器上的 M 个 Reader 通信"设计的。这就是为什么 RTPS 选择了 NACK 式重传——Writer 为每个 Reader 维护一个"位图"(哪些 SN 收到了,哪些没收到),然后按需补发。

4.3 完整的 RTPS 子消息体系

RTPS 定义了多种子消息(Submessage),每种有特定用途:

子消息方向用途
DATAWriter → Reader传输用户数据样本
HEARTBEATWriter → Reader告知"我最新到哪个 SN"
ACKNACKReader → Writer确认已收到 / 请求重传丢失的 SN
GAPWriter → Reader告知"SN=5 到 SN=8 被我跳过了,不用等了"
PAD任意填充字节(对齐用途)
DATA_FRAGWriter → Reader大数据分片传输
HEARTBEAT_FRAGWriter → Reader分片传输的心跳

GAP 的妙用:如果 Writer 的 History QoS 设为KEEP_LAST 3,那么当 SN=5 被发送后,SN=2 已经从缓存中被移除了。此时如果有新 Reader 加入请求重传 SN=2,Writer 会发一个 GAP 消息说"SN=2 已经没有了,请跳过它"。这样 Reader 不会死等永远收不到的包。

4.4 传输优化:不只是 UDP

共享内存(SHM)——同机通信的"高速公路"

当两个 Participant 在同一台机器上时,Fast DDS 默认启用共享内存传输

进程 A 进程 B ┌──────────────┐ ┌──────────────┐ │ Writer │ │ Reader │ │ │ │ │ ↑ │ │ ▼ │ │ │ │ │ 序列化 → 写入 │ │ 读取 → 反序列化│ │ 共享内存区域 │ │ 共享内存区域 │ └──────┬───────┘ └──────┬───────┘ │ │ └─────────同一块物理内存────┘ 绕过网络栈,微秒级延迟

SHM 的优势:

  • 延迟:微秒级 vs UDP 的毫秒级(1000 倍差距)
  • 吞吐量:受内存带宽限制,远高于网卡带宽
  • 零拷贝:数据写入共享内存后,Reader 直接读取,无需拷贝

调试技巧:如果运行 DDS 程序时遇到 SHM 错误(“Failed to create segment”),通常是因为进程没有权限写入共享内存文件。解决方案:

  • 代码中指定仅用 UDP:pqos.setup_transports(BuiltinTransports::UDPv4)
  • 或以管理员身份授权:icacls C:\ProgramData\eprosima /grant Users:(OI)(CI)F /T
数据分片与重组

对于超过网络 MTU(通常 1500 字节)的大数据:

原始数据 10KB │ ▼ 分片: ┌────────┬────────┬────────┬────────┬────────┐ │Frag 1 │Frag 2 │Frag 3 │Frag 4 │Frag 5 │ │SN=10.1 │SN=10.2 │SN=10.3 │SN=10.4 │SN=10.5 │ └────────┴────────┴────────┴────────┴────────┘ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ Reader 收到全部 5 个分片后自动重组为原始数据

每个分片都有逻辑端口号 + 片段编号,允许 Reader 即使乱序收到也能正确重组。

多通道智能选择

Fast DDS 可以同时启用多种传输通道,并智能选择最优路径:

Writer → 同时可用: [SHM] [UDPv4] [TCPv4] │ ├─ 同机 Reader → SHM (微秒级) ├─ 局域网 Reader → UDPv4 (毫秒级) └─ 跨网段 Reader → TCPv4 (可靠穿越防火墙)

4.5 延迟与吞吐量的权衡

DDS 通过配置可以在"低延迟"和"高吞吐量"之间权衡:

场景推荐配置预期表现
控制指令RELIABLE, KEEP_LAST 1, SHM微秒级延迟,单条发送
传感器数据BEST_EFFORT, KEEP_LAST 1毫秒级延迟,按帧发送
大文件传输RELIABLE, KEEP_ALL, TCP高吞吐量,确认发送
点云数据BEST_EFFORT + 分片兼顾实时性和吞吐量

调试技巧:Fast DDS Monitor 可以实时显示每个 Topic 的吞吐量曲线(bytes/sec、samples/sec),帮助你观察 QoS 配置的实际效果。


五、QoS:数据的交通规则

如果说 Topic 是数据的"地址",那么 QoS(Quality of Service)就是数据的**“交通规则”**。DDS 提供了 20+ 种 QoS 策略,以下是最核心的 5 种。

5.1 Reliability:丢包了怎么办?

选项快递类比行为
RELIABLE挂号信(签名确认)丢包必重传,直到 Reader 确认为止
BEST_EFFORT普通平信(丢了就算了)丢了不重传,永远只看最新数据

怎么选?

  • 控制指令 →RELIABLE(丢失了车就失控了)
  • 视频流 →BEST_EFFORT(丢一帧没关系,重传的帧到了也已经过时了)

调试技巧:如果跨进程通信收不到数据,检查两端的 Reliability 设置是否兼容。用 Spy 的endpoints命令可以查看每个端点的 QoS。

5.2 Durability:晚来的人能看到历史吗?

选项行为快递类比
VOLATILE晚加入的订阅者看不到之前的数据不留底稿
TRANSIENT_LOCAL晚加入可以看到 Writer Cache 中仍然保留的历史数据保留最近几份底稿
TRANSIENT数据持久化到内存中,即使原 Writer 退出也保留公司档案室
PERSISTENT数据持久化到磁盘,重启不丢失国家档案馆

场景举例

  • 自动驾驶的 GPS 数据 →TRANSIENT_LOCAL(新加入的节点需要知道当前位置)
  • 实时传感器流 →VOLATILE(只看当前值,历史不重要)

5.3 Deadline:数据多久更新一次?

Deadline 定义的是最大更新间隔

设定 Deadline = 100ms Writer 每 50ms 更新一次 → ✓ 正常 Writer 每 150ms 更新一次 → ✗ 违反 Deadline(触发回调通知应用) Writer 停止更新 → ✗ 连续违反 Deadline

典型应用

  • 心跳检测:设定 Deadline = 1 秒,超过 1 秒没收到数据就报警(节点可能挂了)
  • 周期性传感器:Lidar 10Hz = 100ms Deadline,超过说明设备异常

5.4 History:缓存深度控制

选项行为
KEEP_LAST N只缓存最近 N 个样本(N 默认 1)
KEEP_ALL缓存所有样本(需要配合 Resource Limits 防止内存溢出)

场景举例

  • KEEP_LAST 1:传感器数据永远只看最新
  • KEEP_LAST 5:控制指令保留最近 5 条,供新加入节点恢复状态
  • KEEP_ALL:数据记录器,每条都要保留

5.5 Ownership:多人发言听谁的?

当多个 Writer 向同一个 Topic 写数据时,Ownership 决定订阅者听谁的:

选项行为
SHARED所有 Writer 的数据都可见,订阅者都会收到
EXCLUSIVE只有Strength 最高的 Writer 的数据被订阅者接收

典型应用:主备切换

正常时: Writer A (Strength=10) → 主控,订阅者收 A 的数据 Writer B (Strength=5) → 备控,数据被忽略 A 崩溃后: Writer A (死了) → 不再发送 Writer B (Strength=5) → 自动接管,订阅者开始收 B 的数据

5.6 QoS 策略组合实战

一个真正的自动驾驶系统,不同数据流的 QoS 配置:

Topic Reliability Durability History Deadline ───────────────────────────────────────────────────────────────────────────── /control/cmd_vel RELIABLE VOLATILE KEEP_LAST 1 50ms /scan (Lidar) BEST_EFFORT VOLATILE KEEP_LAST 1 100ms /gps/fix RELIABLE TRANSIENT_LOCAL KEEP_LAST 5 200ms /map RELIABLE TRANSIENT_LOCAL KEEP_ALL 1s /camera/image BEST_EFFORT VOLATILE KEEP_LAST 1 33ms (30fps) /vehicle/status RELIABLE TRANSIENT_LOCAL KEEP_LAST 10 1s

调试技巧:如果你的 DDS 应用行为不符合预期(如收不到数据、延迟过高),先用 Fast DDS Spy 确认端点和 Topic 信息,再用 Fast DDS Monitor 查看实时吞吐量曲线,这比猜问题快 10 倍。


六、一次完整的通信旅程

让我们把前面所有的原理串联起来,看一条数据从写入到接收的完整旅程:

时间点 步骤 发生什么 ──────────────────────────────────────────────────── T+0ms ① Publisher 调用 writer->write("Hello") ↓ T+0.1ms ② DDS 序列化:C++ 对象 → CDR 二进制格式 ↓ T+0.2ms ③ RTPS 封包:加头部、分配 SN=42、打时间戳 存入 Writer History Cache (SN=42) ↓ T+0.3ms ④ 传输层调度: 同机有 Reader → SHM 写入共享内存 远程有 Reader → UDP 多播发送 ↓ T+1ms ⑤ Reader 收到数据: SHM: 直接读取共享内存 UDP: 网卡中断 → 内核缓冲区 → 应用缓冲区 ↓ T+1.1ms ⑥ 存入 Reader History Cache (SN=42) ↓ T+1.2ms ⑦ 触发 on_data_available() 回调 ↓ T+1.3ms ⑧ 用户代码读取数据:take_next_sample() ← 得到 "Hello" ↓ T+5ms ⑨ Writer 发送 HEARTBEAT(SN=42) 确认 Reader 回复 ACK(SN=42) ↓ T+100ms ⑩ 下一个周期,重复①~⑨

如果第 4 步的 UDP 包在网络中丢失了:

T+0.3ms ④ DATA(SN=42) 在路由器被丢弃 T+5ms ⑨ HEARTBEAT(SN=42) 到达 Reader Reader 检查 Cache:有 SN=41,没有 SN=42 → 回复 NACK(SN=42) "请重传 SN=42" T+10ms ⑨' Writer 收到 NACK 从 History Cache 取出 SN=42 的副本 重新发送 DATA(SN=42) T+11ms ⑩ Reader 收到重传的 SN=42 存入 Cache,触发回调

这就是 DDS 即使运行在 UDP 上也能保证可靠传输的原因。


七、监控调试工具:看到原理在运行

理解原理是一回事,亲眼看到原理在运行是另一回事。以下工具可以让你观察到前面讲的所有机制:

7.1 Fast DDS Spy——命令行网络侦探

Spy 是一个静默的 DDS Participant,它加入你的 Domain 但不参与数据通信,只监听:

你的应用 A ←→ 你的应用 B ↑ ↑ └── Spy ───────┘ (只监听,不发言)

用 Spy 观察原理:

Spy 命令 观察到的 DDS 原理 ───────────────────────────────────────────── participants SPDP 发现的每个 Participant(GUID、名称) topics SEDP 交换后列出的所有 Topic endpoints SEDP 交换后列出的所有 Writer/Reader echo <topic> RTPS 传输的每个样本(序列号、内容)

实战示例

# 终端 1:启动你的 DDS 应用run_your_app.exe# 终端 2:启动 Spyfastddsspy.exe# Spy 交互:>>participants → 看到你的应用的 Participant 信息(通过 SPDP 发现)>>topics → 看到你应用发布的 Topic 名和类型(通过 SEDP 交换)>>echoMyTopic → 实时看到每条数据(通过 RTPS 传输)[echo]RECEIVED: MyType{index:1,...}[echo]RECEIVED: MyType{index:2,...}

7.2 Fast DDS Monitor——图形化拓扑视图

Monitor 是一个图形化工具,可以实时显示 DDS 网络的拓扑图和数据流

功能看到什么
拓扑图所有 Participant 的图标 + 连线(SPDP 结果可视化)
Topic 列表所有 Topic 及其类型、Writer/Reader 数(SEDP 结果)
吞吐量曲线每个 Topic 的 bytes/sec 和 samples/sec
延迟统计端到端的通信延迟

7.3 日志分析

Fast DDS 提供详细的运行时日志,可以帮助你理解内部状态:

# 开启调试日志运行你的程序your_app.exe --log-verbosity info# 你会看到类似这样的日志:[SPDP]Sending DATA(p)to239.255.0.0:7400[SEDP]Received DATA(w):Topic=Square,Type=ShapeType[RTPS]Received DATA(SN=42)from Writer 01.0f.xxxx[RTPS]Sending NACK(SN=42)formissing sample[RTPS]Received HEARTBEAT(SN=50), cache has up toSN=50

八、总结

DDS 并不神秘,它是一套设计极其精巧的分布式实时数据总线,通过层层递进的协议栈解决了分布式通信的核心难题:

问题层 DDS 的答案 ──────────────────────────────────────────────── "数据怎么传?" → 以数据为中心,构建全局数据空间(GDS) "如何找到人?" → SPDP:多播发送 DATA(p) 报文 "怎样谈合作?" → SEDP:单播交换 DATA(w) 和 DATA(r) "可靠怎么办?" → RTPS:History Cache + Heartbeat/ACKNACK "太慢了怎么办?" → SHM 共享内存 + 数据分片 + 多通道 "规则怎么定?" → 20+ 种 QoS 策略灵活配置 "如何调试?" → Fast DDS Spy(CLI)+ Fast DDS Monitor(GUI)+ 日志 通信全流程: ① 多播发现 (SPDP) → "找人" ② 端点匹配 (SEDP) → "谈判" ③ 序列化 (CDR) → "打包" ④ 发送 (RTPS/UDP/SHM) → "发货" ⑤ 确认 (ACK/NACK) → "签收" ⑥ 重传 (NACK→Data) → "补发" ⑦ 反序列化 → "拆包" ⑧ 回调通知 → "送达"

正是这些机制的叠加,使得 DDS 成为了 ROS2 的底层基石,自动驾驶和工业 4.0 时代的"神经系统"。

理解 DDS,不仅是学会使用一个中间件,更是理解一种构建高实时、高可靠分布式系统的架构思维


延伸阅读

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

3步搞定iOS应用永久安装:告别7天重签名烦恼

3步搞定iOS应用永久安装&#xff1a;告别7天重签名烦恼 【免费下载链接】TrollInstallerX A TrollStore installer for iOS 14.0 - 16.6.1 项目地址: https://gitcode.com/gh_mirrors/tr/TrollInstallerX 还在为iOS应用7天后自动失效而烦恼吗&#xff1f;每次测试应用都…

作者头像 李华
网站建设 2026/5/30 7:39:39

三款HLK雷达模块怎么选?LD105、LD2410B、LD2420实测对比与选购指南

HLK雷达模块深度评测&#xff1a;LD105、LD2410B、LD2420如何选型&#xff1f;在智能家居和IoT设备开发中&#xff0c;人体感应模块的选择往往决定了产品的用户体验和成本结构。HLK作为国内知名的雷达模块供应商&#xff0c;其LD系列产品因性价比高、性能稳定而备受开发者青睐。…

作者头像 李华
网站建设 2026/5/30 7:39:23

Wasserstein距离在蒙特卡洛模拟中的应用与优化

1. Wasserstein距离与蒙特卡洛模拟的基础原理1.1 什么是Wasserstein距离Wasserstein距离&#xff08;又称Earth Movers Distance&#xff09;是衡量两个概率分布之间差异的数学工具。想象你有一堆沙子堆成山A&#xff0c;需要搬运成山B的形状——Wasserstein距离就是完成这个搬…

作者头像 李华