数据写入与读取:DataWriter、DataReader、Publisher、Subscriber

好,咱们今天聊聊DDS里最核心的四个角色。说白了,数据写入器、数据读取器、发布者、订阅者,这四个家伙构成了DDS通信的骨架。我刚开始接触DDS时,也经常搞混它们之间的关系。你想想看,为什么要有Publisher和DataWriter两层?直接写不行吗?

嗯,这里面的门道,我慢慢给你拆开讲。

1. 发布者与数据写入器:谁才是真正干活的人?

先看发布者(Publisher)。它其实是个“管理者”。它不直接写数据,而是管理一个或多个DataWriter。我习惯把Publisher比作一个项目组长,DataWriter就是组里的工程师。组长负责协调资源、制定策略,具体写代码的活,还是工程师来干。

那DataWriter(数据写入器)呢?它是真正往DDS全局数据空间里写数据的实体。每个DataWriter都绑定一个特定的Topic(主题)。你写数据时,必须通过DataWriter的write()方法。

核心关系:

  • 一个Publisher可以管理多个DataWriter
  • 每个DataWriter只能写一个Topic
  • DataWriter负责序列化、发送数据
  • Publisher负责控制发送策略(比如批量发送、流控)

我在项目中遇到过一个问题:有人把多个Topic的数据写到了同一个DataWriter里,结果数据全乱了。记住,一个DataWriter只对应一个Topic,这是铁律。

2. 订阅者与数据读取器:数据怎么到你手里?

订阅者(Subscriber)和读取器(DataReader)的关系,跟上面那对很像。Subscriber是管理者,DataReader是具体干活的人。

DataReader从DDS全局数据空间里拉取数据。它怎么知道有新数据来了?有两种方式:

  • 监听(Listener)模式:数据一来,回调函数自动触发。我建议实时性要求高的场景用这个。
  • 轮询(Polling)模式:你主动调用take()或read()去拿数据。适合你控制节奏的场景。

这里有个坑。我曾经用轮询模式,忘了设置超时时间,结果线程卡死在那了。嗯,后来我养成了习惯:轮询一定要设超时,哪怕设个100毫秒也行。

3. 数据写入流程:从应用到网络

咱们走一遍完整的数据写入流程。你写代码时,大概会经历这几步:

  1. 创建Publisher实例
  2. 通过Publisher创建DataWriter
  3. DataWriter内部创建Topic实例
  4. 调用DataWriter.write(data)
  5. DDS中间件自动序列化数据
  6. 数据发送到网络

代码示例大概长这样:

// 创建发布者
Publisher publisher = participant.createPublisher(
    PUBLISHER_QOS_DEFAULT, null);

// 创建数据写入器
DataWriter writer = publisher.createDataWriter(
    topic, DATAWRITER_QOS_DEFAULT, null);

// 写入数据
MyData data = new MyData();
data.id = 1001;
data.value = 3.14f;
writer.write(data, HANDLE_NIL);

你看,代码其实很简洁。但背后DDS帮你做了很多事:序列化、缓存、网络发送、可靠性保证。我个人习惯把write()调用放在一个独立的线程里,避免阻塞主业务逻辑。

4. 数据读取流程:从网络到应用

读取流程是写入的逆过程。但有个关键区别:读取时你可以选择“拿走”还是“看一眼”。

  • read():数据还在DataReader的缓存里,别人还能读到。
  • take():数据从缓存里移除,别人读不到了。

我建议:如果你做的是数据分发,用read();如果你做的是命令处理,用take()。为什么?命令执行一次就够了,数据可能需要多个订阅者都看到。

// 创建订阅者
Subscriber subscriber = participant.createSubscriber(
    SUBSCRIBER_QOS_DEFAULT, null);

// 创建数据读取器
DataReader reader = subscriber.createDataReader(
    topic, DATAREADER_QOS_DEFAULT, null);

// 读取数据(不删除)
SampleInfo[] infos;
MyData[] data = reader.read(infos, LENGTH_UNLIMITED);

// 或者拿走数据(删除)
MyData[] takenData = reader.take(infos, LENGTH_UNLIMITED);

小技巧:读取数据后,一定要检查SampleInfo里的valid_data字段。有时候你会读到“空样本”,那是DDS内部的控制消息,不是真正的数据。我刚开始做时没检查这个,结果拿了一堆空数据去处理,排查了半天。

5. 关键QoS策略:控制数据行为

DDS的强大之处在于QoS(服务质量)。DataWriter和DataReader都有各自的QoS配置。我挑几个最常用的说说:

QoS策略 DataWriter DataReader 说明
RELIABILITY RELIABLE / BEST_EFFORT RELIABLE / BEST_EFFORT 可靠还是尽力发送
DURABILITY TRANSIENT_LOCAL / VOLATILE TRANSIENT_LOCAL / VOLATILE 晚加入的订阅者能否收到历史数据
DEADLINE 设置发送周期 设置期望接收周期 超时未收到数据触发回调
LIVELINESS 设置心跳周期 监控对方是否存活 检测节点是否离线

这里有个经验:如果你做的是ADAS(高级驾驶辅助系统)里的传感器数据,用BEST_EFFORT就够了。为什么?传感器数据丢了就丢了,下一帧马上来。但如果你做的是控制指令,必须用RELIABLE,丢一条指令车可能就出问题。

注意:DataWriter和DataReader的QoS必须匹配。比如Writer设了RELIABLE,Reader设了BEST_EFFORT,那实际通信会降级为BEST_EFFORT。我曾经因为这个排查了一整天,两边QoS对不上,数据就是发不过去。

6. 实战中的避坑指南

最后,我总结几个实战中容易踩的坑:

  • DataWriter生命周期管理:不要频繁创建和销毁DataWriter。创建开销很大,我建议复用。我在一个项目里看到有人每发一条数据就新建一个Writer,结果性能惨不忍睹。
  • 数据一致性:多个DataWriter写同一个Topic时,DDS会按时间戳排序。但如果你用多个进程写同一个Topic,要小心时间戳冲突。我建议每个Writer分配唯一的ID。
  • 内存管理:DataReader读取数据后,记得释放内存。特别是C++版本,很容易内存泄漏。Java和Python有GC,但也要注意引用问题。
  • 线程安全:DataWriter的write()方法一般是线程安全的,但DataReader的read()/take()不是。我习惯在读取时加锁,或者用独立的读取线程。

嗯,这四个角色的关系,说白了就是“管理者”和“执行者”的分离。Publisher和Subscriber负责策略,DataWriter和DataReader负责具体干活。你把这个关系理清了,DDS的通信模型就掌握了一半。

DDS数据写入与读取核心架构 发布者 (Publisher) DataWriter 1 Topic: /sensor/radar DataWriter 2 Topic: /sensor/camera DataWriter 3 Topic: /control/steering DDS全局数据空间 订阅者 (Subscriber) DataReader 1 Topic: /sensor/radar DataReader 2 Topic: /sensor/camera DataReader 3 Topic: /control/steering DataWriter DataReader 全局数据空间 写入 读取

这张图把整个架构串起来了。左边是发布者管理多个DataWriter,右边是订阅者管理多个DataReader,中间是DDS全局数据空间。数据从左边写入,从右边读取,完全解耦。

好了,DataWriter和DataReader的核心用法就这些。记住:Publisher和Subscriber是管理者,DataWriter和DataReader是执行者。把角色分清楚,代码写起来就顺了。

专注资料整理