从零实现简化 DDS 完整技术文档(含模块作用与详细注释)

从零实现简化 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 传输等功能。

相关推荐
Dream Cosmos1 小时前
C++ 多态下篇:虚函数表、动态绑定与多态底层原理
开发语言·c++
琥珀色糖10 小时前
Linux守护进程
linux·终端·守护进程·会话·进程组
敢敢のwings12 小时前
ACT-2 深度解析:家用机器人如何把“会做”推进到“可靠地做”
人工智能·机器人
朗宇芯工控13 小时前
注塑机械手自动化上下料原理、流程与应用优势
机器人·自动化·制造·工业·运动控制系统
GrepowTattu14 小时前
从2026世界机器人大会看机器人补能:为什么智能充电正在成为重要一环
人工智能·机器人
开心大爆炸14 小时前
xrdp 连接 登录对话框输入密码后闪退
linux·运维·服务器
微小冷14 小时前
ROS2 Xacro机器人建模初步教程
机器人·ros·rviz·urdf·xacro
_Twink1e15 小时前
openJiuwen 实训营 Day 1 · WorkSwarm 入门教程
开发语言·c++·openjiuwen
oushaojun215 小时前
大厂 C++ 面试:深度剖析 Linux Signal 信号机制(转)
linux