3. 车云数据通信概述

好,咱们进入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 整体关系图

下面这张图把六个概念串起来了,我建议你多看两眼:

DDS六大核心概念关系图 全局数据空间(Global Data Space) 域(Domain ID: 100) 域参与者 A (车端采集节点) 发布者(Publisher) DataWriter 主题:车速 域参与者 B (云端接收节点) 订阅者(Subscriber) DataReader 主题:车速 发布者通过DataWriter写入主题数据,订阅者通过DataReader从主题读取数据 所有通信都在同一个域内,域属于全局数据空间

从图里能清楚看到:

  • 全局数据空间是最大的容器
  • 域是逻辑隔离区
  • 域参与者是通信实体
  • 主题是数据通道
  • 发布者/订阅者通过DataWriter/DataReader操作数据

3.7 实际项目中的配置建议

最后,分享几个我在车云采集项目中的经验:

命名规范: 主题名用“车辆ID_信号名”的格式,比如“VIN12345_BatterySOC”。这样在云端一看就知道是哪辆车的数据。

域ID规划: 我曾经遇到过两个团队用了同一个域ID,结果测试数据互相干扰。后来我们定了规矩:开发环境用0-99,测试用100-199,生产用200以上。

好了,这六个概念是DDS的基石。你理解了它们,后面讲QoS、发现机制、序列化就轻松多了。记住一句话:全局数据空间是舞台,域是剧场,域参与者是演员,主题是剧本,发布者和订阅者是上台表演的人。