2. DDS核心概念:数据空间、域、域参与者、主题、发布者、订阅者、数据写入器、数据读取器
好,咱们进入DDS最核心的部分。说实话,我第一次接触DDS时,被这一堆名词搞得有点晕——什么域、参与者、写入器、读取器……感觉像在学一门新语言。但后来我发现,这些概念其实特别接地气,你把它想象成一个「数据交换市场」,一下子就通了。
我个人习惯把DDS的架构拆成三层来理解:空间层(数据在哪)、角色层(谁在参与)、操作层(具体怎么读写)。咱们一个一个来。
2.1 数据空间(Data Space)—— 数据的「虚拟世界」
数据空间,说白了就是DDS管理数据的那个「虚拟世界」。它不是一个物理位置,而是一个逻辑上的全局数据视图。
我在项目中遇到过这样的场景:车上几十个ECU,每个都在产生数据。如果没有数据空间的概念,你根本不知道谁的数据该发给谁。DDS的数据空间就像一张「大数据地图」,所有数据都在这个空间里被统一管理。
核心要点:数据空间是DDS的基石。它不关心数据存在哪个硬盘里,也不关心数据走的是以太网还是共享内存。它只关心一件事——数据有没有被正确地「发布」和「订阅」。
2.2 域(Domain)—— 数据的「隔离区」
域是什么?你可以把它理解成一个独立的通信圈子。同一个域里的节点才能互相通信,不同域之间天然隔离。
嗯,这里要注意:域是用一个整数ID来标识的。比如域ID=0是默认域,你可以在同一个车里定义多个域——比如把ADAS感知数据和娱乐系统数据分到不同域,互不干扰。
| 域ID | 用途 | 典型场景 |
|---|---|---|
| 0 | 默认域 | 开发调试 |
| 1 | ADAS感知域 | 摄像头、雷达数据 |
| 2 | 车辆控制域 | 转向、制动指令 |
| 3 | 娱乐信息域 | 导航、多媒体 |
我曾经踩过一个坑:两个团队各自开发,一个用域ID=1,另一个用域ID=2,结果联调时发现数据死活不通。查了半天,原来是域ID写错了。所以我的建议是——域ID一定要在项目初期就定好,写进设计文档里。
2.3 域参与者(DomainParticipant)—— 域的「入口」
域参与者,就是进入某个域的「通行证」。每个应用程序要参与DDS通信,必须先创建一个域参与者对象。
你想想看,这就像你要进一个小区,得先办门禁卡。域参与者就是那张卡。一个进程可以有多个域参与者,分别进入不同的域。
我的经验:在嵌入式系统里,域参与者通常是一个单例。因为资源有限,没必要创建多个。但在PC仿真环境里,我有时会创建多个域参与者来模拟不同的ECU节点。
2.4 主题(Topic)—— 数据的「标签」
主题,是DDS里最核心的概念之一。它定义了数据的「类型」和「名称」。
举个例子:你有一个「摄像头原始图像」数据,它的主题名可以是 /sensor/camera/image,数据类型是 ImageFrame。发布者按这个主题发数据,订阅者按这个主题收数据。
// 主题定义示例(IDL语言)
struct ImageFrame {
long timestamp; // 时间戳
long width; // 图像宽度
long height; // 图像高度
sequence<octet> data; // 图像数据
};
// 创建主题
Topic<ImageFrame> topic("sensor/camera/image");
主题的命名我建议用层级结构,比如 /vehicle/speed、/sensor/lidar/pointcloud。这样后期维护起来特别清晰。
2.5 发布者(Publisher)与数据写入器(DataWriter)
这两个概念经常被搞混。我简单说一下:
- 发布者:是一个「管理者」,负责管理一组数据写入器。
- 数据写入器:是真正干活的,负责把数据写进DDS数据空间。
说白了,发布者就像是一个项目经理,数据写入器是具体干活的工程师。一个发布者可以管理多个数据写入器,每个数据写入器对应一个主题。
避坑指南:我曾经在项目中为了省事,一个发布者只配一个数据写入器。后来发现当数据量大了以后,这种一对一的配置反而增加了管理开销。正确的做法是:按功能模块划分发布者,比如「传感器发布者」管理所有传感器数据的写入器,「控制发布者」管理所有控制指令的写入器。
// 创建发布者和数据写入器
Publisher publisher = participant.createPublisher();
DataWriter<ImageFrame> writer = publisher.createDataWriter(topic);
// 写入数据
ImageFrame frame;
frame.timestamp = getCurrentTime();
frame.width = 1920;
frame.height = 1080;
// ... 填充图像数据
writer.write(frame);
2.6 订阅者(Subscriber)与数据读取器(DataReader)
和发布端对称,订阅端也有两个角色:
- 订阅者:管理者,管理一组数据读取器。
- 数据读取器:真正从DDS数据空间里取数据的。
你想想看,数据读取器怎么知道有新数据来了?DDS提供了两种方式:
- 轮询(Polling):你主动去问「有新数据吗?」
- 监听(Listener):数据来了,DDS主动通知你。
我个人更推荐监听方式。为什么?因为轮询会浪费CPU。我在一个项目中用轮询,结果CPU占用率飙到30%,换成监听后直接降到5%。
// 创建订阅者和数据读取器
Subscriber subscriber = participant.createSubscriber();
DataReader<ImageFrame> reader = subscriber.createDataReader(topic);
// 方式一:轮询
while (true) {
if (reader.hasData()) {
ImageFrame frame = reader.read();
processFrame(frame);
}
}
// 方式二:监听(推荐)
reader.setListener(new DataReaderListener() {
void onDataAvailable(DataReader reader) {
ImageFrame frame = reader.read();
processFrame(frame);
}
});
2.7 这些概念怎么串起来?
好,咱们把这些概念串成一个完整的流程:
- 首先,你有一个数据空间,它是DDS的整个世界。
- 在这个世界里,你可以划分多个域,每个域是一个独立的通信圈子。
- 你的应用程序通过域参与者进入某个域。
- 在域里,你定义主题来描述数据的类型和名称。
- 发布端:创建发布者,再创建数据写入器,按主题写入数据。
- 订阅端:创建订阅者,再创建数据读取器,按主题读取数据。
一句话总结:域参与者是门禁卡,主题是数据标签,发布者/订阅者是管理者,数据写入器/读取器是干活的。记住这个比喻,DDS的核心概念你就拿下了。
下一章,咱们会深入DDS的QoS策略——这才是DDS真正强大的地方。到时候我会分享一些实际调优的经验,保证让你少走弯路。