具身智能—DDS通讯架构介绍 欢迎来到我的技术小筑一个专为技术探索者打造的交流空间。在这里我们不仅分享代码的智慧还探讨技术的深度与广度。无论您是资深开发者还是技术新手这里都有一片属于您的天空。让我们在知识的海洋中一起航行共同成长探索技术的无限可能。 探索专栏学步_技术的首页 —— 持续学习不断进步让学习成为我们共同的习惯让总结成为我们前进的动力。 技术导航人工智能深入探讨人工智能领域核心技术。自动驾驶分享自动驾驶领域核心技术和实战经验。环境配置分享Linux环境下相关技术领域环境配置所遇到的问题解决经验。图像生成分享图像生成领域核心技术和实战经验。虚拟现实技术分享虚拟现实技术领域核心技术和实战经验。 非常期待在这个数字世界里与您相遇一起学习、探讨、成长。不要忘了订阅本专栏让我们的技术之旅不再孤单 ✨✨ 欢迎关注和订阅一起开启技术探索之旅 ✨✨文章目录背景介绍DDS 解决的核心问题以数据为中心的架构思想去中心化发现机制QoSDDS 最重要的配置层一个简单的发布/订阅示例发布者订阅者带基本 QoS 的 Cyclone DDS 示例典型应用场景机器人系统自动驾驶工业和轨道交通仿真与测试平台常见的 DDS 实现调优与排障时需要关注的点与其他通信方式的对比总结背景介绍在机器人、自动驾驶、工业控制、航天器以及一些大型仿真平台里系统通常不是简单的一对一通信。一个感知模块发布的障碍物列表可能同时被规划、预测、显示、日志记录等多个节点消费底盘控制模块发出的状态也会被故障诊断、运动控制和人机交互订阅。传统做法往往是用 TCP 或 UDP 自己定义一套消息协议再加上点对点连接、心跳、重连、序列化、线程调度等逻辑。系统小的时候还可以维护但一旦节点数量增加通信拓扑会迅速变得复杂。这类系统的共性是参与者对“某类数据什么时候出现”感兴趣而不关心对端进程在哪个主机上也不希望把精力花在建立连接、转发消息和处理断线上。ROS 2 在选择中间件时放弃了自研的底层通信协议转而采用 DDS也是出于这个原因。DDS 的全称是 Data Distribution Service中文一般叫数据分发服务。它最初来自 OMG 的规范体系后来在实时性和可靠性要求较高的领域得到了大量使用。与常见的消息队列不同DDS 并不是一个中心化的 Broker。发布者和订阅者之间直接通信没有像 Kafka、RabbitMQ 那样必须依赖一个持续运行的消息服务进程。这个特点在机器人系统和分布式控制系统中很重要因为这类系统通常要求数据链路尽量短同时不能让一个中心节点成为单点故障。DDS 解决的核心问题如果用一个比较直白的说法概括DDS 解决的是“分布式系统里不同节点之间如何可靠、实时、低耦合地共享数据”的问题。假设一个机器人系统里有一个激光雷达节点它持续发布点云数据。下游的建图模块、避障模块和可视化模块都需要这份数据但它们关心的细节并不一样建图模块可能要求可靠传输不能丢帧避障模块更在意低延迟宁可丢掉旧点云也要尽快拿到最新一帧可视化模块只要周期性看到画面即可对带宽消耗比较敏感。如果直接使用 TCP 连接开发人员需要自己处理这些问题谁先启动断线后怎么恢复多播还是单播每一类数据的可靠性策略是否一样不同订阅者要不要分别维护队列这些逻辑如果散落在业务代码里系统规模越大越难维护。DDS 把这些内容抽象成了发布/订阅模型和一组 QoS 策略。通信的双方只需要约定 Topic 和数据类型再把各自的可靠性、历史缓存、生命周期等需求表达出来底层中间件就会负责匹配、连接、传输和资源管理。也就是说DDS 把很多分布式通信里容易重复实现的基础能力前移到了中间件层。以数据为中心的架构思想DDS 与普通 Pub/Sub 中间件最大的差异在于它强调“以数据为中心”。传统 Pub/Sub 通常围绕消息主题和消息体组织系统节点之间彼此知道对方的 Topic。而 DDS 把系统建模成一个“全局数据空间”。某个模块发布的是这个数据空间里的对象状态订阅者看到的是这些状态在本地维护的副本。可以把 DDS 的数据空间理解成一个逻辑上的共享内存。它并不要求所有数据真的集中存储在某台机器上而是通过底层协议把数据的最新值和状态变化分发到需要的节点。订阅者拿到数据后可以直接在自己的本地缓存里读取。这种模型对机器人系统很自然传感器数据、定位结果、地图、控制指令本质上都是系统中的状态数据。DDS 中有几个基础概念需要先分清楚。Domain 表示一个通信域。不同的 Domain 之间默认完全隔离即使两个节点位于同一台机器或同一个局域网中只要 Domain ID 不同它们也不会互相发现、不会互相通信。这个机制很适合把不同功能子系统隔开比如规划仿真使用 Domain 0传感器原始数据使用 Domain 1。DomainParticipant 是节点进入某个 Domain 的入口。一个进程里可以创建多个 Participant但大多数情况下一个节点只需要一个。Publisher 和 Subscriber 分别是数据发送侧和接收侧的容器。它们下面再创建具体的 DataWriter 和 DataReader。真正读写数据的对象是 DataWriter 和 DataReader而 Publisher/Subscriber 更多承担组织和资源管理的职责。Topic 是数据主题用来描述“这一类数据是什么”。它由 Topic 名称和数据类型共同决定。只有 Topic 名称和数据类型都能匹配时DDS 才会把发布端和订阅端关联起来。简单来说一次 DDS 通信的参与对象大致是DomainParticipant ├── Publisher │ └── DataWriter │ └── Topic └── Subscriber └── DataReader └── Topic去中心化发现机制DDS 有一个非常关键的特性没有中心 Broker。那么两个节点如何知道彼此的存在答案是自动发现。DDS 中的发现协议通常称为 RTPS Discovery底层一般依赖组播和单播。一个 Participant 启动后会向预先约定的组播地址发送自己的信息包括 Domain ID、Participant ID、可用的传输地址、支持的 Reader/Writer 实体等。同一 Domain 中的其他 Participant 收到这些报文后会尝试建立单播连接。之后双方会交换更详细的端点信息比如哪些 Topic 存在、数据类型是什么、QoS 是否兼容。如果两条链路之间的 QoS 不兼容即使 Topic 名称和数据类型一致DDS 也不会把这两端匹配起来。一个典型例子是发布端设置了BEST_EFFORT订阅端设置了RELIABLE。这种情况下订阅端拿不到可靠语义匹配会失败。反过来发布端RELIABLE、订阅端BEST_EFFORT通常可以匹配因为订阅端只是选择不要求可靠性。这种去中心化发现的好处是DDS 没有集中的注册中心新增节点不会造成一个服务进程的连接风暴。节点之间直接交换端点信息数据路径也更短。缺点是组播环境必须可用或者说 DDS 的发现端口和地址必须能正常访问。在一些容器网络、虚拟机网络、多网卡机器和复杂防火墙环境下DDS 的发现经常出问题。很多工程师第一次使用 DDS 时遇到的现象并不是代码错误而是“程序正常运行但两个节点互相看不见”。这时通常需要配置网络接口、对端地址列表或者显式关闭/开启组播。RTPS 是 Real-Time Publish-Subscribe Protocol 的缩写它是 DDS 规范底层的线协议。可以把 DDS 理解成上层 API 和系统模型而 RTPS 负责把这些模型在网络上具体实现。不同厂商的 DDS 实现只要遵循 RTPS就有机会与彼此通信。QoSDDS 最重要的配置层DDS 的强大之处不只在于自动发现还在于 QoS。QoS 决定了数据如何在时间、可靠性和资源之间权衡。常见的策略包括Reliability可靠传输或尽力传输。History本地缓存策略如保留最近 N 条或只保留最后一条。Durability后加入的订阅者能否收到历史数据。Deadline数据必须多久更新一次。Lifespan数据的有效期过期后自动丢弃。Ownership多个发布者写同一 Topic 时的仲裁方式。以传感器数据为例激光雷达、相机、IMU 这类数据通常更在意实时性。如果网络抖动导致旧帧堆积继续把 200 ms 前的点云发给避障模块意义不大甚至可能造成误判。所以这类 Topic 通常会配置Reliability BEST_EFFORT History KEEP_LAST, depth 1 或 2而控制指令、安全状态、地图更新这类数据则可能相反。丢一帧可能带来严重后果因此更适合使用Reliability RELIABLE History KEEP_LAST, depth 10 Durability TRANSIENT_LOCALTRANSIENT_LOCAL的意义是新加入的订阅者可以拿到发布者本地保留的一部分历史数据。比如系统启动较晚的参数查询节点仍然有机会读到最近一次系统状态。QoS 并不是孤立的开关。不同策略之间会相互作用配置不当也会带来性能问题。例如RELIABLE不等于无限重传它依然受到传输层、缓存和资源限制的影响KEEP_LAST的 depth 过大可能造成旧数据堆积过小则可能丢掉对业务有价值的数据。实际项目中最好根据数据频率、帧大小、容忍延迟和消费者处理能力来调整。一个简单的发布/订阅示例下面用 C 说明 DDS 的基本写法。这里以 Cyclone DDS 为例IDL 类型定义部分先省略只看核心流程。假设已经通过 IDL 生成了HelloWorld类型代码骨架大致如下。发布者#includedds/dds.hpp#includeHelloWorld.hppusingnamespaceorg::eclipse::cyclonedds;intmain(){dds::domain::DomainParticipantparticipant(0);dds::topic::TopicHelloWorldtopic(participant,HelloWorldTopic);dds::pub::Publisherpublisher(participant);dds::pub::DataWriterHelloWorldwriter(publisher,topic);HelloWorld sample;sample.index(0);sample.message(hello dds);for(inti0;i10;i){sample.index(i);writer.write(sample);std::this_thread::sleep_for(std::chrono::milliseconds(500));}return0;}订阅者#includedds/dds.hpp#includeiostream#includeHelloWorld.hppusingnamespaceorg::eclipse::cyclonedds;intmain(){dds::domain::DomainParticipantparticipant(0);dds::topic::TopicHelloWorldtopic(participant,HelloWorldTopic);dds::sub::Subscribersubscriber(participant);dds::sub::DataReaderHelloWorldreader(subscriber,topic);while(true){autosamplesreader.take();for(constautosample:samples){if(sample.info().valid()){constHelloWorlddatasample.data();std::coutindex: data.index(), message: data.message()std::endl;}}std::this_thread::sleep_for(std::chrono::milliseconds(100));}return0;}这段代码体现了 DDS 的一个基本特点发布者和订阅者没有显式连接对方的地址。双方只声明了自己的 Domain、Topic 和类型。只要发现协议正常工作两端就能自动匹配。带基本 QoS 的 Cyclone DDS 示例实际项目中通常需要给 Topic 配置 QoS。下面是一个把 Reliability 设置为 Reliable、History 设置为 KEEP_LAST 10 的例子。#includedds/dds.hpp#includeHelloWorld.hppusingnamespaceorg::eclipse::cyclonedds;intmain(){dds::domain::DomainParticipantparticipant(0);dds::topic::TopicHelloWorldtopic(participant,HelloWorldTopic);dds::pub::qos::DataWriterQos writer_qos;writer_qos.reliability(dds::core::policy::Reliability::RELIABLE);writer_qos.history(dds::core::policy::History::KeepLast(10));dds::pub::Publisherpublisher(participant);dds::pub::DataWriterHelloWorldwriter(publisher,topic,writer_qos);HelloWorld sample;sample.index(0);sample.message(reliable hello);for(inti0;i20;i){sample.index(i);writer.write(sample);std::this_thread::sleep_for(std::chrono::milliseconds(200));}return0;}订阅端如果不设置RELIABLE发布端的可靠语义可能无法匹配成功。一个常见做法是在订阅端也使用相同或兼容的 QoSdds::sub::qos::DataReaderQos reader_qos;reader_qos.reliability(dds::core::policy::Reliability::RELIABLE);reader_qos.history(dds::core::policy::History::KeepLast(10));dds::sub::DataReaderHelloWorldreader(subscriber,topic,reader_qos);如果使用 ROS 2很多 DDS 概念会被封装在 Node、Publisher、Subscriber、Service 和 Action 之下。但它的 QoS Profile、发现机制、传输策略仍然来自 DDS。理解 DDS 对排查 ROS 2 的网络问题和调优通信性能很有帮助。典型应用场景机器人系统机器人系统通常是典型的多节点协作场景。感知、定位、规划、控制、诊断和日志模块可能分布在同一台工控机上也可能分布在相机控制器、底盘控制器和远程监控端。DDS 的发布/订阅模型可以直接映射到传感器流和状态流的共享同时通过 QoS 区分不同数据的重要性。例如 IMU 数据适合低延迟传输电池状态适合可靠传输地图更新则可以结合 Durability 让晚启动的模块拿到最新状态。自动驾驶自动驾驶中大量数据在感知、融合、规划、控制之间流动。DDS 的实时性、去中心化和 QoS 能力适合这类场景。特别是当系统中存在多路相机、毫米波雷达、激光雷达和车辆状态数据时每一类数据都有不同的频率、丢包容忍度和处理延迟要求。使用统一的 DDS 中间件可以减少各模块自行实现传输细节的成本。工业和轨道交通在工业控制、轨道交通、电力监控等领域DDS 常用于实时状态同步和控制命令分发。这些场景通常关注确定性、可用性和可观测性。DDS 提供的 Deadline、Liveliness、Ownership 等 QoS可以帮助系统表达“某个数据多久必须更新一次”“某个发布者是否仍然存活”“多个控制源之间谁优先”这类约束。仿真与测试平台仿真平台往往需要同时连接多个仿真节点、测试工具和可视化工具。DDS 可以让仿真数据、测试指令、车辆模型状态在不同的进程和主机之间共享而不必为每个工具单独实现通信接口。常见的 DDS 实现目前常见的 DDS 实现包括 Cyclone DDS、Fast DDS、RTI Connext DDS、OpenDDS 等。Cyclone DDS 的实现比较轻量性能表现良好配置相对直观在 ROS 2 中也是常见选择。Fast DDS 是 eProsima 的实现同样广泛用于 ROS 2 生态。RTI Connext DDS 是商业产品功能完整工具链强大很多安全关键领域使用较多。OpenDDS 是开源实现历史较长适合一些传统项目。不同实现的 API 大体遵循 DDS 规范但在安装方式、配置文件、默认行为、调试工具和性能细节上有差异。项目选型时除了关注吞吐和延迟还要考虑许可协议、平台支持、工具链、配置能力以及团队维护经验。调优与排障时需要关注的点DDS 使用起来的门槛往往不在写代码而在网络环境和 QoS 配置。实际项目中以下几个方面值得重点关注。第一是多网卡问题。一台机器可能同时存在有线网卡、无线网卡、Docker 虚拟网卡和 VPN 网卡。DDS 发现过程可能会选择错误的接口导致本机两个进程能通信跨机器节点却无法互相发现。这类问题通常可以通过配置 General/Interfaces、AllowedInterfaces 或类似的网络接口过滤策略解决。第二是组播是否可用。部分云环境、容器网络或交换机配置会限制组播。此时 DDS 可以配置为只使用单播或者通过 Peer 列表显式指定对端地址。第三是大消息传输。相机图像、点云、大地图等数据可能超过 UDP 报文的安全大小。DDS 底层通常会进行分片但如果接收端处理慢或网络质量差可能出现丢帧、重组失败或内存占用上升。必要时可以开启共享内存、调整分片大小或者降低单条消息的大小。第四是 QoS 不匹配。DDS 的自动匹配是强约束的。Topic 名称相同、类型相同但 Reliability、Durability、Ownership 等策略不兼容时两端不会连起来。排障时不要只盯着代码里的字符串还要检查 QoS Profile。第五是历史队列深度。KEEP_LAST模式下depth 决定了发布端或订阅端保留多少条数据。消费者处理慢时队列会被覆盖或堆积。对实时数据来说depth 通常不宜太大对命令或事件类数据则要根据重放和容错需求设计。与其他通信方式的对比与 TCP 直接通信相比DDS 提供了类型系统、自动发现、发布/订阅模型和 QoS省去了大量自定义协议代码。它更适合多对多、动态加入退出、数据状态频繁更新的系统。与 Kafka 或 RabbitMQ 这类 Broker 架构相比DDS 没有中心服务进程数据路径更短实时性更容易控制。Kafka 更适合大规模日志、事件回放和数据管道RabbitMQ 常用于业务解耦和任务队列。DDS 则偏向实时分布式系统中的状态数据共享。与 gRPC 相比gRPC 更偏远程调用接口形式通常是请求/响应。DDS 更偏持续数据流的发布与订阅。两者不是互斥关系。在一些系统里管理接口和服务调用可以使用 gRPC而高频状态流、传感器流和控制流使用 DDS。总结DDS 的价值在于它把分布式系统中很多底层通信问题抽象成了数据模型和策略配置。开发者面对的不再是地址、端口、连接和重连逻辑而是 Participant、Topic、DataWriter、DataReader 和 QoS。系统通过自动发现完成端点匹配通过 RTPS 完成实际的数据传输通过 QoS 表达不同数据的可靠性、实时性和生命周期需求。它的核心思想可以概括为三点以数据为中心没有中心 Broker用 QoS 表达通信约束。对机器人、自动驾驶、工业控制这类系统来说DDS 并不是万能的也不是所有场景下都最优但在多节点实时数据共享场景中它提供了一套非常成熟的工程模型。如果只是发几条测试消息DDS 的代码看起来可能比直接写 TCP 更复杂。可一旦系统扩展到几十个节点、上百个 Topic并且存在不同的实时性和可靠性要求DDS 带来的结构化能力就会明显体现出来。真正用好 DDS关键还是理解自己的数据特征哪些数据允许丢哪些数据必须到达哪些数据只需要最新值哪些数据要交给后加入者哪些数据要求周期更新。把这些业务语义映射到 DDS 的 Topic 和 QoS 上往往比照抄默认配置更重要。 在这篇博文的旅程中感谢您的陪伴与阅读。如果内容对您有所启发或帮助请不要吝啬您的点赞 这是对我最大的鼓励和支持。 本人虽致力于提供准确且深入的技术分享但学识有限难免会有疏漏之处。如有不足或错误恳请各位业界同仁在评论区留下宝贵意见您的批评指正是我不断进步的动力 如果您发现这篇博文对您的研究或工作有所裨益请不吝点赞、收藏或分享给更多需要的朋友让知识的力量传播得更远。 “Stay Hungry, Stay Foolish” —— 求知的道路永无止境让我们保持渴望与初心面对挑战勇往直前。无论前路多么漫长只要我们坚持不懈终将抵达目的地。 在此我也邀请您加入我的技术交流社区共同探讨、学习和成长。让我们携手并进共创辉煌