在分布式系统的世界里我们习惯了 HTTP 的请求/响应模式也熟悉了 Kafka 和 RabbitMQ 那种中心化的消息队列。但在自动驾驶、工业机器人、航空航天等高实时性领域有一个名字频繁出现——DDSData 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 ID0-232 的整数区分不同 Domain 完全隔离互不干扰同一个进程可以同时加入多个 Domain作用网络分区隔离、多租户、安全性2.2 Participant——“群成员”Participant 是 DDS 网络中的一个节点。一个进程可以有一个或多个 Participant。每个 Participant 持有GUIDGlobally Unique Identifier— 全球唯一标识结构HostId AppId ObjectId共 16 字节例如01.0f.001122334455.0a1b2c3d.0.0.1相当于 DDS 世界的 MAC 地址 PIDIP 地址和端口— 用于网络通信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在白板上写字的人发布数据到 TopicDataReader看白板的人从 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 个 SubscriberParticipant “Planner” 里有 2 个 Subscriber 1 个 Publisher不需要配置任何 IP 地址——SPDP/SEDP 自动发现匹配———容易混淆的 5 个问题Q1一个进程可以有几个 Participant一个或多个。但通常一个进程只需要一个。Q2一个 Participant 可以同时发布和订阅吗当然可以。一个 Participant 里同时有 Publisher 和 Subscriber 是很常见的比如上面的规划模块。Q3不同进程里的 Participant 名字相同会怎样没事。名字只是给人看的标签DDS 内部靠 GUID 区分所以名字重复没关系。Q4Participant 加入 Domain 后做了什么就两件事发多播告诉所有人我来了SPDP我叫什么、IP 在哪、开了哪些端口监听别人的多播看还有谁在SPDP谁加入了、谁离开了Q5Participant 可以中途换 Domain 吗不行。Participant 创建时指定 Domain ID之后就固定了。想加入另一个 Domain 就再创建一个 Participant。———一句话记忆Domain 微信群隔离不同业务的数据Participant 你容器/入口持有 GUID 身份创建 Participant 之后才能在这个 Domain 里做发布/订阅/发现等一切操作三、自动发现如何找到彼此DDS 最令人惊叹的特性之一是零配置自动发现——你不需要配置 IP 地址、不需要启动中心化服务程序启动后自动找到彼此。这背后是两层发现协议在工作。3.1 SPDP参与方发现“这里有哪些人”SPDPSimple 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 存在后SEDPSimple 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 用 ShapeTypeReader 用 ImageType ✗QoS 兼容性发布端和订阅端 QoS 必须能兼容Writer RELIABLEReader BEST_EFFORT ✗QoS 兼容性矩阵最常用的 ReliabilityWriter ↓ / Reader → RELIABLE BEST_EFFORT ────────────────────────────────────────────── RELIABLE ✓ 兼容 ✗ 不兼容 BEST_EFFORT ✓ 兼容 ✓ 兼容简单记忆更严格的 Writer 要求 Reader 也严格更宽松的 Writer 兼容一切。调试技巧在fastddsspy.exe中用topics查看当前所有 Topic——你能看到 SEDP 交换后的结果endpoints可以看到每个 Writer/Reader 的 QoS 设置如果两个进程匹配不上先检查 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 真正的核心在于 RTPSReal-Time Publish-Subscribe Protocol——定义了数据如何在 UDP 这种不可靠的传输层上实现可靠、实时、高效的传输。4.1 History CacheDDS 的本地 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 │ │ ← SN3 丢了 │ └───┴───┴───┴───┴───┴───┘ │ │ Missing: [3] │ └──────────────────────────────┘每个样本Sample都带一个递增的Sequence Number (SN)类似于 TCP 的序列号SN1→ 第一个样本SN2→ 第二个样本SNk→ 第 k 个样本Cache 的深度由History QoS控制KEEP_LAST N→ 只缓存最近 N 个样本KEEP_ALL→ 缓存所有样本直到内存耗尽类比 GitWriter Cache ≈ 你的本地提交历史Reader Cache ≈ 别人拉取后的本地仓库。丢了的 commit 就是 missing SN需要 git fetchNACK来补上。4.2 Heartbeat ACKNACKUDP 上的可靠传输UDP 本身是不可靠的——数据包可能丢失、乱序、重复。RTPS 在 UDP 之上实现了一整套可靠机制。正常流程无丢包Writer Reader │ │ │── DATA(SN1, temp25.5) ──────────────→│ ✓ 收到存入 Cache │── DATA(SN2, temp25.6) ──────────────→│ ✓ 收到 │── DATA(SN3, temp25.4) ──────────────→│ ✓ 收到 │ │ │── HEARTBEAT(SN3) ────────────────────→│ 确认 最新 SN3 │←─ ACK(SN1~3全部收到) ────────────────│ 回复 全收到了丢包处理重传流程Writer Reader │ │ │── DATA(SN1) ─────────────────────────→│ ✓ │── DATA(SN2) ─────────────────────────→│ ✗ 网络丢包 │── DATA(SN3) ─────────────────────────→│ ✓ │ │ │── HEARTBEAT(SN3) ───────────────────→│ 我最新到 SN3 │←─ NACK(SN2) ─────────────────────────│ SN2 我没收到请重发 │ │ │── DATA(SN2, 重传) ──────────────────→│ ✓ 终于收到了 │ │ │←─ ACK(SN1~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告知我最新到哪个 SNACKNACKReader → Writer确认已收到 / 请求重传丢失的 SNGAPWriter → Reader告知SN5 到 SN8 被我跳过了不用等了PAD任意填充字节对齐用途DATA_FRAGWriter → Reader大数据分片传输HEARTBEAT_FRAGWriter → Reader分片传输的心跳GAP 的妙用如果 Writer 的 History QoS 设为KEEP_LAST 3那么当 SN5 被发送后SN2 已经从缓存中被移除了。此时如果有新 Reader 加入请求重传 SN2Writer 会发一个 GAP 消息说SN2 已经没有了请跳过它。这样 Reader 不会死等永远收不到的包。4.4 传输优化不只是 UDP共享内存SHM——同机通信的高速公路当两个 Participant 在同一台机器上时Fast DDS 默认启用共享内存传输进程 A 进程 B ┌──────────────┐ ┌──────────────┐ │ Writer │ │ Reader │ │ │ │ │ ↑ │ │ ▼ │ │ │ │ │ 序列化 → 写入 │ │ 读取 → 反序列化│ │ 共享内存区域 │ │ 共享内存区域 │ └──────┬───────┘ └──────┬───────┘ │ │ └─────────同一块物理内存────┘ 绕过网络栈微秒级延迟SHM 的优势延迟微秒级 vs UDP 的毫秒级1000 倍差距吞吐量受内存带宽限制远高于网卡带宽零拷贝数据写入共享内存后Reader 直接读取无需拷贝调试技巧如果运行 DDS 程序时遇到 SHM 错误“Failed to create segment”通常是因为进程没有权限写入共享内存文件。解决方案代码中指定仅用 UDPpqos.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 │ │SN10.1 │SN10.2 │SN10.3 │SN10.4 │SN10.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 是数据的地址那么 QoSQuality 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 默认 1KEEP_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 (Strength10) → 主控订阅者收 A 的数据 Writer B (Strength5) → 备控数据被忽略 A 崩溃后 Writer A (死了) → 不再发送 Writer B (Strength5) → 自动接管订阅者开始收 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 倍。六、一次完整的通信旅程让我们把前面所有的原理串联起来看一条数据从写入到接收的完整旅程时间点 步骤 发生什么 ──────────────────────────────────────────────────── T0ms ① Publisher 调用 writer-write(Hello) ↓ T0.1ms ② DDS 序列化C 对象 → CDR 二进制格式 ↓ T0.2ms ③ RTPS 封包加头部、分配 SN42、打时间戳 存入 Writer History Cache (SN42) ↓ T0.3ms ④ 传输层调度 同机有 Reader → SHM 写入共享内存 远程有 Reader → UDP 多播发送 ↓ T1ms ⑤ Reader 收到数据 SHM: 直接读取共享内存 UDP: 网卡中断 → 内核缓冲区 → 应用缓冲区 ↓ T1.1ms ⑥ 存入 Reader History Cache (SN42) ↓ T1.2ms ⑦ 触发 on_data_available() 回调 ↓ T1.3ms ⑧ 用户代码读取数据take_next_sample() ← 得到 Hello ↓ T5ms ⑨ Writer 发送 HEARTBEAT(SN42) 确认 Reader 回复 ACK(SN42) ↓ T100ms ⑩ 下一个周期重复①~⑨如果第 4 步的 UDP 包在网络中丢失了T0.3ms ④ DATA(SN42) 在路由器被丢弃 T5ms ⑨ HEARTBEAT(SN42) 到达 Reader Reader 检查 Cache有 SN41没有 SN42 → 回复 NACK(SN42) 请重传 SN42 T10ms ⑨ Writer 收到 NACK 从 History Cache 取出 SN42 的副本 重新发送 DATA(SN42) T11ms ⑩ Reader 收到重传的 SN42 存入 Cache触发回调这就是 DDS 即使运行在 UDP 上也能保证可靠传输的原因。七、监控调试工具看到原理在运行理解原理是一回事亲眼看到原理在运行是另一回事。以下工具可以让你观察到前面讲的所有机制7.1 Fast DDS Spy——命令行网络侦探Spy 是一个静默的 DDS Participant它加入你的 Domain 但不参与数据通信只监听你的应用 A ←→ 你的应用 B ↑ ↑ └── Spy ───────┘ (只监听不发言)用 Spy 观察原理Spy 命令 观察到的 DDS 原理 ───────────────────────────────────────────── participants SPDP 发现的每个 ParticipantGUID、名称 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):TopicSquare,TypeShapeType[RTPS]Received DATA(SN42)from Writer 01.0f.xxxx[RTPS]Sending NACK(SN42)formissing sample[RTPS]Received HEARTBEAT(SN50), cache has up toSN50八、总结DDS 并不神秘它是一套设计极其精巧的分布式实时数据总线通过层层递进的协议栈解决了分布式通信的核心难题问题层 DDS 的答案 ──────────────────────────────────────────────── 数据怎么传 → 以数据为中心构建全局数据空间GDS 如何找到人 → SPDP多播发送 DATA(p) 报文 怎样谈合作 → SEDP单播交换 DATA(w) 和 DATA(r) 可靠怎么办 → RTPSHistory Cache Heartbeat/ACKNACK 太慢了怎么办 → SHM 共享内存 数据分片 多通道 规则怎么定 → 20 种 QoS 策略灵活配置 如何调试 → Fast DDS SpyCLI Fast DDS MonitorGUI 日志 通信全流程 ① 多播发现 (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 文档
揭秘 DDS原理:无中心、自发现、实时可靠的“分布式神经“
在分布式系统的世界里我们习惯了 HTTP 的请求/响应模式也熟悉了 Kafka 和 RabbitMQ 那种中心化的消息队列。但在自动驾驶、工业机器人、航空航天等高实时性领域有一个名字频繁出现——DDSData 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 ID0-232 的整数区分不同 Domain 完全隔离互不干扰同一个进程可以同时加入多个 Domain作用网络分区隔离、多租户、安全性2.2 Participant——“群成员”Participant 是 DDS 网络中的一个节点。一个进程可以有一个或多个 Participant。每个 Participant 持有GUIDGlobally Unique Identifier— 全球唯一标识结构HostId AppId ObjectId共 16 字节例如01.0f.001122334455.0a1b2c3d.0.0.1相当于 DDS 世界的 MAC 地址 PIDIP 地址和端口— 用于网络通信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在白板上写字的人发布数据到 TopicDataReader看白板的人从 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 个 SubscriberParticipant “Planner” 里有 2 个 Subscriber 1 个 Publisher不需要配置任何 IP 地址——SPDP/SEDP 自动发现匹配———容易混淆的 5 个问题Q1一个进程可以有几个 Participant一个或多个。但通常一个进程只需要一个。Q2一个 Participant 可以同时发布和订阅吗当然可以。一个 Participant 里同时有 Publisher 和 Subscriber 是很常见的比如上面的规划模块。Q3不同进程里的 Participant 名字相同会怎样没事。名字只是给人看的标签DDS 内部靠 GUID 区分所以名字重复没关系。Q4Participant 加入 Domain 后做了什么就两件事发多播告诉所有人我来了SPDP我叫什么、IP 在哪、开了哪些端口监听别人的多播看还有谁在SPDP谁加入了、谁离开了Q5Participant 可以中途换 Domain 吗不行。Participant 创建时指定 Domain ID之后就固定了。想加入另一个 Domain 就再创建一个 Participant。———一句话记忆Domain 微信群隔离不同业务的数据Participant 你容器/入口持有 GUID 身份创建 Participant 之后才能在这个 Domain 里做发布/订阅/发现等一切操作三、自动发现如何找到彼此DDS 最令人惊叹的特性之一是零配置自动发现——你不需要配置 IP 地址、不需要启动中心化服务程序启动后自动找到彼此。这背后是两层发现协议在工作。3.1 SPDP参与方发现“这里有哪些人”SPDPSimple 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 存在后SEDPSimple 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 用 ShapeTypeReader 用 ImageType ✗QoS 兼容性发布端和订阅端 QoS 必须能兼容Writer RELIABLEReader BEST_EFFORT ✗QoS 兼容性矩阵最常用的 ReliabilityWriter ↓ / Reader → RELIABLE BEST_EFFORT ────────────────────────────────────────────── RELIABLE ✓ 兼容 ✗ 不兼容 BEST_EFFORT ✓ 兼容 ✓ 兼容简单记忆更严格的 Writer 要求 Reader 也严格更宽松的 Writer 兼容一切。调试技巧在fastddsspy.exe中用topics查看当前所有 Topic——你能看到 SEDP 交换后的结果endpoints可以看到每个 Writer/Reader 的 QoS 设置如果两个进程匹配不上先检查 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 真正的核心在于 RTPSReal-Time Publish-Subscribe Protocol——定义了数据如何在 UDP 这种不可靠的传输层上实现可靠、实时、高效的传输。4.1 History CacheDDS 的本地 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 │ │ ← SN3 丢了 │ └───┴───┴───┴───┴───┴───┘ │ │ Missing: [3] │ └──────────────────────────────┘每个样本Sample都带一个递增的Sequence Number (SN)类似于 TCP 的序列号SN1→ 第一个样本SN2→ 第二个样本SNk→ 第 k 个样本Cache 的深度由History QoS控制KEEP_LAST N→ 只缓存最近 N 个样本KEEP_ALL→ 缓存所有样本直到内存耗尽类比 GitWriter Cache ≈ 你的本地提交历史Reader Cache ≈ 别人拉取后的本地仓库。丢了的 commit 就是 missing SN需要 git fetchNACK来补上。4.2 Heartbeat ACKNACKUDP 上的可靠传输UDP 本身是不可靠的——数据包可能丢失、乱序、重复。RTPS 在 UDP 之上实现了一整套可靠机制。正常流程无丢包Writer Reader │ │ │── DATA(SN1, temp25.5) ──────────────→│ ✓ 收到存入 Cache │── DATA(SN2, temp25.6) ──────────────→│ ✓ 收到 │── DATA(SN3, temp25.4) ──────────────→│ ✓ 收到 │ │ │── HEARTBEAT(SN3) ────────────────────→│ 确认 最新 SN3 │←─ ACK(SN1~3全部收到) ────────────────│ 回复 全收到了丢包处理重传流程Writer Reader │ │ │── DATA(SN1) ─────────────────────────→│ ✓ │── DATA(SN2) ─────────────────────────→│ ✗ 网络丢包 │── DATA(SN3) ─────────────────────────→│ ✓ │ │ │── HEARTBEAT(SN3) ───────────────────→│ 我最新到 SN3 │←─ NACK(SN2) ─────────────────────────│ SN2 我没收到请重发 │ │ │── DATA(SN2, 重传) ──────────────────→│ ✓ 终于收到了 │ │ │←─ ACK(SN1~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告知我最新到哪个 SNACKNACKReader → Writer确认已收到 / 请求重传丢失的 SNGAPWriter → Reader告知SN5 到 SN8 被我跳过了不用等了PAD任意填充字节对齐用途DATA_FRAGWriter → Reader大数据分片传输HEARTBEAT_FRAGWriter → Reader分片传输的心跳GAP 的妙用如果 Writer 的 History QoS 设为KEEP_LAST 3那么当 SN5 被发送后SN2 已经从缓存中被移除了。此时如果有新 Reader 加入请求重传 SN2Writer 会发一个 GAP 消息说SN2 已经没有了请跳过它。这样 Reader 不会死等永远收不到的包。4.4 传输优化不只是 UDP共享内存SHM——同机通信的高速公路当两个 Participant 在同一台机器上时Fast DDS 默认启用共享内存传输进程 A 进程 B ┌──────────────┐ ┌──────────────┐ │ Writer │ │ Reader │ │ │ │ │ ↑ │ │ ▼ │ │ │ │ │ 序列化 → 写入 │ │ 读取 → 反序列化│ │ 共享内存区域 │ │ 共享内存区域 │ └──────┬───────┘ └──────┬───────┘ │ │ └─────────同一块物理内存────┘ 绕过网络栈微秒级延迟SHM 的优势延迟微秒级 vs UDP 的毫秒级1000 倍差距吞吐量受内存带宽限制远高于网卡带宽零拷贝数据写入共享内存后Reader 直接读取无需拷贝调试技巧如果运行 DDS 程序时遇到 SHM 错误“Failed to create segment”通常是因为进程没有权限写入共享内存文件。解决方案代码中指定仅用 UDPpqos.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 │ │SN10.1 │SN10.2 │SN10.3 │SN10.4 │SN10.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 是数据的地址那么 QoSQuality 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 默认 1KEEP_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 (Strength10) → 主控订阅者收 A 的数据 Writer B (Strength5) → 备控数据被忽略 A 崩溃后 Writer A (死了) → 不再发送 Writer B (Strength5) → 自动接管订阅者开始收 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 倍。六、一次完整的通信旅程让我们把前面所有的原理串联起来看一条数据从写入到接收的完整旅程时间点 步骤 发生什么 ──────────────────────────────────────────────────── T0ms ① Publisher 调用 writer-write(Hello) ↓ T0.1ms ② DDS 序列化C 对象 → CDR 二进制格式 ↓ T0.2ms ③ RTPS 封包加头部、分配 SN42、打时间戳 存入 Writer History Cache (SN42) ↓ T0.3ms ④ 传输层调度 同机有 Reader → SHM 写入共享内存 远程有 Reader → UDP 多播发送 ↓ T1ms ⑤ Reader 收到数据 SHM: 直接读取共享内存 UDP: 网卡中断 → 内核缓冲区 → 应用缓冲区 ↓ T1.1ms ⑥ 存入 Reader History Cache (SN42) ↓ T1.2ms ⑦ 触发 on_data_available() 回调 ↓ T1.3ms ⑧ 用户代码读取数据take_next_sample() ← 得到 Hello ↓ T5ms ⑨ Writer 发送 HEARTBEAT(SN42) 确认 Reader 回复 ACK(SN42) ↓ T100ms ⑩ 下一个周期重复①~⑨如果第 4 步的 UDP 包在网络中丢失了T0.3ms ④ DATA(SN42) 在路由器被丢弃 T5ms ⑨ HEARTBEAT(SN42) 到达 Reader Reader 检查 Cache有 SN41没有 SN42 → 回复 NACK(SN42) 请重传 SN42 T10ms ⑨ Writer 收到 NACK 从 History Cache 取出 SN42 的副本 重新发送 DATA(SN42) T11ms ⑩ Reader 收到重传的 SN42 存入 Cache触发回调这就是 DDS 即使运行在 UDP 上也能保证可靠传输的原因。七、监控调试工具看到原理在运行理解原理是一回事亲眼看到原理在运行是另一回事。以下工具可以让你观察到前面讲的所有机制7.1 Fast DDS Spy——命令行网络侦探Spy 是一个静默的 DDS Participant它加入你的 Domain 但不参与数据通信只监听你的应用 A ←→ 你的应用 B ↑ ↑ └── Spy ───────┘ (只监听不发言)用 Spy 观察原理Spy 命令 观察到的 DDS 原理 ───────────────────────────────────────────── participants SPDP 发现的每个 ParticipantGUID、名称 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):TopicSquare,TypeShapeType[RTPS]Received DATA(SN42)from Writer 01.0f.xxxx[RTPS]Sending NACK(SN42)formissing sample[RTPS]Received HEARTBEAT(SN50), cache has up toSN50八、总结DDS 并不神秘它是一套设计极其精巧的分布式实时数据总线通过层层递进的协议栈解决了分布式通信的核心难题问题层 DDS 的答案 ──────────────────────────────────────────────── 数据怎么传 → 以数据为中心构建全局数据空间GDS 如何找到人 → SPDP多播发送 DATA(p) 报文 怎样谈合作 → SEDP单播交换 DATA(w) 和 DATA(r) 可靠怎么办 → RTPSHistory Cache Heartbeat/ACKNACK 太慢了怎么办 → SHM 共享内存 数据分片 多通道 规则怎么定 → 20 种 QoS 策略灵活配置 如何调试 → Fast DDS SpyCLI Fast DDS MonitorGUI 日志 通信全流程 ① 多播发现 (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 文档