数据写入与读取: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. 数据写入流程:从应用到网络
咱们走一遍完整的数据写入流程。你写代码时,大概会经历这几步:
- 创建Publisher实例
- 通过Publisher创建DataWriter
- DataWriter内部创建Topic实例
- 调用DataWriter.write(data)
- DDS中间件自动序列化数据
- 数据发送到网络
代码示例大概长这样:
// 创建发布者
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的通信模型就掌握了一半。
这张图把整个架构串起来了。左边是发布者管理多个DataWriter,右边是订阅者管理多个DataReader,中间是DDS全局数据空间。数据从左边写入,从右边读取,完全解耦。
好了,DataWriter和DataReader的核心用法就这些。记住:Publisher和Subscriber是管理者,DataWriter和DataReader是执行者。把角色分清楚,代码写起来就顺了。