第四节:DDS基础概念——数据空间、全局数据空间、域参与者、发布者、订阅者

好,咱们进入DDS的世界。说实话,我第一次接触DDS时,被那一堆术语搞得有点懵。什么数据空间、全局数据空间、域参与者……听着像科幻小说里的概念。但干久了你会发现,这些抽象名词背后,其实都是很朴实的工程思想。

今天我就带你把这几个核心概念掰开揉碎。你理解了它们,DDS的骨架也就搭起来了。

4.1 数据空间(Data Space)——数据在哪?

先问个问题:在分布式系统里,数据到底存在哪?

传统做法是:A节点发消息给B节点,B收到后存到自己的内存里。数据是“搬来搬去”的。

DDS不这么干。它提出了一个概念叫数据空间。说白了,就是一个虚拟的、全局共享的数据池子。发布者往池子里扔数据,订阅者从池子里取数据。谁都不需要知道对方在哪。

我当年做第一个DDS项目时,跟团队解释这个模型,有人问:“那数据到底存在谁的机器上?” 我说:“你不用管。DDS中间件帮你搞定。你只关心数据长什么样、你要发什么、你要收什么。”

数据空间的核心特征:

  • 逻辑统一:所有节点看到的是同一个数据空间
  • 物理分布:数据实际分散在各节点内存中
  • 自动同步:中间件保证数据一致性

重点理解:数据空间不是一块物理硬盘或内存,而是一个抽象层。它让分布式系统里的数据看起来就像在本地一样。

4.2 全局数据空间(Global Data Space, GDS)——把数据空间连成网

单个数据空间只能管一个系统。但车载网络里,有多个域:动力域、座舱域、智驾域……每个域都有自己的数据空间。

这时候就需要全局数据空间(GDS)了。它把多个数据空间桥接起来,形成一个更大的虚拟数据池。

举个例子:

  • 智驾域的数据空间里,有“目标物列表”这个数据
  • 座舱域的数据空间里,有“HMI显示状态”这个数据
  • 通过GDS,座舱域可以订阅智驾域的“目标物列表”,在仪表盘上显示

我参与过一个项目,智驾和座舱分别用了不同的DDS实现。一开始两边数据空间是隔离的,后来通过GDS桥接,才实现了跨域数据共享。嗯,这里要注意:GDS的配置很关键,搞不好会引入延迟。

概念 范围 典型场景
数据空间 单个域内 智驾域内传感器数据共享
全局数据空间 跨域 智驾域→座舱域的数据桥接

4.3 域参与者(DomainParticipant)——你是谁?

每个要加入DDS网络的节点,都得先创建一个域参与者。你可以把它理解成“身份凭证”。

我习惯这么比喻:数据空间是一个大会议室,域参与者就是你的工牌。你拿着工牌走进会议室,才能发言或听别人发言。

创建域参与者时,需要指定:

  • 域ID:你进哪个会议室?不同域ID的节点互相看不见
  • QoS策略:你的通信质量要求?可靠性、延迟、持久性等
  • 监听器:你关心哪些事件?比如有节点加入/离开
// 伪代码示例:创建域参与者
DomainParticipant* participant = 
    DomainParticipantFactory::get_instance()->create_participant(
        0,                    // 域ID,0表示默认域
        PARTICIPANT_QOS_DEFAULT,
        nullptr,              // 监听器
        StatusMask::all()     // 监听所有事件
    );

避坑指南:我曾经在项目里忘了设置域ID,结果两个节点的域ID不同,互相发现不了。排查了半天才发现是这个问题。记住:域ID必须一致,否则就是两个世界。

4.4 发布者(Publisher)与数据写入器(DataWriter)

有了域参与者,你就能创建发布者了。发布者负责把数据写进数据空间。

但发布者本身不直接写数据,它通过数据写入器(DataWriter)来写。一个发布者可以管理多个DataWriter,每个DataWriter对应一种数据类型。

你想想看,这就像:

  • 发布者 = 一个出版社
  • DataWriter = 一个编辑,专门负责某类书籍的出版

实际代码里,你通常这样用:

// 创建发布者
Publisher* publisher = participant->create_publisher(
    PUBLISHER_QOS_DEFAULT,
    nullptr
);

// 创建数据写入器(假设数据类型是SensorData)
DataWriter* writer = publisher->create_datawriter(
    topic,                    // 话题,后面会讲
    DATAWRITER_QOS_DEFAULT,
    nullptr
);

// 写入数据
SensorData data;
data.sensor_id = 1;
data.value = 25.6;
writer->write(&data);

注意:DataWriter的QoS设置会影响数据发送行为。比如设置成“可靠”模式,中间件会重传丢失的数据包。但代价是延迟增加。车载场景里,控制信号要可靠,传感器流数据可以容忍少量丢包。别搞反了。

4.5 订阅者(Subscriber)与数据读取器(DataReader)

有发就有收。订阅者负责从数据空间里取数据。同样,它通过数据读取器(DataReader)来读。

订阅者可以订阅多个话题,每个话题对应一个DataReader。当数据到达时,DataReader会通知你。

我习惯用“收件箱”来理解:

  • 订阅者 = 你的邮箱
  • DataReader = 邮箱里的一个文件夹,专门放某类邮件

读取数据有两种方式:

  1. 拉模式:你主动调用read()或take()去取数据
  2. 推模式:设置监听器,数据一来自动回调
// 创建订阅者
Subscriber* subscriber = participant->create_subscriber(
    SUBSCRIBER_QOS_DEFAULT,
    nullptr
);

// 创建数据读取器
DataReader* reader = subscriber->create_datareader(
    topic,
    DATAREADER_QOS_DEFAULT,
    nullptr
);

// 拉模式读取数据
SensorDataSeq data_seq;
SampleInfoSeq info_seq;
reader->take(data_seq, info_seq, LENGTH_UNLIMITED);
// 处理数据...
reader->return_loan(data_seq, info_seq);

关键点:take()会从数据空间里移除数据,read()不会。如果你只想看一眼数据而不消费它,用read()。如果你处理完数据不想再看到它,用take()。我见过有人用错了,导致数据被重复消费或丢失。

4.6 它们怎么配合?一个完整流程

咱们串起来看一个完整例子:

  1. 节点A创建域参与者,加入域ID=0
  2. 节点A创建发布者,再创建DataWriter,往话题“/sensor/temperature”里写数据
  3. 节点B创建域参与者,也加入域ID=0
  4. 节点B创建订阅者,再创建DataReader,订阅话题“/sensor/temperature”
  5. DDS中间件自动发现这两个节点,建立通信
  6. 节点A写数据,节点B自动收到

整个过程,你不需要关心IP地址、端口号、序列化格式。DDS全包了。

我刚开始用DDS时,觉得这太神奇了。后来才明白,它背后做了大量工作:服务发现、数据序列化、QoS协商、可靠性保证……但这些对开发者是透明的。你只需要关心业务逻辑。

4.7 小结与避坑

今天这几个概念,是DDS的基石。我建议你动手写个demo,把发布者和订阅者跑通。光看概念是记不住的。

最后几个坑,我帮你提前踩了:

  • 域ID必须一致:否则节点互相看不见
  • 话题名必须完全匹配:大小写敏感,别写错了
  • 数据类型必须一致:发布者和订阅者的数据类型定义要相同
  • QoS要匹配:比如发布者设了可靠,订阅者也必须设可靠,否则通信失败

下一节,咱们聊DDS的核心通信模型——发布/订阅模式。到时候你会看到,今天学的这些概念是怎么协同工作的。