从零实现简化 DDS 完整技术文档(含模块作用与详细注释)
1. 概述
本教程通过从零实现一个简化版的 DDS(数据分发服务)中间件,深入理解其核心原理。我们不依赖任何开源 DDS 库,使用 C++17 和 POSIX socket,实现一个具备自动发现、基于 UDP 的发布/订阅系统,并给出可直接编译运行的完整代码。同时,将实现的概念与 ROS 2 中的 DDS 应用进行对照,帮助理解 ROS 2 底层通信机制。
2. DDS 核心原理
DDS 是一种以数据为中心的通信中间件,定义了发布/订阅模型,并提供了自动发现、丰富的 QoS 策略和跨平台数据交换能力。其核心实体包括:
- DomainParticipant:通信域的入口,负责创建其他实体和管理发现机制。
- Topic:数据的逻辑通道,由名称和数据类型标识。
- DataWriter:发布端实体,将数据样本写入 Topic。
- DataReader:订阅端实体,从 Topic 接收数据。
- 发现机制:自动发现网络中的其他参与者及端点,无需手动配置地址。
- 序列化:将数据转换为平台无关的字节流(如 CDR)。
- 传输:基于 UDP/TCP 进行数据传输,通常使用组播进行发现。
3. ROS 2 中 DDS 的应用
ROS 2 采用分层架构,DDS 作为中间件层,通过 rmw 接口与上层客户端库(如 rclcpp)交互。ROS 2 节点、话题、发布者、订阅者与 DDS 实体的对应关系如下:
| ROS 2 概念 | DDS 实体 | 说明 |
|---|---|---|
rclcpp::Node |
DomainParticipant | 每个节点创建一个 DDS 参与者 |
| Topic 名称 | Topic | 话题名映射为 DDS Topic(加前缀 rt/) |
Publisher |
DataWriter | 发布者底层是一个 DataWriter |
Subscription |
DataReader | 订阅者底层是一个 DataReader |
| QoS 策略 | DDS QoS | 如 reliable 映射为 RELIABLE_RELIABILITY_QOS |
| 自动发现 | SPDP/SEDP 发现协议 | 节点启动后自动发现并匹配 |
4. 从零实现简化 DDS
4.1 项目结构
dds_demo/
├── include/
│ ├── guid.h
│ ├── serialization.h
│ ├── udp_socket.h
│ ├── domain_participant.h
│ ├── topic.h
│ ├── data_writer.h
│ └── data_reader.h
├── src/
│ ├── guid.cpp
│ ├── serialization.cpp
│ ├── udp_socket.cpp
│ ├── domain_participant.cpp
│ ├── topic.cpp
│ ├── data_writer.cpp
│ └── data_reader.cpp
├── examples/
│ ├── publisher.cpp
│ └── subscriber.cpp
└── CMakeLists.txt
4.2 GUID 模块
文件作用:定义全局唯一标识符(GUID)的数据结构,用于在分布式系统中唯一标识每个 DDS 实体(参与者、DataWriter、DataReader)。GUID 是发现机制和端点匹配的基础。
include/guid.h
cpp
#pragma once
#include <cstdint>
#include <string>
/**
* @brief 全局唯一标识符(GUID)
*
* DDS 中每个实体都需要一个全局唯一的标识符。我们简化为 96 位,
* 由三个 32 位字段组成:主机标识、应用标识、实例标识。
*/
struct GUID {
uint32_t hostId; ///< 主机标识(IP 地址的整数值)
uint32_t appId; ///< 应用标识(进程 ID)
uint32_t instanceId; ///< 实例标识(递增计数器,用于区分同一进程内的多个实体)
/// 默认构造函数,所有字段初始化为 0
GUID();
/// 带参数构造函数
GUID(uint32_t host, uint32_t app, uint32_t inst);
/// 将 GUID 转换为字符串形式,便于调试输出
std::string toString() const;
/// 相等比较运算符,用于判断两个 GUID 是否相同
bool operator==(const GUID& other) const;
/// 小于比较运算符,用于作为 std::map 的键
bool operator<(const GUID& other) const;
};
src/guid.cpp
cpp
#include "guid.h"
#include <sstream>
#include <iomanip>
GUID::GUID() : hostId(0), appId(0), instanceId(0) {}
GUID::GUID(uint32_t host, uint32_t app, uint32_t inst)
: hostId(host), appId(app), instanceId(inst) {}
std::string GUID::toString() const {
std::ostringstream oss;
// 使用十六进制输出,便于阅读
oss << std::hex << hostId << "." << appId << "." << instanceId;
return oss.str();
}
bool GUID::operator==(const GUID& other) const {
return hostId == other.hostId && appId == other.appId && instanceId == other.instanceId;
}
bool GUID::operator<(const GUID& other) const {
// 按字段顺序比较,用于排序和 map 键
if (hostId != other.hostId) return hostId < other.hostId;
if (appId != other.appId) return appId < other.appId;
return instanceId < other.instanceId;
}
4.3 序列化模块
文件作用:提供数据序列化与反序列化函数,将基本数据类型(整数、字符串)转换为平台无关的大端序字节流,以便在网络中传输。这是 DDS 数据交换的基础。
include/serialization.h
cpp
#pragma once
#include <cstdint>
#include <vector>
#include <string>
#include <stdexcept>
/**
* @brief 将 16 位无符号整数以网络字节序(大端)写入缓冲区
* @param buf 目标缓冲区
* @param val 要写入的值
*/
void write_uint16_be(std::vector<uint8_t>& buf, uint16_t val);
/**
* @brief 将 32 位无符号整数以网络字节序(大端)写入缓冲区
* @param buf 目标缓冲区
* @param val 要写入的值
*/
void write_uint32_be(std::vector<uint8_t>& buf, uint32_t val);
/**
* @brief 将字符串写入缓冲区(先写长度,再写内容)
* @param buf 目标缓冲区
* @param str 要写入的字符串
*/
void write_string(std::vector<uint8_t>& buf, const std::string& str);
/**
* @brief 从缓冲区读取 16 位无符号整数(大端),并推进 offset
* @param buf 源缓冲区
* @param offset 当前读取位置,读取后自动前进
* @return 读取的值
*/
uint16_t read_uint16_be(const std::vector<uint8_t>& buf, size_t& offset);
/**
* @brief 从缓冲区读取 32 位无符号整数(大端),并推进 offset
* @param buf 源缓冲区
* @param offset 当前读取位置,读取后自动前进
* @return 读取的值
*/
uint32_t read_uint32_be(const std::vector<uint8_t>& buf, size_t& offset);
/**
* @brief 从缓冲区读取字符串(先读长度,再读内容),并推进 offset
* @param buf 源缓冲区
* @param offset 当前读取位置,读取后自动前进
* @return 读取的字符串
*/
std::string read_string(const std::vector<uint8_t>& buf, size_t& offset);
src/serialization.cpp
cpp
#include "serialization.h"
#include <cstring>
#include <arpa/inet.h>
void write_uint16_be(std::vector<uint8_t>& buf, uint16_t val) {
// 将主机字节序转换为网络字节序(大端)
uint16_t net_val = htons(val);
// 将 2 字节数据追加到缓冲区
buf.insert(buf.end(), reinterpret_cast<uint8_t*>(&net_val),
reinterpret_cast<uint8_t*>(&net_val) + 2);
}
void write_uint32_be(std::vector<uint8_t>& buf, uint32_t val) {
uint32_t net_val = htonl(val);
buf.insert(buf.end(), reinterpret_cast<uint8_t*>(&net_val),
reinterpret_cast<uint8_t*>(&net_val) + 4);
}
void write_string(std::vector<uint8_t>& buf, const std::string& str) {
// 先写入字符串长度(4 字节)
write_uint32_be(buf, static_cast<uint32_t>(str.size()));
// 再写入字符串内容
buf.insert(buf.end(), str.begin(), str.end());
}
uint16_t read_uint16_be(const std::vector<uint8_t>& buf, size_t& offset) {
// 检查是否有足够的数据可读
if (offset + 2 > buf.size()) throw std::runtime_error("Buffer underflow");
uint16_t val;
memcpy(&val, buf.data() + offset, 2);
offset += 2;
// 将网络字节序转换回主机字节序
return ntohs(val);
}
uint32_t read_uint32_be(const std::vector<uint8_t>& buf, size_t& offset) {
if (offset + 4 > buf.size()) throw std::runtime_error("Buffer underflow");
uint32_t val;
memcpy(&val, buf.data() + offset, 4);
offset += 4;
return ntohl(val);
}
std::string read_string(const std::vector<uint8_t>& buf, size_t& offset) {
uint32_t len = read_uint32_be(buf, offset);
if (offset + len > buf.size()) throw std::runtime_error("Buffer underflow");
// 从缓冲区中截取字符串内容
std::string str(buf.begin() + offset, buf.begin() + offset + len);
offset += len;
return str;
}
4.4 UDP Socket 封装
文件作用:封装 POSIX UDP socket 操作,提供绑定端口、发送数据、接收数据、加入组播组和设置接收超时等功能。UDP 是 DDS 传输层常用的协议,组播用于自动发现。
include/udp_socket.h
cpp
#pragma once
#include <string>
#include <cstdint>
/**
* @brief UDP Socket 的简单封装类
*
* 支持绑定端口、发送/接收数据、加入组播组、设置超时等操作。
*/
class UDPSocket {
public:
UDPSocket();
~UDPSocket();
/**
* @brief 绑定到指定端口
* @param port 端口号,0 表示随机端口
* @return 成功返回 true
*/
bool bind(uint16_t port);
/**
* @brief 加入组播组
* @param group_ip 组播地址
* @param port 组播端口
* @return 成功返回 true
*/
bool join_multicast(const std::string& group_ip, uint16_t port);
/**
* @brief 发送数据到指定地址
* @param data 数据指针
* @param len 数据长度
* @param ip 目标 IP
* @param port 目标端口
* @return 发送的字节数,失败返回 -1
*/
int send_to(const void* data, size_t len, const std::string& ip, uint16_t port);
/**
* @brief 接收数据
* @param buffer 接收缓冲区
* @param len 缓冲区大小
* @param src_ip 输出参数,源 IP
* @param src_port 输出参数,源端口
* @return 接收的字节数,失败返回 -1
*/
int recv_from(void* buffer, size_t len, std::string& src_ip, uint16_t& src_port);
/**
* @brief 设置接收超时
* @param ms 超时毫秒数
*/
void set_recv_timeout(int ms);
/**
* @brief 获取本地绑定的端口号
* @return 端口号
*/
uint16_t get_port() const;
private:
int sockfd_; ///< socket 文件描述符
uint16_t port_; ///< 绑定的端口
};
src/udp_socket.cpp
cpp
#include "udp_socket.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
UDPSocket::UDPSocket() : sockfd_(-1), port_(0) {}
UDPSocket::~UDPSocket() {
if (sockfd_ >= 0) close(sockfd_);
}
bool UDPSocket::bind(uint16_t port) {
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd_ < 0) return false;
// 允许端口复用,便于多个进程绑定相同端口(组播场景)
int reuse = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (::bind(sockfd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
close(sockfd_);
return false;
}
// 获取实际绑定的端口(当 port=0 时由系统分配)
socklen_t len = sizeof(addr);
getsockname(sockfd_, reinterpret_cast<sockaddr*>(&addr), &len);
port_ = ntohs(addr.sin_port);
return true;
}
bool UDPSocket::join_multicast(const std::string& group_ip, uint16_t port) {
ip_mreq mreq;
// 组播组地址
mreq.imr_multiaddr.s_addr = inet_addr(group_ip.c_str());
// 使用默认网卡
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(sockfd_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
return false;
return true;
}
int UDPSocket::send_to(const void* data, size_t len, const std::string& ip, uint16_t port) {
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &addr.sin_addr);
return sendto(sockfd_, data, len, 0, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
}
int UDPSocket::recv_from(void* buffer, size_t len, std::string& src_ip, uint16_t& src_port) {
sockaddr_in addr;
socklen_t addr_len = sizeof(addr);
int n = recvfrom(sockfd_, buffer, len, 0, reinterpret_cast<sockaddr*>(&addr), &addr_len);
if (n > 0) {
char ip_buf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));
src_ip = ip_buf;
src_port = ntohs(addr.sin_port);
}
return n;
}
void UDPSocket::set_recv_timeout(int ms) {
timeval tv;
tv.tv_sec = ms / 1000;
tv.tv_usec = (ms % 1000) * 1000;
setsockopt(sockfd_, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
}
uint16_t UDPSocket::get_port() const {
return port_;
}
4.5 Topic 模块
文件作用:定义 Topic 类,用于标识数据的逻辑通道,包含名称和类型名。发布者和订阅者通过相同的 Topic 名称和类型进行匹配。
include/topic.h
cpp
#pragma once
#include <string>
/**
* @brief Topic 类,表示一个数据主题
*
* 包含主题名称和类型名称,用于发布者与订阅者之间的匹配。
*/
class Topic {
public:
Topic(const std::string& name, const std::string& type_name);
/// 获取主题名称
const std::string& get_name() const;
/// 获取类型名称
const std::string& get_type_name() const;
private:
std::string name_; ///< 主题名称
std::string type_name_; ///< 类型名称
};
src/topic.cpp
cpp
#include "topic.h"
Topic::Topic(const std::string& name, const std::string& type_name)
: name_(name), type_name_(type_name) {}
const std::string& Topic::get_name() const { return name_; }
const std::string& Topic::get_type_name() const { return type_name_; }
4.6 DomainParticipant 与发现机制
文件作用:DomainParticipant 是 DDS 通信的入口,负责创建 Topic、DataWriter、DataReader,并实现简化的自动发现机制。发现机制基于 UDP 组播:参与者周期性向组播地址发送公告,同时监听组播;发现新参与者后,通过单播交换端点列表,从而建立匹配关系。同时管理本地和远程端点信息。
include/domain_participant.h
cpp
#pragma once
#include <map>
#include <vector>
#include <thread>
#include <atomic>
#include <mutex>
#include <string>
#include <memory>
#include <functional>
#include "guid.h"
#include "udp_socket.h"
class Topic;
class DataWriter;
class DataReader;
/**
* @brief 远程端点信息结构体
*
* 描述一个远程 DataWriter 或 DataReader 的详细信息,用于匹配和数据传输。
*/
struct RemoteEndpoint {
GUID guid; ///< 端点 GUID
std::string topic_name; ///< 主题名称
std::string type_name; ///< 类型名称
bool is_writer; ///< 是否为 DataWriter(否则为 DataReader)
std::string ip; ///< 端点所在主机的 IP
uint16_t port; ///< 端点监听的端口
};
/**
* @brief DomainParticipant 类,代表一个 DDS 域参与者
*
* 负责创建实体、管理发现机制、维护已知端点和匹配关系。
*/
class DomainParticipant {
public:
/**
* @param domain_id 域 ID,用于逻辑隔离(当前实现未实际使用)
*/
DomainParticipant(int domain_id);
~DomainParticipant();
/**
* @brief 创建主题
*/
Topic* create_topic(const std::string& name, const std::string& type_name);
/**
* @brief 创建数据写入者
*/
DataWriter* create_datawriter(Topic* topic);
/**
* @brief 创建数据读取者
* @param callback 收到数据时的回调函数
*/
DataReader* create_datareader(Topic* topic, std::function<void(const std::vector<uint8_t>&)> callback);
/// 获取参与者 GUID
const GUID& get_guid() const;
/// 获取本机 IP
const std::string& get_ip() const;
/**
* @brief 获取与指定主题匹配的远程 DataWriter 列表
*/
std::vector<RemoteEndpoint> get_matched_writers(const std::string& topic_name);
/**
* @brief 获取与指定主题匹配的远程 DataReader 列表
*/
std::vector<RemoteEndpoint> get_matched_readers(const std::string& topic_name);
/**
* @brief 添加本地 DataWriter 端点
*/
void add_local_writer(const RemoteEndpoint& ep);
/**
* @brief 添加本地 DataReader 端点
*/
void add_local_reader(const RemoteEndpoint& ep);
private:
/// 发现线程主循环
void discovery_loop();
/// 向指定地址发送本地端点列表
void send_endpoints_list(const std::string& ip, uint16_t port);
/// 处理收到的发现消息
void handle_discovery_message(const std::vector<uint8_t>& msg, const std::string& src_ip, uint16_t src_port);
int domain_id_; ///< 域 ID(目前未使用)
GUID guid_; ///< 参与者自身的 GUID
std::string ip_; ///< 本机 IP
std::unique_ptr<UDPSocket> discovery_socket_; ///< 用于发现的 UDP socket(绑定固定组播端口)
std::thread discovery_thread_; ///< 发现线程
std::atomic<bool> running_; ///< 线程运行标志
std::mutex mutex_; ///< 保护共享数据
std::map<std::string, Topic*> topics_; ///< 已创建的主题
std::vector<DataWriter*> writers_; ///< 已创建的 DataWriter 列表
std::vector<DataReader*> readers_; ///< 已创建的 DataReader 列表
std::map<GUID, std::pair<std::string, uint16_t>> remote_participants_; ///< 远程参与者 GUID -> (IP, 发现端口)
std::vector<RemoteEndpoint> local_writers_; ///< 本地 DataWriter 端点
std::vector<RemoteEndpoint> local_readers_; ///< 本地 DataReader 端点
std::vector<RemoteEndpoint> remote_writers_; ///< 远程 DataWriter 端点
std::vector<RemoteEndpoint> remote_readers_; ///< 远程 DataReader 端点
};
src/domain_participant.cpp
cpp
#include "domain_participant.h"
#include "topic.h"
#include "data_writer.h"
#include "data_reader.h"
#include "serialization.h"
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <cstring>
#include <chrono>
// 组播地址和端口(所有参与者共享)
const std::string MULTICAST_GROUP = "239.255.0.1";
const uint16_t MULTICAST_PORT = 7400;
// 消息类型定义
const uint32_t MSG_ANNOUNCE = 0x01; // 参与者公告
const uint32_t MSG_ENDPOINTS = 0x02; // 端点列表
const uint32_t MSG_DATA = 0x03; // 用户数据(在 DataWriter/DataReader 中使用)
DomainParticipant::DomainParticipant(int domain_id) : domain_id_(domain_id), running_(true) {
// 获取本机 IP,此处简化为 127.0.0.1,若需多机通信请改为实际网卡 IP
ip_ = "127.0.0.1";
// 生成 GUID:hostId 使用 IP 的整数值,appId 使用进程 ID
struct in_addr addr;
inet_pton(AF_INET, ip_.c_str(), &addr);
uint32_t host = ntohl(addr.s_addr);
guid_ = GUID(host, static_cast<uint32_t>(getpid()), 0);
// 创建发现 socket,绑定固定组播端口
discovery_socket_ = std::make_unique<UDPSocket>();
if (!discovery_socket_->bind(MULTICAST_PORT)) {
std::cerr << "Failed to bind discovery socket" << std::endl;
exit(1);
}
// 加入组播组,用于接收其他参与者的公告
discovery_socket_->join_multicast(MULTICAST_GROUP, MULTICAST_PORT);
// 启动发现线程
discovery_thread_ = std::thread(&DomainParticipant::discovery_loop, this);
}
DomainParticipant::~DomainParticipant() {
running_ = false;
if (discovery_thread_.joinable()) discovery_thread_.join();
}
Topic* DomainParticipant::create_topic(const std::string& name, const std::string& type_name) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = topics_.find(name);
if (it != topics_.end()) return it->second;
Topic* topic = new Topic(name, type_name);
topics_[name] = topic;
return topic;
}
const GUID& DomainParticipant::get_guid() const { return guid_; }
const std::string& DomainParticipant::get_ip() const { return ip_; }
void DomainParticipant::add_local_writer(const RemoteEndpoint& ep) {
std::lock_guard<std::mutex> lock(mutex_);
local_writers_.push_back(ep);
}
void DomainParticipant::add_local_reader(const RemoteEndpoint& ep) {
std::lock_guard<std::mutex> lock(mutex_);
local_readers_.push_back(ep);
}
std::vector<RemoteEndpoint> DomainParticipant::get_matched_writers(const std::string& topic_name) {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<RemoteEndpoint> result;
for (auto& ep : remote_writers_) {
if (ep.topic_name == topic_name) result.push_back(ep);
}
return result;
}
std::vector<RemoteEndpoint> DomainParticipant::get_matched_readers(const std::string& topic_name) {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<RemoteEndpoint> result;
for (auto& ep : remote_readers_) {
if (ep.topic_name == topic_name) result.push_back(ep);
}
return result;
}
void DomainParticipant::discovery_loop() {
const int announce_interval_ms = 1000; // 公告发送周期
auto last_announce = std::chrono::steady_clock::now();
char buffer[65535];
while (running_) {
// 周期性发送参与者公告
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_announce).count() >= announce_interval_ms) {
// 构造公告消息
std::vector<uint8_t> msg;
write_uint32_be(msg, 0x44534453); // 魔数
write_uint32_be(msg, MSG_ANNOUNCE);
size_t len_pos = msg.size();
write_uint32_be(msg, 0); // 长度占位
write_uint32_be(msg, guid_.hostId);
write_uint32_be(msg, guid_.appId);
write_uint32_be(msg, guid_.instanceId);
// 写入发现端口,以便对方回复端点列表
write_uint16_be(msg, discovery_socket_->get_port());
// 回填长度字段
uint32_t payload_len = msg.size() - len_pos - 4;
msg[len_pos] = (payload_len >> 24) & 0xFF;
msg[len_pos+1] = (payload_len >> 16) & 0xFF;
msg[len_pos+2] = (payload_len >> 8) & 0xFF;
msg[len_pos+3] = payload_len & 0xFF;
// 发送到组播地址
discovery_socket_->send_to(msg.data(), msg.size(), MULTICAST_GROUP, MULTICAST_PORT);
last_announce = now;
}
// 接收组播消息
std::string src_ip;
uint16_t src_port;
int n = discovery_socket_->recv_from(buffer, sizeof(buffer), src_ip, src_port);
if (n > 0) {
std::vector<uint8_t> msg(buffer, buffer + n);
handle_discovery_message(msg, src_ip, src_port);
}
// 短暂休眠,避免 CPU 忙等
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void DomainParticipant::handle_discovery_message(const std::vector<uint8_t>& msg, const std::string& src_ip, uint16_t src_port) {
if (msg.size() < 12) return;
size_t offset = 0;
uint32_t magic = read_uint32_be(msg, offset);
if (magic != 0x44534453) return;
uint32_t type = read_uint32_be(msg, offset);
uint32_t length = read_uint32_be(msg, offset);
if (msg.size() < offset + length) return;
if (type == MSG_ANNOUNCE) {
// 收到参与者公告,向对方单播发送自己的端点列表
send_endpoints_list(src_ip, src_port);
} else if (type == MSG_ENDPOINTS) {
// 收到端点列表,解析并存储远程端点
uint32_t count = read_uint32_be(msg, offset);
for (uint32_t i = 0; i < count; ++i) {
RemoteEndpoint ep;
ep.guid.hostId = read_uint32_be(msg, offset);
ep.guid.appId = read_uint32_be(msg, offset);
ep.guid.instanceId = read_uint32_be(msg, offset);
ep.topic_name = read_string(msg, offset);
ep.type_name = read_string(msg, offset);
ep.is_writer = (msg[offset++] != 0);
ep.ip = read_string(msg, offset);
ep.port = read_uint16_be(msg, offset);
std::lock_guard<std::mutex> lock(mutex_);
// 去重并存储
bool exists = false;
if (ep.is_writer) {
for (auto& w : remote_writers_) {
if (w.guid == ep.guid) { exists = true; break; }
}
if (!exists) remote_writers_.push_back(ep);
} else {
for (auto& r : remote_readers_) {
if (r.guid == ep.guid) { exists = true; break; }
}
if (!exists) remote_readers_.push_back(ep);
}
}
}
}
void DomainParticipant::send_endpoints_list(const std::string& ip, uint16_t port) {
std::vector<uint8_t> msg;
write_uint32_be(msg, 0x44534453);
write_uint32_be(msg, MSG_ENDPOINTS);
size_t len_pos = msg.size();
write_uint32_be(msg, 0); // 长度占位
std::lock_guard<std::mutex> lock(mutex_);
uint32_t count = local_writers_.size() + local_readers_.size();
write_uint32_be(msg, count);
// 写入所有本地 DataWriter
for (auto& ep : local_writers_) {
write_uint32_be(msg, ep.guid.hostId);
write_uint32_be(msg, ep.guid.appId);
write_uint32_be(msg, ep.guid.instanceId);
write_string(msg, ep.topic_name);
write_string(msg, ep.type_name);
msg.push_back(1); // is_writer = true
write_string(msg, ep.ip);
write_uint16_be(msg, ep.port);
}
// 写入所有本地 DataReader
for (auto& ep : local_readers_) {
write_uint32_be(msg, ep.guid.hostId);
write_uint32_be(msg, ep.guid.appId);
write_uint32_be(msg, ep.guid.instanceId);
write_string(msg, ep.topic_name);
write_string(msg, ep.type_name);
msg.push_back(0); // is_writer = false
write_string(msg, ep.ip);
write_uint16_be(msg, ep.port);
}
// 回填长度字段
uint32_t payload_len = msg.size() - len_pos - 4;
msg[len_pos] = (payload_len >> 24) & 0xFF;
msg[len_pos+1] = (payload_len >> 16) & 0xFF;
msg[len_pos+2] = (payload_len >> 8) & 0xFF;
msg[len_pos+3] = payload_len & 0xFF;
// 单播发送到指定地址
discovery_socket_->send_to(msg.data(), msg.size(), ip, port);
}
DataWriter* DomainParticipant::create_datawriter(Topic* topic) {
// 生成 writer GUID:使用参与者 GUID 并修改 instanceId
GUID writer_guid = guid_;
writer_guid.instanceId = writers_.size() + 1;
// 为 writer 创建独立的 UDP socket
auto socket = std::make_unique<UDPSocket>();
if (!socket->bind(0)) {
std::cerr << "Failed to bind writer socket" << std::endl;
return nullptr;
}
uint16_t port = socket->get_port();
// 构建本地端点信息
RemoteEndpoint ep;
ep.guid = writer_guid;
ep.topic_name = topic->get_name();
ep.type_name = topic->get_type_name();
ep.is_writer = true;
ep.ip = ip_;
ep.port = port;
add_local_writer(ep);
// 创建 DataWriter 对象
DataWriter* writer = new DataWriter(this, topic, writer_guid, std::move(socket));
writers_.push_back(writer);
return writer;
}
DataReader* DomainParticipant::create_datareader(Topic* topic, std::function<void(const std::vector<uint8_t>&)> callback) {
// 生成 reader GUID
GUID reader_guid = guid_;
reader_guid.instanceId = readers_.size() + 1000;
// 创建独立的 socket
auto socket = std::make_unique<UDPSocket>();
if (!socket->bind(0)) {
std::cerr << "Failed to bind reader socket" << std::endl;
return nullptr;
}
uint16_t port = socket->get_port();
// 构建本地端点信息
RemoteEndpoint ep;
ep.guid = reader_guid;
ep.topic_name = topic->get_name();
ep.type_name = topic->get_type_name();
ep.is_writer = false;
ep.ip = ip_;
ep.port = port;
add_local_reader(ep);
// 创建 DataReader 对象
DataReader* reader = new DataReader(this, topic, reader_guid, std::move(socket), callback);
readers_.push_back(reader);
return reader;
}
4.7 DataWriter 与 DataReader
文件作用:DataWriter 负责将用户数据序列化后发送给所有匹配的远程 DataReader;DataReader 在独立线程中持续接收数据,解析后调用用户回调。两者都持有独立的 UDP socket。
include/data_writer.h
cpp
#pragma once
#include "guid.h"
#include "udp_socket.h"
#include <vector>
#include <memory>
class DomainParticipant;
class Topic;
/**
* @brief DataWriter 类,发布端实体
*
* 负责将用户数据发送给所有匹配的远程 DataReader。
*/
class DataWriter {
public:
DataWriter(DomainParticipant* participant, Topic* topic, const GUID& guid, std::unique_ptr<UDPSocket> socket);
/**
* @brief 发布数据
* @param data 已经序列化好的数据字节流
*/
void write(const std::vector<uint8_t>& data);
private:
DomainParticipant* participant_; ///< 所属参与者
Topic* topic_; ///< 绑定的主题
GUID guid_; ///< 自身 GUID
std::unique_ptr<UDPSocket> socket_; ///< 用于发送数据的 socket
};
src/data_writer.cpp
cpp
#include "data_writer.h"
#include "domain_participant.h"
#include "topic.h"
#include "serialization.h"
#include <iostream>
DataWriter::DataWriter(DomainParticipant* participant, Topic* topic, const GUID& guid, std::unique_ptr<UDPSocket> socket)
: participant_(participant), topic_(topic), guid_(guid), socket_(std::move(socket)) {}
void DataWriter::write(const std::vector<uint8_t>& data) {
// 获取与主题匹配的远程 DataReader 列表
auto readers = participant_->get_matched_readers(topic_->get_name());
if (readers.empty()) return; // 没有订阅者,直接返回
// 构造数据消息
std::vector<uint8_t> msg;
write_uint32_be(msg, 0x44534453); // 魔数
write_uint32_be(msg, 0x03); // MSG_DATA
size_t len_pos = msg.size();
write_uint32_be(msg, 0); // 长度占位
// 写入 Writer GUID
write_uint32_be(msg, guid_.hostId);
write_uint32_be(msg, guid_.appId);
write_uint32_be(msg, guid_.instanceId);
// 写入数据长度
write_uint32_be(msg, data.size());
// 写入数据内容
msg.insert(msg.end(), data.begin(), data.end());
// 回填长度字段
uint32_t payload_len = msg.size() - len_pos - 4;
msg[len_pos] = (payload_len >> 24) & 0xFF;
msg[len_pos+1] = (payload_len >> 16) & 0xFF;
msg[len_pos+2] = (payload_len >> 8) & 0xFF;
msg[len_pos+3] = payload_len & 0xFF;
// 向每个匹配的 Reader 发送数据
for (auto& reader : readers) {
socket_->send_to(msg.data(), msg.size(), reader.ip, reader.port);
}
}
include/data_reader.h
cpp
#pragma once
#include "guid.h"
#include "udp_socket.h"
#include <vector>
#include <functional>
#include <thread>
#include <atomic>
#include <memory>
class DomainParticipant;
class Topic;
/**
* @brief DataReader 类,订阅端实体
*
* 负责接收来自远程 DataWriter 的数据,并通过回调通知用户。
*/
class DataReader {
public:
DataReader(DomainParticipant* participant, Topic* topic, const GUID& guid, std::unique_ptr<UDPSocket> socket,
std::function<void(const std::vector<uint8_t>&)> callback);
~DataReader();
private:
/// 接收线程主循环
void receive_loop();
DomainParticipant* participant_; ///< 所属参与者
Topic* topic_; ///< 绑定的主题
GUID guid_; ///< 自身 GUID
std::unique_ptr<UDPSocket> socket_; ///< 用于接收数据的 socket
std::function<void(const std::vector<uint8_t>&)> callback_; ///< 用户回调
std::thread recv_thread_; ///< 接收线程
std::atomic<bool> running_; ///< 线程运行标志
};
src/data_reader.cpp
cpp
#include "data_reader.h"
#include "serialization.h"
#include <iostream>
DataReader::DataReader(DomainParticipant* participant, Topic* topic, const GUID& guid,
std::unique_ptr<UDPSocket> socket,
std::function<void(const std::vector<uint8_t>&)> callback)
: participant_(participant), topic_(topic), guid_(guid),
socket_(std::move(socket)), callback_(callback), running_(true) {
// 启动接收线程
recv_thread_ = std::thread(&DataReader::receive_loop, this);
}
DataReader::~DataReader() {
running_ = false;
if (recv_thread_.joinable()) recv_thread_.join();
}
void DataReader::receive_loop() {
char buffer[65535];
while (running_) {
std::string src_ip;
uint16_t src_port;
int n = socket_->recv_from(buffer, sizeof(buffer), src_ip, src_port);
if (n <= 0) continue;
// 解析消息
std::vector<uint8_t> msg(buffer, buffer + n);
if (msg.size() < 12) continue;
size_t offset = 0;
uint32_t magic = read_uint32_be(msg, offset);
if (magic != 0x44534453) continue;
uint32_t type = read_uint32_be(msg, offset);
uint32_t length = read_uint32_be(msg, offset);
if (msg.size() < offset + length) continue;
if (type == 0x03) { // 数据消息
// 跳过 Writer GUID(12 字节)
offset += 12;
uint32_t data_len = read_uint32_be(msg, offset);
// 提取用户数据
std::vector<uint8_t> data(msg.begin() + offset, msg.begin() + offset + data_len);
// 调用用户回调
if (callback_) callback_(data);
}
// 忽略其他消息类型
}
}
4.8 示例程序
文件作用:演示如何使用我们实现的 DDS API 进行发布和订阅。发布者每秒发送一条消息,订阅者通过回调打印接收到的数据。
examples/publisher.cpp
cpp
#include "domain_participant.h"
#include "data_writer.h"
#include "serialization.h"
#include <iostream>
#include <chrono>
#include <thread>
#include <vector>
#include <string>
// 自定义数据结构
struct MyData {
std::string message;
int count;
};
// 序列化 MyData 到字节流
void serialize_my_data(const MyData& data, std::vector<uint8_t>& buf) {
write_string(buf, data.message); // 写入字符串字段
write_uint32_be(buf, data.count); // 写入整数字段
}
int main() {
// 创建参与者
DomainParticipant participant(0);
// 创建主题
Topic* topic = participant.create_topic("MyTopic", "MyData");
// 创建 DataWriter
DataWriter* writer = participant.create_datawriter(topic);
MyData data;
int counter = 0;
while (true) {
data.message = "Hello DDS! Count=" + std::to_string(counter);
data.count = counter;
std::vector<uint8_t> buffer;
serialize_my_data(data, buffer); // 序列化
writer->write(buffer); // 发布
std::cout << "Sent: " << data.message << std::endl;
counter++;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 0;
}
examples/subscriber.cpp
cpp
#include "domain_participant.h"
#include "data_reader.h"
#include "serialization.h"
#include <iostream>
#include <thread>
#include <chrono>
#include <vector>
#include <string>
// 自定义数据结构
struct MyData {
std::string message;
int count;
};
// 从字节流反序列化 MyData
void deserialize_my_data(const std::vector<uint8_t>& buffer, MyData& data) {
size_t offset = 0;
data.message = read_string(buffer, offset); // 读取字符串
data.count = read_uint32_be(buffer, offset); // 读取整数
}
int main() {
// 创建参与者
DomainParticipant participant(0);
// 创建主题
Topic* topic = participant.create_topic("MyTopic", "MyData");
// 定义回调函数
auto callback = [](const std::vector<uint8_t>& data) {
MyData mydata;
deserialize_my_data(data, mydata);
std::cout << "Received: " << mydata.message << ", count=" << mydata.count << std::endl;
};
// 创建 DataReader
DataReader* reader = participant.create_datareader(topic, callback);
// 保持程序运行
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 0;
}
4.9 CMakeLists.txt
文件作用:构建配置文件,用于编译发布者和订阅者两个可执行程序。
cmake
cmake_minimum_required(VERSION 3.10)
project(dds_demo)
# 使用 C++17 标准
set(CMAKE_CXX_STANDARD 17)
# 头文件搜索路径
include_directories(include)
# 编译发布者和订阅者(包含 src 目录下的所有 .cpp 文件)
add_executable(publisher examples/publisher.cpp src/*.cpp)
add_executable(subscriber examples/subscriber.cpp src/*.cpp)
# 链接线程库
find_package(Threads REQUIRED)
target_link_libraries(publisher Threads::Threads)
target_link_libraries(subscriber Threads::Threads)
4.10 编译与运行
bash
mkdir build
cd build
cmake ..
make
打开两个终端,分别运行:
bash
./publisher
bash
./subscriber
你将看到发布者每秒发送一条消息,订阅者实时接收并打印。
5. 总结
通过从零实现简化 DDS,我们深入理解了 DDS 的核心机制:参与者、主题、读写者、基于 UDP 组播的自动发现、序列化和 UDP 数据传输。这些概念与 ROS 2 中的节点、话题、发布/订阅一一对应。理解底层原理有助于调试 ROS 2 通信问题、优化 QoS 配置以及选择 DDS 实现。你可以在此基础上继续扩展可靠传输、更多 QoS 策略、TCP 传输等功能。