企业网站主页素描模板监测网站定制
企业网站主页素描模板,监测网站定制,wordpress连接pgsql,dw做的静态网站怎么分享链接Fast DDS实战#xff1a;5分钟搞定C发布订阅通信#xff08;附完整CMake配置#xff09;
如果你正在为机器人、自动驾驶或者分布式工业系统寻找一个靠谱的通信中间件#xff0c;大概率已经听说过DDS的大名。它不像传统的消息队列那样中心化#xff0c;而是采用了一种去中心…Fast DDS实战5分钟搞定C发布订阅通信附完整CMake配置如果你正在为机器人、自动驾驶或者分布式工业系统寻找一个靠谱的通信中间件大概率已经听说过DDS的大名。它不像传统的消息队列那样中心化而是采用了一种去中心化的“发布-订阅”模型让数据在多个节点间直接流动延迟低、可靠性高。而Fast DDS作为DDS标准的一个高性能开源实现正成为越来越多C开发者的首选。但说实话第一次接触Fast DDS时我也被它复杂的配置和编译过程劝退过。官方文档虽然详尽但对于只想快速验证一个“Hello World”通信是否可行的开发者来说步骤显得有些冗长。特别是如何将Fast DDS库、自定义的数据类型生成以及CMake构建流程优雅地整合在一起常常是新手遇到的第一个拦路虎。这篇文章的目的就是帮你跨过这个初始门槛。我们不深入讨论DDS复杂的服务质量QoS策略也不铺开讲解其庞大的API体系。我们只聚焦一件事如何用最短的时间、最清晰的步骤搭建一个能跑起来的C发布者和订阅者示例并且拥有一个可复用、易于维护的CMake项目结构。无论你是想评估Fast DDS是否适合你的项目还是急需一个可运行的demo来理解其基本工作流程接下来的内容都将为你提供一条直达目标的路径。1. 理解核心Fast DDS的极简模型在动手写代码之前花两分钟理解Fast DDS最核心的几个概念能让你后面的操作事半功倍。你可以暂时忘掉那些复杂的配置先抓住主干。Fast DDS的通信模型围绕几个关键实体展开DomainParticipant域参与者这是应用的入口点。每个使用Fast DDS的程序都必须创建一个域参与者它相当于这个程序在DDS通信域中的“身份证”。同一个域ID下的参与者才能互相发现和通信。Topic主题这是数据汇流的“管道”或“频道”。它有一个唯一的名字比如SensorDataTopic和一个特定的数据类型。发布者和订阅者通过订阅相同的主题来建立联系。Publisher发布者与DataWriter数据写入器发布者是一个工厂用于创建和管理多个DataWriter。真正负责将数据对象写入特定主题的是DataWriter。你可以把一个Publisher想象成一个出版社而DataWriter就是出版社里负责某个特定专栏的撰稿人。Subscriber订阅者与DataReader数据读取器与发布端对应订阅者创建和管理DataReader。DataReader则负责从特定主题读取数据。订阅者好比一个报亭DataReader就是报亭里专门取送某份报纸的店员。它们之间的关系可以用下面这个简化的序列来理解应用程序创建DomainParticipant。通过参与者注册你想要传输的数据类型比如一个包含温度和湿度的SensorMsg结构体。使用该数据类型在参与者内创建一个命名的Topic。发布者应用在参与者内创建一个Publisher然后通过它创建一个指向该Topic的DataWriter。订阅者应用在参与者内创建一个Subscriber然后通过它创建一个指向同一Topic的DataReader。当DataWriter写入数据时Fast DDS中间件会自动将数据传递给所有订阅了该Topic的DataReader。注意这里我们全部使用默认的QoS策略以保证最简单直接的通信。在实际生产环境中你需要根据可靠性、持久性、截止时间等需求仔细配置QoS。理解了这些我们就知道代码需要依次创建这些实体。但在这之前还有一个更基础的问题如何让Fast DDS认识我们想传输的数据结构这就需要用到IDL和代码生成工具了。2. 项目基石用IDL定义数据与CMake自动化构建Fast DDS需要明确知道数据类型的“模样”包括每个字段的类型和内存布局。我们通过接口定义语言IDL来描述它。这就像为通信双方制定一份数据合同。2.1 定义数据结构编写IDL文件假设我们要传输一个简单的问候消息包含消息序号和内容。我们在项目根目录下创建一个idl文件夹并在其中新建HelloWorld.idl文件。// idl/HelloWorld.idl module HelloWorldDemo { // 定义一个模块用于组织类型避免命名冲突 struct HelloWorld { unsigned long index; // 无符号长整型作为消息序号 string message; // 字符串作为消息内容 }; };这个结构非常直观。module类似于C的命名空间。struct定义了我们数据传输的单元。接下来我们需要将这个IDL文件“翻译”成C代码。2.2 核心自动化集成Fast DDS Gen的CMake脚本手动调用fastddsgen命令生成代码再引入项目很麻烦且不易复用。最佳实践是将此过程集成到CMake中实现构建时自动生成。这是保证“5分钟搞定”的关键一步。我们在项目根目录创建CMakeLists.txt并重点关注如何查找fastddsgen并执行它。# 根目录 CMakeLists.txt cmake_minimum_required(VERSION 3.16) project(FastDDS_QuickStart LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 1. 查找Fast DDS相关依赖包 find_package(fastcdr REQUIRED) find_package(fastrtps REQUIRED) # Fast DDS 2.x版本后包名通常是fastrtps # 2. 关键查找fastddsgen可执行文件 find_program(FASTDDSGEN_EXECUTABLE NAMES fastddsgen fastddsgen.bat fastddsgen.exe DOC Path to the Fast DDS IDL compiler REQUIRED # 找不到则直接报错便于早期发现问题 ) message(STATUS Found fastddsgen: ${FASTDDSGEN_EXECUTABLE}) # 3. 设置生成代码的输出目录 set(GENERATED_CODE_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated) file(MAKE_DIRECTORY ${GENERATED_CODE_DIR}) # 4. 定义生成函数便于管理 function(generate_dds_code IDL_FILE) get_filename_component(IDL_NAME ${IDL_FILE} NAME_WE) # 提取文件名不含扩展名 set(GENERATED_FILES ${GENERATED_CODE_DIR}/${IDL_NAME}.hpp ${GENERATED_CODE_DIR}/${IDL_NAME}CdrAux.hpp ${GENERATED_CODE_DIR}/${IDL_NAME}PubSubTypes.hpp ${GENERATED_CODE_DIR}/${IDL_NAME}TypeObjectSupport.hpp ${GENERATED_CODE_DIR}/${IDL_NAME}CdrAux.ipp ${GENERATED_CODE_DIR}/${IDL_NAME}PubSubTypes.cxx ${GENERATED_CODE_DIR}/${IDL_NAME}TypeObjectSupport.cxx ) # 添加自定义命令指定输入输出依赖关系 add_custom_command( OUTPUT ${GENERATED_FILES} COMMAND ${FASTDDSGEN_EXECUTABLE} -d ${GENERATED_CODE_DIR} # 指定输出目录 -replace # 替换已存在的文件 ${CMAKE_CURRENT_SOURCE_DIR}/${IDL_FILE} DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${IDL_FILE} COMMENT Generating DDS code for ${IDL_FILE} VERBATIM ) # 将生成的文件路径添加到父作用域的变量中 set(GENERATED_SOURCES ${GENERATED_SOURCES} ${GENERATED_FILES} PARENT_SCOPE) endfunction() # 5. 应用函数生成代码 generate_dds_code(idl/HelloWorld.idl) # 6. 添加子目录发布者和订阅者 add_subdirectory(publisher) add_subdirectory(subscriber) # 7. 将生成的头文件目录包含进来使所有子目标都能找到 target_include_directories(publisher PUBLIC ${GENERATED_CODE_DIR}) target_include_directories(subscriber PUBLIC ${GENERATED_CODE_DIR})这个CMake脚本做了几件重要的事自动探测工具链使用find_program定位fastddsgen避免了手动配置路径的麻烦。构建时生成通过add_custom_command将IDL编译定义为构建过程的一部分。只要IDL文件有更新生成的C代码会自动更新。集中管理生成代码所有生成的文件都放在${CMAKE_CURRENT_BINARY_DIR}/generated目录下保持源码树的整洁。依赖传递将生成的头文件目录公开给所有子目标确保发布者和订阅者都能正确包含HelloWorldPubSubTypes.hpp等文件。提示如果你在构建时遇到“找不到fastddsgen”的错误请确保Fast DDS的安装路径已添加到系统的PATH环境变量中或者通过-DFASTDDSGEN_EXECUTABLE/path/to/fastddsgen直接传递给CMake。有了数据类型和自动化构建框架我们就可以着手实现发布者和订阅者了。3. 实现发布者让数据“发声”发布者的核心职责是周期性地创建数据样本并通过DataWriter发送出去。我们创建一个publisher目录并实现main.cpp。// publisher/main.cpp #include chrono #include thread #include iostream #include fastdds/dds/domain/DomainParticipant.hpp #include fastdds/dds/domain/DomainParticipantFactory.hpp #include fastdds/dds/publisher/Publisher.hpp #include fastdds/dds/publisher/DataWriter.hpp #include fastdds/dds/publisher/DataWriterListener.hpp #include fastdds/dds/topic/Topic.hpp // 包含由fastddsgen生成的数据类型支持代码 #include HelloWorldPubSubTypes.hpp using namespace HelloWorldDemo; // 使用IDL中定义的模块 class HelloWorldPublisher { private: HelloWorld m_data; // 要发送的数据实例 eprosima::fastdds::dds::DomainParticipant* m_participant; eprosima::fastdds::dds::Publisher* m_publisher; eprosima::fastdds::dds::Topic* m_topic; eprosima::fastdds::dds::DataWriter* m_writer; eprosima::fastdds::dds::TypeSupport m_type; // 监听器用于接收DataWriter的事件回调如匹配发现 class PubListener : public eprosima::fastdds::dds::DataWriterListener { public: PubListener() : matched(0) {} ~PubListener() override default; void on_publication_matched( eprosima::fastdds::dds::DataWriter* writer, const eprosima::fastdds::dds::PublicationMatchedStatus info) override { if (info.current_count_change 1) { matched info.total_count; std::cout [Publisher] 发现一个订阅者匹配成功。当前匹配数: matched std::endl; } else if (info.current_count_change -1) { matched info.total_count; std::cout [Publisher] 一个订阅者断开连接。当前匹配数: matched std::endl; } } std::atomicint matched; } m_listener; public: HelloWorldPublisher() : m_participant(nullptr), m_publisher(nullptr), m_topic(nullptr), m_writer(nullptr), m_type(new HelloWorldPubSubType()) { // 初始化数据样本 m_data.index(0); m_data.message(Hello from Fast DDS Publisher!); } ~HelloWorldPublisher() { // 清理资源顺序与创建相反 if (m_writer) m_publisher-delete_datawriter(m_writer); if (m_publisher) m_participant-delete_publisher(m_publisher); if (m_topic) m_participant-delete_topic(m_topic); if (m_participant) eprosima::fastdds::dds::DomainParticipantFactory::get_instance()-delete_participant(m_participant); } bool init() { // 1. 创建域参与者 (Domain ID 0) m_participant eprosima::fastdds::dds::DomainParticipantFactory::get_instance()-create_participant(0); if (!m_participant) { std::cerr 创建域参与者失败! std::endl; return false; } // 2. 向参与者注册数据类型 m_type.register_type(m_participant); // 3. 创建主题名称必须与订阅者一致 m_topic m_participant-create_topic(HelloWorldTopic, m_type.get_type_name(), eprosima::fastdds::dds::TOPIC_QOS_DEFAULT); if (!m_topic) { std::cerr 创建主题失败! std::endl; return false; } // 4. 创建发布者 m_publisher m_participant-create_publisher(eprosima::fastdds::dds::PUBLISHER_QOS_DEFAULT); if (!m_publisher) { std::cerr 创建发布者失败! std::endl; return false; } // 5. 创建数据写入器并关联监听器 m_writer m_publisher-create_datawriter(m_topic, eprosima::fastdds::dds::DATAWRITER_QOS_DEFAULT, m_listener); if (!m_writer) { std::cerr 创建数据写入器失败! std::endl; return false; } std::cout [Publisher] 初始化完成等待订阅者连接... std::endl; return true; } bool publish() { // 只有检测到有匹配的订阅者时才发送 if (m_listener.matched 0) { m_data.index(m_data.index() 1); // 序号递增 m_writer-write(m_data); // 发送数据 return true; } return false; } void run(int samples) { int count 0; while (count samples) { if (publish()) { std::cout [Publisher] 发送消息: index m_data.index() , message\ m_data.message() \ std::endl; count; } std::this_thread::sleep_for(std::chrono::seconds(1)); // 每秒发送一次 } std::cout [Publisher] 已完成 samples 次发送程序退出。 std::endl; } }; int main(int argc, char** argv) { std::cout Fast DDS 发布者启动 std::endl; HelloWorldPublisher publisher; if (publisher.init()) { publisher.run(10); // 发送10条消息后停止 } else { std::cerr 发布者初始化失败程序终止。 std::endl; return 1; } return 0; }对应的publisher/CMakeLists.txt则非常简单# publisher/CMakeLists.txt add_executable(fastdds_publisher main.cpp) target_link_libraries(fastdds_publisher fastrtps fastcdr) # 链接Fast DDS库 # 注意生成的源代码已在根CMakeLists.txt中通过include_directories引入此处无需重复添加。发布者的逻辑是线性的初始化所有DDS实体 - 进入循环 - 检查是否有订阅者 - 有则更新数据并发送 - 等待。监听器PubListener让我们能感知到订阅者的加入和离开这是实现动态发现的关键。4. 实现订阅者让数据“入耳”订阅者与发布者对称它创建DataReader来监听主题上的数据。创建subscriber目录和main.cpp。// subscriber/main.cpp #include iostream #include atomic #include fastdds/dds/domain/DomainParticipant.hpp #include fastdds/dds/domain/DomainParticipantFactory.hpp #include fastdds/dds/subscriber/Subscriber.hpp #include fastdds/dds/subscriber/DataReader.hpp #include fastdds/dds/subscriber/DataReaderListener.hpp #include fastdds/dds/subscriber/SampleInfo.hpp #include fastdds/dds/topic/Topic.hpp #include HelloWorldPubSubTypes.hpp using namespace HelloWorldDemo; class HelloWorldSubscriber { private: eprosima::fastdds::dds::DomainParticipant* m_participant; eprosima::fastdds::dds::Subscriber* m_subscriber; eprosima::fastdds::dds::Topic* m_topic; eprosima::fastdds::dds::DataReader* m_reader; eprosima::fastdds::dds::TypeSupport m_type; // 监听器处理数据到达和匹配事件 class SubListener : public eprosima::fastdds::dds::DataReaderListener { public: SubListener() : samples(0) {} ~SubListener() override default; void on_subscription_matched( eprosima::fastdds::dds::DataReader* reader, const eprosima::fastdds::dds::SubscriptionMatchedStatus info) override { if (info.current_count_change 1) { std::cout [Subscriber] 发现一个发布者匹配成功。 std::endl; } else if (info.current_count_change -1) { std::cout [Subscriber] 发布者断开连接。 std::endl; } } // 最重要的回调当有新数据到达时触发 void on_data_available(eprosima::fastdds::dds::DataReader* reader) override { HelloWorld data; eprosima::fastdds::dds::SampleInfo info; // 从DataReader中取出下一个样本 if (reader-take_next_sample(data, info) eprosima::fastrtps::types::ReturnCode_t::RETCODE_OK) { // 检查是否为有效数据而非元数据或失效数据 if (info.valid_data) { samples; std::cout [Subscriber] 收到消息: index data.index() , message\ data.message() \ (总计收到: samples 条) std::endl; } } } std::atomicint samples; } m_listener; public: HelloWorldSubscriber() : m_participant(nullptr), m_subscriber(nullptr), m_topic(nullptr), m_reader(nullptr), m_type(new HelloWorldPubSubType()) { } ~HelloWorldSubscriber() { if (m_reader) m_subscriber-delete_datareader(m_reader); if (m_topic) m_participant-delete_topic(m_topic); if (m_subscriber) m_participant-delete_subscriber(m_subscriber); if (m_participant) eprosima::fastdds::dds::DomainParticipantFactory::get_instance()-delete_participant(m_participant); } bool init() { // 1. 创建域参与者 (必须与发布者使用相同的Domain ID此处为0) m_participant eprosima::fastdds::dds::DomainParticipantFactory::get_instance()-create_participant(0); if (!m_participant) { std::cerr 创建域参与者失败! std::endl; return false; } // 2. 注册数据类型 m_type.register_type(m_participant); // 3. 创建主题名称必须与发布者完全一致 m_topic m_participant-create_topic(HelloWorldTopic, m_type.get_type_name(), eprosima::fastdds::dds::TOPIC_QOS_DEFAULT); if (!m_topic) { std::cerr 创建主题失败! std::endl; return false; } // 4. 创建订阅者 m_subscriber m_participant-create_subscriber(eprosima::fastdds::dds::SUBSCRIBER_QOS_DEFAULT); if (!m_subscriber) { std::cerr 创建订阅者失败! std::endl; return false; } // 5. 创建数据读取器并关联监听器 m_reader m_subscriber-create_datareader(m_topic, eprosima::fastdds::dds::DATAREADER_QOS_DEFAULT, m_listener); if (!m_reader) { std::cerr 创建数据读取器失败! std::endl; return false; } std::cout [Subscriber] 初始化完成等待发布者消息... std::endl; return true; } void run(int target_samples) { // 简单循环直到收到指定数量的样本 while (m_listener.samples target_samples) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 短暂休眠避免空转 } std::cout [Subscriber] 已收到 target_samples 条消息程序退出。 std::endl; } }; int main(int argc, char** argv) { std::cout Fast DDS 订阅者启动 std::endl; HelloWorldSubscriber subscriber; if (subscriber.init()) { subscriber.run(10); // 等待接收10条消息 } else { std::cerr 订阅者初始化失败程序终止。 std::endl; return 1; } return 0; }订阅者的CMakeLists.txt同样简洁# subscriber/CMakeLists.txt add_executable(fastdds_subscriber main.cpp) target_link_libraries(fastdds_subscriber fastrtps fastcdr)订阅者的核心在于on_data_available回调函数。一旦有数据发布到“HelloWorldTopic”主题上这个函数就会被Fast DDS中间件自动调用。我们通过take_next_sample方法获取数据并通过SampleInfo判断数据的有效性。这种异步回调机制使得订阅者无需轮询效率更高。5. 构建、运行与问题排查现在我们有了一个完整的项目结构fastdds_quickstart/ ├── CMakeLists.txt # 根CMake集成代码生成 ├── idl/ │ └── HelloWorld.idl # 数据定义 ├── publisher/ │ ├── CMakeLists.txt │ └── main.cpp # 发布者实现 └── subscriber/ ├── CMakeLists.txt └── main.cpp # 订阅者实现5.1 一键构建打开终端进入项目根目录执行标准的CMake构建流程# 1. 创建并进入构建目录 mkdir build cd build # 2. 生成构建系统假设使用Makefile cmake .. -DCMAKE_BUILD_TYPERelease # 3. 编译项目 make -j$(nproc)如果一切顺利你将在build/publisher和build/subscriber目录下看到生成的可执行文件fastdds_publisher和fastdds_subscriber。5.2 运行与验证你需要打开两个终端窗口。终端1 (运行订阅者):cd /path/to/your/project/build/subscriber ./fastdds_subscriber输出应显示[Subscriber] 初始化完成等待发布者消息...终端2 (运行发布者):cd /path/to/your/project/build/publisher ./fastdds_publisher发布者启动后两个终端会立刻显示匹配成功的日志。随后发布者开始每秒发送一条消息订阅者同步接收并打印。一个成功的运行输出示例如下# 发布者终端 Fast DDS 发布者启动 [Publisher] 初始化完成等待订阅者连接... [Publisher] 发现一个订阅者匹配成功。当前匹配数: 1 [Publisher] 发送消息: index1, messageHello from Fast DDS Publisher! [Publisher] 发送消息: index2, messageHello from Fast DDS Publisher! ... # 订阅者终端 Fast DDS 订阅者启动 [Subscriber] 初始化完成等待发布者消息... [Subscriber] 发现一个发布者匹配成功。 [Subscriber] 收到消息: index1, messageHello from Fast DDS Publisher! (总计收到: 1 条) [Subscriber] 收到消息: index2, messageHello from Fast DDS Publisher! (总计收到: 2 条) ...5.3 常见问题与解决思路即使按照步骤操作也可能会遇到一些问题。这里列出几个最常见的问题现象可能原因排查步骤CMake找不到fastddsgenFast DDS未正确安装或环境变量未设置。1. 在终端直接运行fastddsgen --version看是否有效。2. 如果无效检查Fast DDS安装路径并将其bin目录添加到PATH。3. 或者在CMake命令中指定路径cmake .. -DFASTDDSGEN_EXECUTABLE/usr/local/bin/fastddsgen。编译时链接错误找不到Fast DDS库CMake未找到fastrtps或fastcdr包。1. 确保通过包管理器如apt, yum, vcpkg, conan或源码安装了Fast DDS。2. 如果安装在非标准路径使用-DCMAKE_PREFIX_PATH指定安装目录。发布者和订阅者无法发现彼此1. 不在同一网络。2. 防火墙/安全组阻止了DDS发现端口默认7400-7500。3. Domain ID不一致。4. Topic名称或数据类型不匹配。1. 检查两者是否在同一台机器或可互通的网络。2. 临时关闭防火墙或开放端口进行测试。3. 确认双方代码中的create_participant(0)的0一致。4. 仔细核对双方create_topic的第一个参数主题名是否完全一致包括大小写。订阅者收不到数据1. 监听器on_data_available未被触发。2. 数据类型注册失败。3. QoS不兼容如可靠性策略。1. 检查on_subscription_matched是否被调用以确认匹配成功。2. 确认发布者和订阅者都正确调用了register_type。3. 本例使用默认QoS通常是“尽力而为”的可靠性。确保双方都使用默认QoS或显式设置为兼容的QoS如RELIABLE_RELIABILITY_QOS。第一次运行成功听到两个程序之间“嘀嗒嘀嗒”地传递消息那种感觉是很棒的。这个最小化的示例为你提供了一个坚实的起点。你可以基于这个框架轻松地修改IDL文件来定义更复杂的传感器数据、控制指令或任何你需要的业务数据结构。CMake的自动化集成也意味着当你添加新的.idl文件时只需在根CMakeLists.txt中再调用一次generate_dds_code函数即可无需手动干预构建过程。