资讯动态

OpenDDS 发布订阅示例拆解:从 IDL 定义到数据收发的完整链路

发布时间:2026/10/21 16:16:21 来源:尧图企业网站定制
简介本资源是一套面向分布式通信初学者的 OpenDDS 入门示例程序围绕 DDS 发布/订阅模型展开帮助读者理解实时数据分发服务的基本用法。包内包含 Publisher、Subscriber、Topic、DataWriter/DataReader 等核心组件的实现代码并配有 idl 接口定义、QoS 策略配置、代码生成产物及传输层与自动发现机制相关文件注释清晰便于对照学习。资源共 172 个文件以 cpp、h、idl 源码为主辅以 obj、pdb、tlog 等编译中间产物和少量 dll、exe 可执行文件压缩包约 199.85MB目录结构完整可直接编译运行调试。目前已有 3256 人学习下载适合希望快速上手 OpenDDS、掌握发布订阅通信流程与 QoS 配置的开发者参考实践。1. 从一份能跑通的 OpenDDS 示例说起很多人第一次接触 DDS都是被它的概念劝退的域、主题、发布订阅、QoS 策略一大堆文档翻半天还是不知道从哪下手。这份 OpenDDS 示例程序的价值就在这儿——它把一套完整的发布订阅链路拆成了能直接编译运行的代码注释写得足够细你不需要先去啃几百页规范打开工程就能看到数据从发布端怎么发出去、订阅端怎么收回来。它适合两类人一类是刚接触分布式实时通信、想找个最小可运行例子摸清 OpenDDS 骨架的开发者另一类是手里已经有 OpenDDS 环境但需要一份结构清晰、方便改造成自己业务模型的参考代码。下面我就按「先看清结构、再跑起来、最后动手改」的顺序把这份示例拆一遍。2. 项目结构与技术栈IDL、发布端、订阅端三件套2.1 目录组织与文件职责这份示例的目录结构是典型的 OpenDDS 工程布局核心文件不多但每个都有明确分工。常见做法是分成 IDL 定义、发布端、订阅端、构建脚本四块OpenDDS_Demo/ ├── IDL/ │ └── DemoMessage.idl # 数据类型定义发布订阅双方共用 ├── Publisher/ │ ├── Publisher.cpp # 发布端主逻辑 │ └── DataWriterListener.h # 写入端监听器 ├── Subscriber/ │ ├── Subscriber.cpp # 订阅端主逻辑 │ └── DataReaderListener.h # 读取端监听器 ├── run_test.pl # 一键启动脚本 └── CMakeLists.txt # 构建配置IDL 文件是整个工程的契约发布端和订阅端都依赖它生成的数据类型。Publisher 和 Subscriber 各自独立编译通过主题名在运行时对接。run_test.pl 负责按顺序拉起进程避免手动开两个终端。CMakeLists.txt 里会调用 OpenDDS 提供的opendds_target_sources宏把 IDL 自动编译成 C 类型支持代码。提示不同版本的 OpenDDS 对 CMake 宏的支持程度不一样如果构建时报找不到opendds_target_sources先确认你的 OpenDDS 是用 CMake 方式安装的而不是仅编译了源码。2.2 技术栈与依赖关系这份示例的技术栈很纯粹C 作为实现语言IDL 作为接口描述语言OpenDDS 作为运行时中间件底层走的是 RTPS 协议。它不依赖任何图形界面或数据库编译产物就是两个可执行文件加若干动态库。组件作用是否必须OpenDDS 运行时库提供 DDS 核心实现必须TAO/ACEOpenDDS 底层依赖必须Perl运行 run_test.pl 脚本可选CMake构建工程推荐依赖关系上OpenDDS 本身依赖 ACE 和 TAO所以编译前必须先把这两个基础库装好。示例代码里不会重复这些底层配置它假设你已经有一个可用的 OpenDDS 环境。如果你用的是源码编译方式记得先设置好DDS_ROOT和ACE_ROOT环境变量否则 CMake 配置阶段就会报找不到头文件。2.3 IDL 定义数据类型的唯一来源先看 IDL 文件它决定了发布端和订阅端能交换什么数据。示例里通常定义一个简单的结构体// DemoMessage.idl module Demo { struct Message { long id; // 消息序号 string content; // 消息正文 double timestamp; // 发送时间戳 }; #pragma DCPS_DATA_TYPE Demo::Message #pragma DCPS_DATA_KEY Demo::Message id };这里有两个关键点。第一#pragma DCPS_DATA_TYPE告诉 OpenDDS 这个结构体是一个可传输的数据类型没有这行编译不会报错但运行时注册类型会失败。第二DCPS_DATA_KEY指定了id作为键意味着相同 id 的消息会被视为同一实例的更新而不是新消息。如果你希望每条消息都是独立实例把 key 去掉或者换成一个每次递增的字段。逻辑说明IDL 编译后会生成MessageTypeSupport、MessageDataWriter、MessageDataReader等类发布端用 DataWriter 写数据订阅端用 DataReader 读数据。参数上long对应 32 位整型string是变长字符串double是双精度浮点。改数据类型时IDL 改完必须重新编译否则两端类型不一致会导致运行时匹配失败。3. 运行链路拆解从初始化到数据收发3.1 发布端初始化流程发布端的启动过程可以拆成六步每一步都有对应的 API 调用。示例代码里注释会标出每步的作用我按顺序讲一遍// Publisher.cpp 核心初始化片段 DDS::DomainParticipantFactory_var dpf TheParticipantFactoryWithArgs(argc, argv); // 1. 创建参与者加入域 42 DDS::DomainParticipant_var participant dpf-create_participant(42, PARTICIPANT_QOS_DEFAULT, 0, OpenDDS::DCPS::DEFAULT_STATUS_MASK); // 2. 注册数据类型 Demo::MessageTypeSupport_var ts new Demo::MessageTypeSupportImpl(); ts-register_type(participant, ); // 3. 创建主题 DDS::Topic_var topic participant-create_topic( DemoTopic, ts-get_type_name(), TOPIC_QOS_DEFAULT, 0, OpenDDS::DCPS::DEFAULT_STATUS_MASK); // 4. 创建发布者 DDS::Publisher_var publisher participant-create_publisher( PUBLISHER_QOS_DEFAULT, 0, OpenDDS::DCPS::DEFAULT_STATUS_MASK); // 5. 创建 DataWriter DDS::DataWriter_var writer publisher-create_datawriter( topic, DATAWRITER_QOS_DEFAULT, 0, OpenDDS::DCPS::DEFAULT_STATUS_MASK); // 6. 窄化到具体类型 Demo::MessageDataWriter_var message_writer Demo::MessageDataWriter::_narrow(writer);逻辑说明域 ID 42 是示例里常用的值发布端和订阅端必须一致否则互相发现不了。register_type传入空字符串表示使用 IDL 里定义的默认类型名。create_topic的主题名DemoTopic也是两端约定的关键。窄化操作是把通用 DataWriter 转成具体类型的写入接口只有窄化成功后才能调用write()方法。参数说明PARTICIPANT_QOS_DEFAULT表示使用默认 QoS示例里不刻意配置 QoS 是为了降低理解成本。实际项目中如果对可靠性、持久性有要求需要在这里传入自定义 QoS 结构体。DEFAULT_STATUS_MASK是状态掩码示例里用默认值即可。3.2 订阅端初始化与监听器订阅端的前五步和发布端几乎对称区别在第六步创建的是 DataReader并且要挂一个监听器// Subscriber.cpp 监听器注册片段 DataReaderListenerImpl* listener new DataReaderListenerImpl(); DDS::DataReader_var reader subscriber-create_datareader( topic, DATAREADER_QOS_DEFAULT, listener, OpenDDS::DCPS::DEFAULT_STATUS_MASK); Demo::MessageDataReader_var message_reader Demo::MessageDataReader::_narrow(reader);监听器里最关键的是on_data_available回调// DataReaderListener.h 回调实现 void on_data_available(DDS::DataReader_ptr reader) { Demo::MessageDataReader_var message_reader Demo::MessageDataReader::_narrow(reader); Demo::Message message; DDS::SampleInfo info; DDS::ReturnCode_t status message_reader-take_next_sample(message, info); if (status DDS::RETCODE_OK info.valid_data) { std::cout 收到消息 id message.id content message.content ts message.timestamp std::endl; } }逻辑说明take_next_sample会把数据从 reader 的缓存里取走取走后缓存不再保留。如果你想保留数据供多次读取应该用read_next_sample。info.valid_data用来过滤无效样本比如实例被注销时也会触发回调但 valid_data 为 false。参数说明SampleInfo里还包含sample_state、instance_state、view_state等字段示例里只用 valid_data 做简单过滤。实际业务中如果关心数据是否被覆盖需要检查sample_rank和generation_rank。3.3 编译与运行的具体命令假设你已经装好 OpenDDS 并设置了环境变量构建和运行按下面走# 配置构建目录 mkdir build cd build cmake .. -DOPENDDS_ROOT$DDS_ROOT # 编译 cmake --build . --config Release # 运行先起订阅端再起发布端 ./Subscriber sleep 1 ./Publisher逻辑说明先起订阅端是为了避免发布端发出的数据在订阅端还没就绪时丢失。示例里发布端通常只发几条消息就退出如果订阅端起晚了可能一条都收不到。sleep 1是给订阅端留出发现时间实际项目中可以用更可靠的同步机制。参数说明-DOPENDDS_ROOT告诉 CMake 去哪里找 OpenDDS 的配置文件和库。如果你的 OpenDDS 装在系统路径下这个参数可以省略。--config Release在 Linux 下单配置生成器里会被忽略Windows 下才需要。注意如果运行时报TRANSPORT_FAILURE或两端互相发现不了先检查域 ID 和主题名是否一致再确认防火墙没有拦截 RTPS 使用的端口。OpenDDS 默认使用 7400 以上的 UDP 端口做发现。4. 避坑与常见问题几个容易翻车的地方4.1 现象编译通过但运行时报类型不匹配原因通常是 IDL 改了但只重新编译了一端或者两端用的 IDL 文件版本不一致。OpenDDS 在运行时通过类型名和类型结构做匹配结构对不上就会拒绝连接。解决方法是确保两端引用同一个 IDL 文件改完后两端都重新编译不要只编一端。4.2 现象订阅端收不到任何数据先看发布端是否真的发出去了可以在write()后检查返回值。如果发布端返回 OK 但订阅端没反应大概率是域 ID 或主题名不一致。另一个常见原因是订阅端起得太晚发布端已经发完退出了。解决方法是先起订阅端或者把发布端改成循环发送。4.3 现象程序退出时卡住不返回OpenDDS 的参与者、发布者、订阅者都需要显式删除顺序错了或者漏删会导致进程挂起。示例代码里通常会在最后调用delete_contained_entities()和delete_participant()。如果你自己加了线程或异步逻辑要确保在删除前先停掉所有回调。解决方法是严格按创建的反序删除并且检查每个 delete 的返回值。4.4 现象CMake 找不到 OpenDDS 的宏这通常是因为 OpenDDS 安装时没有生成 CMake 配置文件或者OPENDDS_ROOT指向的目录不对。解决方法是确认$DDS_ROOT下存在cmake目录并且里面有所需的.cmake文件。如果用的是旧版本 OpenDDS可能只支持传统的 MPC 构建方式那就需要改用mwc.pl生成 Makefile。4.5 现象回调里打印乱码或崩溃监听器回调运行在 OpenDDS 的内部线程上不是主线程。如果你在回调里访问了主线程的局部变量或者没有做线程同步就会出问题。解决方法是回调里只做数据拷贝和简单处理复杂逻辑丢到队列里由主线程消费。另外std::cout在多线程下最好加锁否则输出会交错。5. 二次开发改造从示例到业务代码的几步拿到这份示例后最直接的改造就是换数据类型。把 IDL 里的Message换成你自己的业务结构体字段类型按需调整然后重新编译。这一步做完发布端和订阅端的骨架不用动只需要改write()和回调里的字段访问。第二步是调 QoS。示例里全用默认 QoS实际业务中可靠性要求高的场景要把RELIABILITY_QOS设成RELIABLE_RELIABILITY_QOS需要历史数据补发的要配DURABILITY_QOS和HISTORY_QOS。我一般会先在一个独立函数里集中构造 QoS 结构体而不是散落在各个 create 调用里这样改起来只动一处。// 集中配置 QoS 的常见做法 DDS::DataWriterQos writer_qos; publisher-get_default_datawriter_qos(writer_qos); writer_qos.reliability.kind DDS::RELIABLE_RELIABILITY_QOS; writer_qos.history.kind DDS::KEEP_LAST_HISTORY_QOS; writer_qos.history.depth 10; writer_qos.durability.kind DDS::TRANSIENT_LOCAL_DURABILITY_QOS;第三步是加生命周期管理。示例里发布端发完就退出实际项目里发布端通常要长期运行需要处理信号、优雅退出、断线重连。我习惯在main里注册SIGINT处理函数收到信号后先停发送线程再按反序删除 DDS 实体。第四步是验证。改完 IDL 和 QoS 后不要只看程序能不能跑要专门验证边界情况订阅端中途重启能不能收到后续数据、发布端快速连发会不会丢包、网络断开再恢复后能不能自动重连。这些场景在示例里不会覆盖但恰恰是上线后最容易出问题的地方。提示改 QoS 时注意RELIABLE和BEST_EFFORT不能混用发布端设了 RELIABLE 而订阅端设 BEST_EFFORT 会导致匹配失败。两端要么都 RELIABLE要么发布端 RELIABLE、订阅端 BEST_EFFORT 是允许的反过来不行。从那以后我每次改完 IDL 都强制走一遍「两端重新编译 先起订阅端 发三条验证」的流程不再图省事只编一端。希望这份示例能帮你少走点弯路把 OpenDDS 真正用起来。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑