3. DDS核心概念:全局数据空间、域、域参与者、主题、发布者、订阅者
好,咱们进入DDS最核心的部分。说实话,我刚接触DDS时,也被这一堆术语搞得有点晕。什么全局数据空间、域、参与者……感觉像在学一门新语言。但等你真正用起来,会发现这些概念其实很自然,就像我们日常说话一样。
我习惯把这六个概念分成三层来理解:
- 顶层:全局数据空间 —— 整个系统的“虚拟总线”
- 中间层:域 —— 逻辑隔离的通信边界
- 底层:域参与者、主题、发布者、订阅者 —— 真正的通信实体
下面我一个一个拆开讲。
3.1 全局数据空间(Global Data Space)
全局数据空间,说白了就是DDS创造的一个“虚拟黑板”。
想象一下:所有节点都能在这块黑板上写数据,也能从上面读数据。不需要知道谁写的,也不需要知道谁在读。这就是去中心化的精髓。
我在做车云采集项目时,经常跟团队说:“别把DDS当成消息队列,它是个共享内存。” 消息队列是点对点的,你发我收。但全局数据空间是多对多的,一个数据写进去,所有感兴趣的节点都能拿到。
关键特征:
- 全局可见:只要在同一个域内,任何节点都能发现数据
- 按需订阅:节点只接收自己关心的数据,不浪费带宽
- 实时更新:数据变化立即通知订阅者,不需要轮询
你想想看,在车云场景里,一辆车可能有几十个传感器在发数据。如果用传统方式,你得为每个传感器建立单独的连接。但有了全局数据空间,传感器只管写,云端只管读,中间的事情DDS帮你搞定。
3.2 域(Domain)
域,就是给全局数据空间画了个“圈”。
同一个域里的节点才能互相通信。不同域之间天然隔离,数据互不可见。这就像公司里的不同部门——财务部的数据不会跑到研发部去。
我记得有一次,客户问:“我车上既有娱乐系统,又有底盘控制,数据会不会串?” 我说:“不会,你只要把它们放在不同的域里就行。”
| 域ID | 用途 | 示例数据 |
|---|---|---|
| 0 | 车辆控制域 | 车速、转向角、制动压力 |
| 1 | 信息娱乐域 | 音乐、导航、语音 |
| 100 | 车云采集域 | 电池SOC、GPS轨迹、故障码 |
实际项目中,我建议把车云数据采集单独划一个域。原因很简单:采集数据量大、实时性要求没那么极端,跟控制域混在一起容易互相影响。
小技巧: 域ID用整数表示,范围0-232。我习惯把生产环境和测试环境用不同域隔开,避免误操作。
3.3 域参与者(DomainParticipant)
域参与者,是节点进入域的“入口”。每个想通信的节点,必须先创建一个域参与者对象。
你可以把它理解成“身份证”。有了它,你才能在域里发布或订阅数据。
代码里长这样:
// 创建域参与者,加入域100
DomainParticipant* participant =
DomainParticipantFactory::get_instance()->create_participant(
100, // 域ID
PARTICIPANT_QOS_DEFAULT,
nullptr,
StatusMask::none()
);
嗯,这里要注意:一个进程可以创建多个域参与者,加入不同的域。但通常一个进程只加一个域就够了,多了反而增加复杂度。
3.4 主题(Topic)
主题,就是数据的“名字”。
发布者说:“我要发一个叫‘车速’的数据。” 订阅者说:“我只收叫‘车速’的数据。” 两边对上号,通信就建立了。
主题由三要素组成:
- 主题名:字符串,全局唯一(在域内)
- 数据类型:比如IDL定义的结构体
- QoS策略:可靠性、持久性等
我见过不少新手犯的错:两个节点主题名一样,但数据类型定义不同,结果通信失败。DDS对类型匹配检查很严格,名字和类型必须完全一致。
避坑指南: 我曾经在一个项目里,因为IDL文件版本没同步,导致车端和云端的数据结构对不上。排查了两天才发现是类型定义差了一个字段。所以,主题的数据类型一定要用版本管理工具控制好。
3.5 发布者(Publisher)与订阅者(Subscriber)
这两个是真正的“干活的人”。
发布者:负责把数据写进全局数据空间。它不直接操作主题,而是通过数据写入器(DataWriter)来写。
订阅者:负责从全局数据空间读数据。它通过数据读取器(DataReader)来读。
关系图是这样的:
发布者 → 数据写入器 → [主题] → 数据读取器 → 订阅者
代码示例:
// 创建发布者
Publisher* publisher = participant->create_publisher(
PUBLISHER_QOS_DEFAULT,
nullptr
);
// 创建数据写入器(绑定主题)
DataWriter* writer = publisher->create_datawriter(
topic,
DATAWRITER_QOS_DEFAULT,
nullptr
);
// 写入数据
SpeedData data;
data.speed = 60.5;
data.timestamp = get_time();
writer->write(&data, Handle_t());
订阅端类似:
// 创建订阅者
Subscriber* subscriber = participant->create_subscriber(
SUBSCRIBER_QOS_DEFAULT,
nullptr
);
// 创建数据读取器
DataReader* reader = subscriber->create_datareader(
topic,
DATAREADER_QOS_DEFAULT,
nullptr
);
// 读取数据(在回调中处理)
void on_data_available(DataReader* reader) {
SpeedData data;
SampleInfo info;
reader->take_next_sample(&data, &info);
process_speed(data.speed);
}
你可能会问:“为什么非要分发布者/订阅者和DataWriter/DataReader两层?” 嗯,这是为了灵活性。一个发布者可以管理多个DataWriter,一个订阅者可以管理多个DataReader。这样资源复用更高效。
3.6 整体关系图
下面这张图把六个概念串起来了,我建议你多看两眼:
从图里能清楚看到:
- 全局数据空间是最大的容器
- 域是逻辑隔离区
- 域参与者是通信实体
- 主题是数据通道
- 发布者/订阅者通过DataWriter/DataReader操作数据
3.7 实际项目中的配置建议
最后,分享几个我在车云采集项目中的经验:
命名规范: 主题名用“车辆ID_信号名”的格式,比如“VIN12345_BatterySOC”。这样在云端一看就知道是哪辆车的数据。
域ID规划: 我曾经遇到过两个团队用了同一个域ID,结果测试数据互相干扰。后来我们定了规矩:开发环境用0-99,测试用100-199,生产用200以上。
好了,这六个概念是DDS的基石。你理解了它们,后面讲QoS、发现机制、序列化就轻松多了。记住一句话:全局数据空间是舞台,域是剧场,域参与者是演员,主题是剧本,发布者和订阅者是上台表演的人。