
简介OpenDDS 示例程序是一份面向分布式实时通信初学者与 C 开发者的可运行学习工程围绕发布/订阅模型演示 Topic、DataWriter/DataReader、QoS 策略、IDL 数据结构及自动发现等核心概念帮助读者理解 OpenDDS 中间件如何完成低延迟、可靠的数据交换。压缩包共 172 个文件约 199.85MB以 cpp、h 源码与 idl 接口定义为主体配合 vcxproj、sln 工程文件和 exe 可执行程序并保留 log、obj、pdb 等编译运行痕迹便于直接对照工程结构与执行结果。已有 3257 人学习说明其在入门实践中具有一定参考价值。示例注释清晰Publisher、Subscriber 及类型支持代码分层完整读者可据此梳理 QoS 配置、代码生成、传输层选择与调试排错思路适合作为 OpenDDS 入门练手和课程实验的对照材料。1. 一份注释清晰的 OpenDDS 简单示例价值在哪儿一份注释清晰的 OpenDDS 简单示例价值往往不在代码量而在它替你闯过了环境、IDL、QoS 这三道门槛。这份示例围绕 Messenger 主题拆成 IDL 定义、发布端、订阅端、监听器、配置文件几个独立文件关键调用上方都有注释说明“这行在干什么、参数为什么这么填”。它适合三类人刚接触 DDS 发布订阅模型、需要一份能编译的最小骨架的手里已有 OpenDDS 环境、却反复卡在“编得过、跑不通”的想把自家业务主题套进 DDS、又不想从零写 TypeSupport 的。下面按环境、IDL、代码骨架、排错、改造五段推进命令都能直接抄。2. 把示例编出来从环境变量到第一个可执行文件示例能不能跑八成取决于编译这一步有没有踩空。OpenDDS 不像一般的 C 库cmake .. make就能完事——它自带一套 MPC 工程描述体系IDL 到 C 的代码生成规则也藏在这套体系里。很多人第一次编译失败不是代码写错而是根本没让构建系统知道“这个.idl需要先被opendds_idl和tao_idl处理一遍”。这一章把依赖、配置、生成、执行四步拆开每一步都标注了失败时该看哪个文件。2.1 目录与依赖先看清哪个文件归谁管拿到示例包先别急着敲 make。把目录摊开看一眼能省掉后面一半的困惑。典型的 Messenger 示例目录大致是这么几类文件各自职责非常分明文件归属什么时候需要改它Messenger.idl数据契约增删字段、改主键、换主题名Messenger.mpc构建描述加源文件、换传输插件、改编译选项Publisher.cpp写端主流程改发送频率、改 QoS、改数据内容Subscriber.cpp读端主流程改等待方式、改监听器注册DataReaderListener.h/.cpp回调实现改收到数据后的处理逻辑dds_tcp_conf.ini集中式发现配置改 InfoRepo 地址、改端口rtps.ini无中心发现配置换组播地址、改参与者在局域网内的可达性上一层是 OpenDDS 自身的源码树DDS_ROOT指向它再上一层是 ACE/TAOACE_ROOT指向ACE_wrappers。示例里的.mpc文件并不写死这些路径它依赖环境变量解析。也就是说同一个示例包在 A 机器上能编、在 B 机器上报 “cannot open project”原因通常是 B 机器少了DDS_ROOT或MPC_ROOT跟代码一点关系都没有。依赖上还有两个容易被忽略的点。一是 PerlMPC 工程文件本质上是 Perl 脚本mwc.pl需要 Perl 解释器Windows 上常见做法是装一个带 Perl 的构建环境Linux 上一般自带。二是编译器版本与 C 标准要对齐示例里如果用了nullptr、override这类写法--std参数就得给到 C11 及以上否则报的错会非常离谱看起来像是 OpenDDS 头文件坏了实际上只是标准太低。2.2 用 configure 生成平台配置OpenDDS 的configure脚本负责探测当前平台、生成user_macros.GNU之类的平台宏文件。这一步只需要跑一次换编译器或者换 C 标准时才需要重跑。执行的顺序和参数如下# 1) 三个根路径必须全部导出MPC 靠它们拼头文件和库的搜索路径 export DDS_ROOT$HOME/dds/OpenDDS export ACE_ROOT$DDS_ROOT/ACE_wrappers export MPC_ROOT$ACE_ROOT/MPC export PATH$DDS_ROOT/bin:$ACE_ROOT/bin:$PATH export LD_LIBRARY_PATH$DDS_ROOT/lib:$ACE_ROOT/lib:$LD_LIBRARY_PATH # 2) 生成平台配置只需要执行一次 cd $DDS_ROOT ./configure --prefix$DDS_ROOT \ --mpc$MPC_ROOT \ --no-tests \ --stdc11参数不是随便填的每一个都会在后面某一步反咬你一口--prefix决定编译产物最终装到哪儿。示例工程通过DDS_ROOT找库所以这里填的路径必须和DDS_ROOT一致否则链接阶段会出现“头文件找得到、库找不到”的割裂状态。--mpc指向 MPC 目录。这个目录通常在ACE_wrappers/MPC下如果 ACE/TAO 是单独下载解压的路径可能不一样填错时configure会直接退出并提示找不到 MPC。--no-tests跳过自带测试套件的编译。整套测试编下来动辄二三十分钟第一次跑示例完全没必要先把这个时间省掉。--stdc11决定 C 标准。示例源码里但凡出现auto、nullptr、范围 for就必须至少这一档否则报错信息会指向一堆标准库头文件误导性极强。配置完成后通常会生成一个setenv.sh新开终端时先source一下省得每次手动导出五个变量。血泪经验是别把这些export写进.bashrc就完事多版本共存的时候会互相覆盖写成一个独立的env.sh、用的时候再 source 更稳。2.3 用 MPC 生成 Makefile 并编译配好环境之后示例目录里要做的事只有两件把.mpc翻译成 Makefile然后 make。翻译这一步是理解整个构建流程的关键先看工程描述文件长什么样// Messenger.mpc告诉 MPC 这是个 DDS 可执行程序IDL 由谁处理 project(Messenger) : dcpsexe, dcps_tcp { // 保证 IDL 生成的目标先于源码编译 after *idl // 声明数据契约MPC 会自动挂上 opendds_idl 与 tao_idl 两条规则 TypeSupport_Files { Messenger.idl } Source_Files { Publisher.cpp Subscriber.cpp DataReaderListener.cpp } }dcpsexe这个基础工程类型负责把 OpenDDS 的核心库、ACE/TAO 的库、IDL 生成规则一次性挂上dcps_tcp额外加上 TCP 传输插件示例里的dds_tcp_conf.ini就是配它的。TypeSupport_Files是最容易被忽视的一行——写在这里的 IDL 会被自动串成“先opendds_idl生成 TypeSupport再tao_idl生成 CORBA 桩”的完整链条反过来如果你把 IDL 加进了Source_FilesMakefile 会老老实实把它当 C 源文件编译报错内容完全对不上问题本身。生成与编译的命令cd $DDS_ROOT/examples/DCPS/Messenger # -type gnuace 表示产出 GNU Make 体系-value_template 可以覆盖编译器 perl $ACE_ROOT/bin/mwc.pl -type gnuace \ -value_template cxxg \ Messenger.mpc # 并行编译核数按机器来 make -j4编译成功后目录里会多出Publisher、Subscriber两个可执行文件以及一堆MessengerC.cpp、MessengerTypeSupportImpl.cpp之类的中间产物。如果只看到.o没有可执行文件先看 make 输出的最后十行八成是链接阶段少了某个库——这时候回到--prefix那一节检查装的路径和LD_LIBRARY_PATH是否对得上。2.4 跑起来集中式发现与无中心发现两条路OpenDDS 支持两种发现方式示例包里通常两套配置都给了。集中式发现需要先起一个信息仓库进程所有参与者向它注册由它完成读写端配对无中心发现走 RTPS 协议参与者之间直接交换信息不需要额外进程。先看集中式# 终端 1信息仓库负责把 writer 和 reader 配对 $DDS_ROOT/bin/DCPSInfoRepo \ -o repo.ior \ -DCPSConfigFile dds_tcp_conf.ini # 终端 2订阅端先起等数据 ./Subscriber -DCPSInfoRepo file://$PWD/repo.ior \ -DCPSConfigFile dds_tcp_conf.ini # 终端 3发布端 ./Publisher -DCPSInfoRepo file://$PWD/repo.ior \ -DCPSConfigFile dds_tcp_conf.ini-o repo.ior让仓库把自己的对象引用写进文件读写端通过-DCPSInfoRepo file://...读到它。这个文件是“快照”仓库重启之后旧的 ior 就失效了必须重新生成——这是新手最常见的翻车点之一后面排错章节会展开。无中心发现就简单得多去掉 InfoRepo换成 RTPS 配置# 两个终端各自跑顺序无所谓谁先起都能互相发现 ./Subscriber -DCPSConfigFile rtps.ini ./Publisher -DCPSConfigFile rtps.ini两条路怎么选取决于你的场景维度集中式InfoRepo无中心RTPS额外进程需要且要保证存活不需要配对速度快注册即配对依赖周期性通告稍慢跨网段只要都能连到仓库即可需要单播对等列表或可达的发现地址调试便利性高仓库进程能看到全局配对状态低只能看各自日志适用场景单机开发、内网固定拓扑边缘设备、临时组网调试阶段建议先用集中式把逻辑跑通确认业务代码没问题再切到 RTPS 验证部署形态。反过来做的话一旦收不到数据你根本分不清是代码错了还是发现没成功。3. 拆 IDL 与 TypeSupport一个主题是怎么被定义出来的DDS 是“数据为中心”的中间件所有通信都围绕“主题”展开而主题的形状由 IDL 描述。很多人把 IDL 当成普通的头文件改完字段直接重新编译结果运行时报类型不匹配或者读端收到的结构体字段全是空。这一章把 IDL 的两套注解风格、生成文件的职责边界、TypeSupport 的注册时机讲清楚后面改业务主题时才不会一改就崩。3.1 IDL 写法两种注解风格的取舍示例里的Messenger.idl通常长这样用的是 OpenDDS 扩展的注解写法// Messenger.idl —— 读写两端共用的数据契约 module Messenger { topic struct Message { key string from; // 业务主键决定实例(instance)如何划分 string subject; // 主题行 long count; // 序号用来验证有序性与丢包 string text; // 正文 }; };topic标记这个结构体可以作为一个主题的数据类型key标记主键字段。主键不是数据库意义上的唯一索引它决定的是“实例”的划分同一个 key 值的数据属于同一个实例DDS 会按实例维护状态dispose、unregister这类生命周期操作也是按实例走的。示例只用一个from做主键是为了让读端能直观看到“同一个发布者连续发了一串数据”。老一些的示例会用 pragma 风格效果等价只是写法不同module Messenger { #pragma DCPS_DATA_TYPE Messenger::Message #pragma DCPS_DATA_KEY Messenger::Message from struct Message { string from; string subject; long count; string text; }; };两种风格不要混用。混用的后果不是编译报错而是其中一个标记被静默忽略——比如你写了#pragma DCPS_DATA_TYPE又加了key最后主键可能根本没生效读端会把每条数据当成不同实例instance_state永远是ALIVE排查起来极其费劲。选一种然后在整个项目里保持一致。字段类型也有讲究。IDL 里的string默认是变长字符串序列化时会带长度前缀读写两端必须用同一份 IDL 生成代码否则解析错位。如果业务上能确定长度上限用string64这种有界写法更省内存也能避免超长字符串带来的序列化开销。示例为了简单用了无界string改造成业务主题时建议顺手收紧。3.2 生成文件清单谁生的、谁用IDL 到 C 之间隔了两道生成器产出的文件名字很像职责完全不同。搞不清这一点编译报错时你会盯着错误的文件看半天。生成文件由谁生成职责MessengerC.h/.cpptao_idl结构体的 C 映射、序列化运算符、CORBA 桩MessengerS.h/.cpptao_idl服务端骨架示例里基本用不到MessengerTypeSupport.idlopendds_idl中间产物描述 TypeSupport 接口MessengerTypeSupportC.h/.cpptao_idl上面那个 IDL 的 C 映射MessengerTypeSupportImpl.h/.cppopendds_idl真正干活的实现注册类型、取类型名、序列化MessengerTypeSupportS.h/.cpptao_idlTypeSupport 的服务端骨架执行顺序是固定的opendds_idl先读原始 IDL吐出TypeSupport.idl和TypeSupportImpl的实现然后tao_idl分别处理原始 IDL 和中间 IDL生成各自的 C/S 文件。这条链路由 MPC 的TypeSupport_Files自动串起来所以正常编译时你不需要手动跑任何一个生成器。手动跑的意义在于排查。当怀疑生成代码过期时可以单独执行一遍看输出# 只生成 TypeSupport 相关代码不编译 opendds_idl Messenger.idl # 再生成 CORBA 桩-I 指定 IDL 搜索路径 tao_idl -I$DDS_ROOT -I. Messenger.idl tao_idl -I$DDS_ROOT -I. MessengerTypeSupport.idl生成器的版本必须和运行库版本一致。拿旧版本生成器产出的代码去链新版本库症状是链接期报一堆未定义符号符号名看着像乱码——那其实是版本化的命名空间不是真的乱码。3.3 TypeSupport 注册与类型名匹配主题在 DDS 里的身份由“主题名 类型名”共同确定两者都必须两端一致。类型名不是结构体名字符串硬编码而是由 TypeSupport 在运行时给出这一点示例的注释里通常会点明// 注册类型把 IDL 生成的 TypeSupport 挂到 participant 上 Messenger::MessageTypeSupport_var ts new Messenger::MessageTypeSupportImpl(); // 第二个参数传空串表示使用 IDL 里的默认类型名 if (ts-register_type(participant, ) ! DDS::RETCODE_OK) { // 注册失败通常是 participant 为空或类型名冲突 return 1; } // 取回真实类型名建 Topic 时必须用它不能手写字符串 CORBA::String_var type_name ts-get_type_name();register_type的第二个参数是类型名覆盖位。传空串时用 IDL 里module::struct拼出来的全名例如Messenger::Message传了自定义名字读写两端就必须都传同一个名字否则create_topic会成功但两端永远匹配不上on_subscription_matched的计数一直是 0。这种失败没有任何报错是典型的“静默不匹配”。注册顺序也不能乱必须先register_type再create_topic因为后者需要类型名。如果类型名写错create_topic会返回RETCODE_ERROR但错误信息只说“类型未注册”不会告诉你到底是哪个名字对不上。稳妥做法是把get_type_name()的返回值打到日志里两端各看一眼。还有一处容易忽略同一个 participant 上重复注册同名类型会返回错误。多主题场景下每个主题一个 TypeSupport 实例注册前判断一下返回值比事后猜要省事得多。4. 发布端与订阅端的代码骨架示例的两个主文件加起来可能不到三百行但注释密度很高几乎每个 DDS 实体创建调用上面都写了“为什么是这个参数”。这一章按创建顺序把骨架拆开参与者、主题、发布者、写者每一步的返回值都要检查因为 DDS 的错误大多不在抛异常而在返回码里安静地躺着。4.1 参与者与主题三行代码里的两个坑int main(int argc, char* argv[]) { // 带参数的工厂让 -DCPSInfoRepo / -DCPSConfigFile 之类的命令行参数生效 DDS::DomainParticipantFactory_var dpf TheParticipantFactoryWithArgs(argc, argv); // domain id 两端必须一致示例固定写 42 DDS::DomainParticipant_var participant dpf-create_participant(42, PARTICIPANT_QOS_DEFAULT, 0, OpenDDS::DCPS::DEFAULT_STATUS_MASK); if (!participant) { ACE_ERROR_RETURN((LM_ERROR, create_participant failed\n), 1); } // 注册类型……略 // 主题名是普通字符串两端必须逐字符一致包括大小写 DDS::Topic_var topic participant-create_topic( MyTopic, type_name, TOPIC_QOS_DEFAULT, 0, OpenDDS::DCPS::DEFAULT_STATUS_MASK);第一个坑是create_participant的 domain id。它既不是端口也不是进程号只是一个逻辑分区同 id 的参与者才能互相发现。示例写死 42 没问题但同一台机器上同时跑两组示例时两组会互相串数据看起来像“收到了莫名其妙的报文”。改法很简单给不同业务分配不同 id或者干脆从配置文件读。第二个坑是create_participant失败时返回的是空指针而不是抛异常。上面这段如果不判空后面register_type会直接崩在空指针上栈回溯指向 TypeSupport跟真正的原因比如 InfoRepo 连不上差着十万八千里。示例里每个创建调用后面都跟了判空这不是啰嗦是必要的。4.2 写端DataWriter 与 write 的返回值创建链条是 Publisher → DataWriterPublisher 只是个容器真正发数据的是 DataWriter// Publisher 可以省略用 participant 的隐式发布者示例为了结构清晰显式创建 DDS::Publisher_var publisher participant-create_publisher( PUBLISHER_QOS_DEFAULT, 0, OpenDDS::DCPS::DEFAULT_STATUS_MASK); // DataWriter 才是发送实体QoS 在这一步定型 DDS::DataWriter_var writer publisher-create_datawriter( topic, DATAWRITER_QOS_DEFAULT, 0, OpenDDS::DCPS::DEFAULT_STATUS_MASK); // 窄化成具体类型的 writer才能写结构体 Messenger::MessageDataWriter_var msg_writer Messenger::MessageDataWriter::_narrow(writer); if (!msg_writer) { ACE_ERROR_RETURN((LM_ERROR, _narrow failed\n), 1); } Messenger::Message msg; msg.from CORBA::string_dup(publisher-1); msg.subject CORBA::string_dup(hello); for (int i 0; i 10; i) { msg.count i; msg.text CORBA::string_dup(OpenDDS sample); // 返回码必须判超时、背压、未匹配都会在这里体现 DDS::ReturnCode_t rc msg_writer-write(msg, DDS::HANDLE_NIL); if (rc ! DDS::RETCODE_OK) { ACE_ERROR((LM_ERROR, write failed: %d\n, rc)); } ACE_OS::sleep(ACE_Time_Value(0, 500000)); // 500ms }write返回RETCODE_OK只代表“交给了 DDS”不代表对方收到了。想确认有没有人订阅得看on_publication_matched回调或者get_matched_subscriptions。示例如果没写这块建议自己补一个监听器把current_count打出来这是判断“到底连上没连上”的唯一可靠依据。HANDLE_NIL表示让 DDS 按主键自动决定实例句柄。如果手动管理实例需要先register_instance拿句柄再用句柄写否则dispose之类的操作会找不到目标。4.3 读端监听器与等待集两种收数据方式示例的读端通常用监听器回调里收数据代码短、结构清楚class DataListener : public virtual DDS::DataReaderListener { public: // 有数据可达时触发回调线程由 DDS 内部管理 void on_data_available(DDS::DataReader_ptr reader) { Messenger::MessageDataReader_var r Messenger::MessageDataReader::_narrow(reader); if (!r) return; Messenger::Message msg; DDS::SampleInfo info; // take 会把样本从 reader 缓存里移除read 则保留 while (r-take_next_sample(msg, info) DDS::RETCODE_OK) { // 过滤生命周期样本只处理真正的业务数据 if (info.valid_data) { std::cout msg.from # msg.count msg.text std::endl; } } } // 配对变化时触发调试阶段最该打日志的地方 void on_subscription_matched(DDS::DataReader_ptr, const DDS::SubscriptionMatchedStatus s) { std::cout matched current s.current_count std::endl; } };注册监听器的位置在create_datareaderDataListener* listener new DataListener(); DDS::DataReader_var reader subscriber-create_datareader( topic, DATAREADER_QOS_DEFAULT, listener, // 回调对象 DDS::DATA_AVAILABLE_STATUS); // 只订阅这一个事件第二种方式是等待集主线程阻塞在wait上拿到活跃状态再主动读。它适合需要把收数据和业务线程绑在一起的场景比如收一条就写一次数据库不想让回调线程直接碰业务状态。示例为了简洁一般只给监听器版本改造时可以两种都留一份按场景切换。两种方式有个共同点take_next_sample要放在while里循环到返回非 OK 为止。只调一次的话一次回调只取一条剩下的样本留在缓存里等下次on_data_available才被处理——而 DDS 可能认为“已经通知过了”不再触发数据就压在那儿了。这是“能收到但收不全”的最常见成因。4.4 QoS 对照表可靠性与历史深度怎么配示例大量使用*_QOS_DEFAULT跑通没问题但一旦换成真实业务默认值几乎必然不够用。至少这几项要心里有数策略写端设置读端设置不匹配时的表现RELIABILITYRELIABLE_RELIABILITY_QOSRELIABLE_RELIABILITY_QOS读端请求可靠、写端只提供尽力而为两端永不配对HISTORYKEEP_LAST depthKEEP_LAST depthdepth 太小会覆盖未取走的样本DURABILITYVOLATILEVOLATILE晚启动的读端收不到历史数据RESOURCE_LIMITSmax_samplesmax_samples超限后写端阻塞或丢弃取决于reliableDEADLINE周期值周期值超时触发回调不会自动重发可靠性是双向兼容的写端“提供”的能力必须大于等于读端“请求”的能力。读端请求可靠、写端只给尽力而为create_datareader会成功但两边永远匹配不上日志里只有一个安静的 0。这个坑值得单独记一笔因为它的表现和“网络不通”一模一样。HISTORY的 depth 在于“缓存几条”。示例里发 10 条、读端秒收depth 设 1 也没事换成突发流量depth 1 意味着新样本直接覆盖旧样本读端处理慢一点就丢数据而且丢得毫无痕迹。稳妥做法是按峰值速率乘以可接受的处理延迟来估 depth再配RESOURCE_LIMITS兜底。5. 避坑与排查五个把「能编译」变成「跑不通」的瞬间编译通过只是起点真正耗时间的是运行期那些不报错、只沉默的失败。下面五条都是我在拆这类示例时真实撞过的每条按现象、原因、解决写遇到同款症状可以直接对号入座。5.1 改了 IDL 却没重新生成收到的是旧结构体现象给Message加了一个字段重新 make 也过了但读端打印出来新字段是空值或者干脆收到乱码。原因Makefile 对.idl的依赖是基于时间戳的如果生成文件的时间比 IDL 新比如从别处拷贝了一份旧工程make 就认为不需要重新生成。另一种情况是手工跑过tao_idl但没跑opendds_idlTypeSupport 的实现还是旧的。解决先清理再编译别信增量构建。# 清掉所有生成物与中间对象强制重跑两条生成链 make clean rm -f MessengerC.* MessengerS.* MessengerTypeSupport* make -j4改 IDL 之后两端必须同时用新生成的代码编译。只重编一端会得到“类型名一样、序列化布局不一样”的诡异状态症状是部分字段错位比彻底连不上更难查。5.2 信息仓库没起或 ior 文件过期现象读写端启动后卡住或者报连接失败有时能看到Failed to locate之类的提示有时什么都不说就是不配对。原因三种可能——仓库进程根本没起repo.ior是上一次运行留下的仓库重启后对象引用已经变了端口被别的进程占了仓库启动失败但你看的是另一个终端的日志。解决按顺序确认。先看仓库进程是否在跑再看 ior 文件的修改时间是不是本次生成的最后确认端口没被占。# 确认仓库进程存在 ps -ef | grep DCPSInfoRepo # 重新生成 ior路径用绝对路径避免相对路径解析歧义 $DDS_ROOT/bin/DCPSInfoRepo -o /tmp/repo.ior -DCPSConfigFile dds_tcp_conf.ini # 读写端统一指向这个绝对路径 ./Subscriber -DCPSInfoRepo file:///tmp/repo.ior -DCPSConfigFile dds_tcp_conf.ini容器环境里还有个额外变量仓库写进 ior 的主机名如果是容器内部名宿主机上的读写端解析不了。这种情况在配置里显式指定可达地址别依赖自动探测。5.3 动态库找不到程序起来就退现象可执行文件明明在那儿一执行就报error while loading shared libraries提示某个 OpenDDS 或 ACE 的.so找不到。原因LD_LIBRARY_PATH没设、设了但在子 shell 里丢了、或者编译时链接的是 A 路径的库、运行时找的是 B 路径。第三种最阴因为编译期一切正常。解决先确认依赖关系再补路径。# 看这个可执行文件到底依赖哪些库、期望从哪里加载 ldd ./Publisher | grep -i -E opendds|ace|tao # 确认库文件真实存在 ls $DDS_ROOT/lib/libOpenDDS_Dcps.so临时方案是执行前export LD_LIBRARY_PATH...长期方案是写进启动脚本或者把库路径登记到系统级的库搜索配置里。别图省事直接改系统配置又不做记录换机器时你会忘了自己改过什么。5.4 QoS 不兼容写得出去就是收不到现象write返回RETCODE_OK写端日志一切正常读端一条数据没有on_subscription_matched的current_count始终是 0。原因读写端 QoS 不兼容最常见的是可靠性错配——读端要可靠写端只给尽力而为。DDS 的规则是“提供的能力必须覆盖请求的能力”不满足就静默不配对不报错。解决把两端的关键 QoS 打印出来逐项对齐。// 写端确认自己提供的是可靠传输 DDS::DataWriterQos wqos; writer-get_qos(wqos); std::cout writer reliability wqos.reliability.kind std::endl; // 读端确认自己请求的也是可靠 DDS::DataReaderQos rqos; reader-get_qos(rqos); std::cout reader reliability rqos.reliability.kind std::endl;两个值必须“写端 读端”。改法通常是把写端提到RELIABLE_RELIABILITY_QOS或者把读端降到BEST_EFFORT_RELIABILITY_QOS做对照实验先确认问题出在 QoS 上再谈优化。5.5 回调里只取一条数据越积越多现象读端能收到数据但条数对不上发 100 条只处理了十几条或者程序跑一会儿内存缓慢上涨。原因on_data_available里只调了一次take_next_sample剩下的样本留在 reader 缓存里。后续回调是否触发取决于实现细节很多时候不触发数据就压着。反过来如果用read而不是take样本取走后仍留在缓存同样会积压。解决循环取到没有为止并且判断valid_data。DDS::ReturnCode_t rc; do { rc reader-take_next_sample(msg, info); if (rc DDS::RETCODE_OK info.valid_data) { // 只处理业务数据生命周期样本跳过 handle(msg); } } while (rc DDS::RETCODE_OK);配套把RESOURCE_LIMITS的max_samples设成一个合理值超限时至少能在日志里看到而不是无声无息地丢。6. 从 Messenger 迁到自己的主题一份可对照的改造清单示例跑通之后真正的动作是把它换成自己的业务数据结构。这个过程看起来只是改个 IDL实际上牵动四五处漏一处就是几小时的排查。我一般按下面的顺序改改完立即验证不攒着。步骤改什么验证方式1新建MyTopic.idl定义结构体与主键opendds_idl单独跑一遍无报错2.mpc里替换TypeSupport_Files与源文件列表mwc.pl重新生成 Makefile3两端register_type换成新 TypeSupport日志里打出get_type_name()的值4主题名、类型名、domain id 三处对齐两端on_*_matched的计数同步变化5QoS 从默认值改成业务值压测下确认不丢、不积压第三步特别值得强调类型名不要手写。把get_type_name()的返回值打到日志里两端对一眼比在代码里搜字符串靠谱得多。第四步的 domain id 是最容易被复制粘贴带错的东西改完主题忘了改 id两组程序会互相串数据症状是“收到了别人发的报文”。主键设计也得掂量一下。示例用from做主键是因为每个发布者只发自己那一串换成订单、设备状态这类业务数据主键选错会导致实例数量爆炸——DDS 按实例维护状态实例多了内存和 CPU 都会上去。经验做法是主键选“生命周期内状态需要独立维护”的那个维度别拿时间戳、流水号这类每次都变的值做主键。还有一个只在切到 RTPS 之后才会暴露的问题局域网内参与者多了发现流量会明显上升。示例里只有两个进程感觉不到真实部署几十个节点就得考虑限制发现范围、调整通告周期。这些参数在rtps.ini里改之前先确认两端的配置一致否则一边收敛得快、一边收敛得慢表现出来就是“偶尔能连上、偶尔连不上”的玄学。最后说一个习惯。从那以后我每次拿到新的 DDS 示例都强制走一遍“最小闭环”先不改任何代码跑通默认示例再只改主题名跑一遍最后才动 IDL 和 QoS。三步里任何一步失败问题范围立刻缩小到刚改的那一处不用在几十个文件里猜。这份示例的注释已经把关键调用标出来了照着顺序改比从头读源码快得多。希望帮到你。本文还有配套的精品资源点击获取