DDS Publisher 与 DataWriter 的关系:从 DDS 规范到 ROS2 实现
背景
在封装 DDS 通信层时,一个常见的设计疑问是:一个 Node 内的多个 Publisher 是否应该共享同一个 DDS Publisher 实体?这会不会导致 QoS 冲突? 本文从 DDS 规范出发,梳理 Publisher 与 DataWriter 的职责边界,对比 ROS2 (rmw_cyclonedds) 的实际实现,最终说明 dds-wrapper V4 的设计选择。
DDS 规范中的 Publisher 与 DataWriter
职责划分
DDS 规范中,Publisher 和 DataWriter 是两个不同层次的实体:
| 实体 | 职责 | 是否参与数据传输 | 是否持有 QoS |
|---|---|---|---|
Publisher | DataWriter 的工厂和生命周期容器,对属于它的 DataWriter 进行分组管理 | 否 | 否(仅作为默认 QoS 来源) |
DataWriter | 绑定到具体 Topic,实际执行数据写入,持有写入端 QoS | 是 | 是 |
关键点:QoS 设在每个 DataWriter 上,不是设在 Publisher 上。 Publisher 可以提供默认 QoS,但每个 DataWriter 创建时可以覆盖。
层级关系
一个 Publisher 下可以挂多个 DataWriter,每个 DataWriter 的 QoS 完全独立。
ROS2 (rmw_cyclonedds) 的实际做法
实体映射
ROS2 中,每个 rclcpp::Node 底层对应:
- 1 个
dds::domain::DomainParticipant - 1 个
dds::pub::Publisher(Node 级容器) - 1 个
dds::sub::Subscriber(Node 级容器) - N 个
dds::pub::DataWriter<T>(每个rclcpp::Publisher<T>创建一个) - M 个
dds::sub::DataReader<T>(每个rclcpp::Subscription<T>创建一个)
rclcpp::Node
├─ dds::DomainParticipant
├─ dds::pub::Publisher ← 一个,Node 级
│ ├─ DataWriter<TopicA> ← rclcpp::Publisher<TopicA> 创建
│ └─ DataWriter<TopicB> ← rclcpp::Publisher<TopicB> 创建
└─ dds::sub::Subscriber ← 一个,Node 级
└─ DataReader<TopicC> ← rclcpp::Subscription<TopicC> 创建 一对一映射
每个 rclcpp::Publisher<T> 恰好创建一个 DDS DataWriter<T>。不存在"一个 Publisher 对应多个 DataWriter"的情况。
ROS2 的 intra-process 优化(同进程内的零拷贝通信)走的是 ring buffer 内存直传路径,完全绕过 DDS,不会额外创建 DataWriter。
dds-wrapper V4 设计
与 ROS2 的对齐
dds-wrapper V4 采用与 ROS2 完全一致的结构:
v4::Node
├─ Backend::Participant ← DomainParticipant
├─ Backend::Publisher ← dds::pub::Publisher,一个,Node 级
│ ├─ unique_ptr<DataWriter<T1>> ← Publisher<T1>(QoS 独立)
│ └─ unique_ptr<DataWriter<T2>> ← Publisher<T2>(QoS 独立)
└─ Backend::Subscriber ← dds::sub::Subscriber,一个,Node 级
└─ unique_ptr<DataReader<T3>> ← Subscription<T3>(QoS 独立) cpp
// Node 构造时创建 DDS 级 Publisher/Subscriber 容器
Node::Node(std::string_view name, uint32_t domainId)
: _participant(Backend::createParticipant(domainId))
, _ddsPublisher(Backend::createPublisher(_participant))
, _ddsSubscriber(Backend::createSubscriber(_participant))
{}
// 每个 API Publisher 创建独立的 DataWriter,QoS 各自设定
template <typename T>
std::shared_ptr<Publisher<T>> Node::createPublisher(
std::string_view topic, const DdsQoS& qos)
{
auto resolved = resolveTopic(topic);
auto writer = Backend::createWriter<T>(
_participant, _ddsPublisher, resolved, qos); // QoS 传入 DataWriter
// ...
} 设计决策总结
| 维度 | ROS2 (rmw_cyclonedds) | dds-wrapper V4 |
|---|---|---|
| Node 级 Publisher 容器 | 1 个 | 1 个 |
rclcpp::Publisher → DataWriter | 1:1 | 1:1 |
| QoS 归属 | DataWriter 级 | DataWriter 级 |
| 多 Publisher 共享容器 | 是(同一个 Node 下) | 是(同一个 Node 下) |
| QoS 是否互相影响 | 否 | 否 |