在Windows上做多进程架构,很多人的第一反应是命名管道或者Socket。我在一个桌面端采集项目里需要两个独立进程高频交换数据,最开始也用的TCP回环,后来数据量一上来,延迟和丢包问题开始冒头,这才把FastDDS拉进来。这篇文章把我从零到跑通整个流程的过程整理出来,包含选型、编译、代码、联调,还有一堆Windows环境下特有的坑。如果你正打算在Windows上用FastDDS做进程间通信,又不想在编译和环境适配阶段浪费太多时间,这篇应该能帮你少走不少弯路。
FastDDS是eProsima开源的DDS实现,底层走RTPS协议,核心能力是发布/订阅通信。它最大的特点是不管两个进程在同一台机器还是不同机器,只要处在同一个域、订阅同一个主题,就能自动发现并通信,不需要中间服务器,也不需要手动维护连接。这个特性拿到Windows桌面端做进程间通信,可以说是降维打击:你不需要关心TCP连接断了怎么重连,不需要处理UDP丢包,不需要自己设计心跳保活,把业务逻辑丢进发布/订阅模型里就行。
这篇文章适合几类人:刚接触DDS、想在Windows上快速跑通例子的新手;已经在用其他IPC方案、遇到瓶颈想换技术的开发者;以及需要在嵌入式或机器人项目里统一通信架构,先拿Windows环境做原型验证的团队。我会从编译环境讲到代码实现,再讲到联调排错,尽量把每个环节都展开,让你照着做就能跑起来。
1. 项目定位与整体思路
1.1 为什么在Windows上还用DDS
DDS一般在机器人、自动驾驶、军工通信这些领域出现得多,桌面端和工业软件里其实同样有价值。原因很简单:DDS天生就是为了解决"分布式场景下的实时通信"设计的,它天然同时具备跨进程、跨机器的能力,而且自带QoS和动态发现。把它用在Windows上的进程间通信,不是杀鸡用牛刀,而是一次到位。
从架构角度看,DDS对应用层暴露的是发布/订阅模型,你在设计模块时只需要关心业务逻辑,不需要关心"到底谁在收数据、收多少、收多久"。底层RTPS协议会自动做主题匹配和数据路由,新进程上线、老进程退出,都是动态发现的,不需要改任何代码。
在Windows桌面上做进程间通信,常见的候选方案无非是命名管道、Socket回环、共享内存、消息队列。它们各有适用场景,但也各有痛点:命名管道简单可靠,但只适合Windows环境,迁移到Linux就废了;Socket回环通用性最强,但UDP不保证可靠,TCP又引入端口管理和重连逻辑;共享内存性能最好,但同步、生命周期管理全得自己写,一个进程崩溃还要处理残留对象。
FastDDS在这些方案里属于"设计和性能都想兼顾"的那一类,而且它本身就有共享内存传输实现,单机场景性能并不弱。更关键的是,它的QoS机制可以针对不同数据流设置不同的可靠性策略,一套框架支持多种通信诉求,这是传统IPC方案很难做到的。
1.2 方案选型:FastDDS对比其他IPC手段
我放一张对比表,方便大家直观感受:
| 方案 | 单机性能 | 可靠性 | 跨平台 | 自动发现 | 扩展性 |
|---|---|---|---|---|---|
| 命名管道 | 中 | 传输可靠,无QoS | 仅Windows | 无 | 差 |
| UDP回环 | 中低 | 丢包 | 跨平台 | 无 | 差 |
| TCP回环 | 中 | 可靠,重连麻烦 | 跨平台 | 无 | 差 |
| 共享内存 | 高 | 需自己实现同步 | 需自己封装 | 无 | 一般 |
| FastDDS | 高 | QoS可配置 | 跨平台+跨语言 | 有 | 好 |
我实际项目里,同一个机器上同时跑采集和渲染两个进程,数据有两路:一路低频状态同步,一路高频传感器数据上报。这两路消息对可靠性和实时性的要求完全不同。状态消息不能丢,采集数据则可以接受少量丢帧换低延迟。传统方案基本是"要么全可靠要么全尽力",调不了这么细。FastDDS的QoS可以针对不同Topic设置不同策略,一套框架解决多种通信需求,这是它最打动我的地方。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与编译踩坑记录
2.1 依赖清单和版本选择
在Windows上编译FastDDS,首先要确认系统环境。我用的是Windows 10 22H2和Windows 11均测试过,Visual Studio 2019或2022都行,社区版就够了,安装时必须勾选"使用C++的桌面开发"这个工作负载。CMake版本建议3.20以上,FastDDS 2.10以后的版本对CMake版本要求越来越严,用太老版本的CMake会在配置阶段直接报错。
还需要准备Git,以及可选的Java JDK 11+和Gradle,这两个是给FastDDS-Gen用的。FastDDS-Gen是一个IDL编译器,用来把自定义的消息定义生成C++代码。如果你只是用简单的字符串或预定义类型通信,可以完全跳过FastDDS-Gen,后面会细说。
核心组件清单如下:
| 组件 | 作用 |
|---|---|
| FastDDS | DDS/RTPS协议核心库 |
| FastCDR | CDR序列化库,负责消息的二进制编解码 |
| Foonathan memory | 内存分配器,FastDDS底层的依赖 |
| FastDDS-Gen | IDL编译器,从IDL文件生成C++类型代码 |
这里有件事必须提醒:版本之间是有依赖关系的,不是每个都用最新就好。FastDDS和FastCDR的版本必须匹配,否则链接阶段会报一堆函数找不到的错误。我自己就吃过这个亏,FastDDS 2.14配上旧版FastCDR,编译的时候没有任何问题,链接的时候就傻眼了。
2.2 vcpkg方式安装(推荐)
最快的方式是用vcpkg,三步搞定:
bash复制git clone https://github.com/microsoft/vcpkg
cd vcpkg
bootstrap-vcpkg.bat
vcpkg install fastdds:x64-windows
等待编译完成,这一步会自动把FastDDS、FastCDR、Foonathan memory全部编出来,省得自己操心依赖顺序。安装完毕后,在CMake工程里指定toolchain文件即可:
bash复制cmake -B build -DCMAKE_TOOLCHAIN_FILE=D:/vcpkg/scripts/buildsystems/vcpkg.cmake
这里有个很现实的坑:如果这台机器之前用源码编译方式装过FastDDS,再混着用vcpkg的版本,会出现各种运行时找不到DLL、符号重复的诡异问题。我的建议是一台机器只保留一种安装方式,要么全走vcpkg,要么全手动,混着装是最让人头疼的。
还有一点,Windows下用vcpkg编出来的库默认是Release版本的。你自己的工程如果编译成Debug模式去链接它,数据库的CRT运行时不一致,会报一堆堆错误或者链接错误。我在实际测试中的建议是统一使用Release x64配置编译和链接,能少很多麻烦。
2.3 源码编译方式
如果你想定制编译参数,走源码编译也不难。按顺序先编Foonathan memory,再编FastDDS本体。
首先编译Foonathan memory:
bash复制git clone https://github.com/foonathan/memory
cd memory
mkdir build && cd build
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_INSTALL_PREFIX=D:/libs/foonathan_memory ..
cmake --build . --config Release --target install
然后编译FastDDS:
bash复制git clone https://github.com/eProsima/Fast-DDS
cd Fast-DDS
mkdir build && cd build
cmake -G "Visual Studio 17 2022" -A x64 ^
-DCMAKE_INSTALL_PREFIX=D:/libs/fastdds ^
-DCMAKE_PREFIX_PATH=D:/libs/foonathan_memory ^
-DEPROSIMA_BUILD_TESTS=OFF ^
-DEPROSIMA_BUILD_METRICS=OFF ..
cmake --build . --config Release --target install
源码编译的时候,CMAKE_PREFIX_PATH一定要指到依赖库的安装目录。FastDDS找依赖不是靠猜的,要靠这个变量告诉它"你的朋友住在哪"。我见过很多人在这个环节出错,明明Foonathan memory编好了,FastDDS配置时就是找不到,基本都是路径没指对。
2.4 FastDDS-Gen类型生成工具
如果不想用内置的简单类型,需要定义结构体消息,就需要FastDDS-Gen。它是Java写的,编译需要JDK和Gradle:
bash复制git clone https://github.com/eProsima/Fast-DDS-Gen
cd Fast-DDS-Gen
gradle assemble
编译完成之后,把生成的fastddsgen.bat放进PATH,后面的IDL编译就能直接用命令行工具了。
这里我多说一句:如果只做字符串、二进制流这种简单通信,可以完全不碰FastDDS-Gen。用FastDDS内置的字符串类型就能完成大部分工作。但为了贴近真实项目的用法,我的Demo里还是用了自定义IDL结构体,定义了一个带索引、内容、时间戳的传感器消息类型,这样完整展示IDL到C++代码的生成链路,你也可以直接抄走。
2.5 编译阶段最容易踩的几个坑
整理一下编译阶段的常见坑,这些都是我实际遇到过或者看别人踩过的:
- Visual Studio编译器和CMake版本不匹配,CMake太老会不认识VS2022,建议直接用最新的CMake版本。
- 打开CMake配置时报找不到Foonathan memory,几乎都是CMAKE_PREFIX_PATH没指对,Windows下路径分隔符要正确。
- 编译FastDDS自带的测试时容易报一堆奇怪的错误,实测下来直接关掉测试最省心,反正我们用不到。
- 架构问题,大家统一用x64。如果工程是32位的,编译时各种ABI不匹配问题真的能让人血压升高。
- vcpkg安装时默认编的是x64-windows,如果你用的CMake里的平台选错成了Win32,就会出现"库文件不匹配"的诡异问题。
3. 通信模型设计与消息类型定义
3.1 DDS的核心概念
写代码之前,先花两分钟把DDS的几个核心概念对齐。DomainParticipant是域参与者,相当于你加入某个"广播频道"的身份证;Topic是主题,收发双方靠它匹配;Publisher和DataWriter是发布端的对象组合;Subscriber和DataReader是订阅端的对象组合;TypeSupport负责序列化数据类型。
两个进程想通信,需要满足三个条件:在同一个Domain ID下、使用同一个Topic名称、数据类型一致。这三者缺一不可,任何一项不匹配,进程之间就"鸡同鸭讲",谁也找不到谁。
DDS还有一个很实用的特性:动态发现。进程A先启动,进程B后启动,B一上线就能发现A之前发布的消息类型,不需要预先配置任何对端信息。这也是DDS相比传统IPC方案体验上最明显的优势,不需要手动维护对端地址列表。
3.2 消息类型选择
我的Demo里定义了一个简单的传感器消息结构。先写IDL文件:
idl复制struct SensorData
{
long id;
unsigned long sequence;
string content;
string timestamp;
};
然后用FastDDS-Gen生成C++代码:
bash复制fastddsgen SensorData.idl
执行之后会生成SensorData.h、SensorData.cxx、SensorDataPubSubTypes.h、SensorDataPubSubTypes.cxx四个文件,直接加入工程编译即可。SensorData是消息载体,SensorDataPubSubTypes负责序列化和反序列化,是为FastDDS提供的类型适配层。
如果不想用IDL工具,可以直接用内置的std::string类型。DataWriter直接发送std::string,DataReader直接收std::string,完全省去生成步骤。对于Quick Demo来说,这是个不错的选择,我建议第一次跑通流程时就用字符串类型,等确认环境没问题了,再引入自定义类型。
3.3 QoS策略怎么设才靠谱
DDS最有吸引力的地方就是QoS。在Windows上做进程间通信,我建议重点设置这几个:
| QoS项 | 可选值 | 我的推荐 | 原因 |
|---|---|---|---|
| Reliability | BEST_EFFORT / RELIABLE | 状态同步用RELIABLE,采集数据流用BEST_EFFORT | 状态数据不能丢,采集数据接受少量丢弃换低延迟 |
| History | KEEP_LAST / KEEP_ALL | KEEP_LAST,depth设10 | 避免接收端被历史积压数据冲垮 |
| Durability | VOLATILE / TRANSIENT_LOCAL | 低频状态用TRANSIENT_LOCAL | 支持"后来者"也能收到最后一条状态 |
| LatencyBudget | 自定义 | 实时性要求高的Topic设20ms | 高数据量场景避免排队堆积 |
刚开始接触QoS的人容易把每个都调成最严格模式,其实那样会让系统变得又慢又脆弱。我第一次把RELIABLE、KEEP_ALL、TRANSIENT_LOCAL全开,主观体感是"消息一条不漏",但程序启动加载量和内存占用明显上去了。后来才想明白,通信可靠性不是靠把所有开关都开到最大赢来的,而是针对数据特征选择合适的档位。
4. 核心代码实现与逐段解析
4.1 CMakeLists配置
工程配置层面,CMakeLists.txt大致是这样:
cmake复制cmake_minimum_required(VERSION 3.20)
project(FastDDSIpcDemo)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(fastdds REQUIRED)
add_executable(publisher
src/publisher.cpp
src/SensorData.cxx
src/SensorDataPubSubTypes.cxx
)
target_include_directories(publisher PRIVATE src)
target_link_libraries(publisher fastdds::fastdds)
add_executable(subscriber
src/subscriber.cpp
src/SensorData.cxx
src/SensorDataPubSubTypes.cxx
)
target_include_directories(subscriber PRIVATE src)
target_link_libraries(subscriber fastdds::fastdds)
find_package(fastdds REQUIRED)在配置时需要能定位到FastDDS的CMake配置文件。如果是vcpkg方式安装,通过toolchain文件指定;如果是源码安装,在CMAKE_PREFIX_PATH里指到安装目录。Windows上报找不到fastddsConfig.cmake,基本都是这两个路径没配对。
这里有个小建议:FastDDS-Gen生成的文件和手写的业务代码最好分开目录管理,生成代码不要手动改,后续重新生成时直接覆盖。我在一个项目里就吃过手动改生成代码的亏,重新生成之后改动全没了,白白浪费半天时间。
4.2 发布端完整实现
发布端的完整代码,关键注释我直接写在代码里:
cpp复制#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/domain/DomainParticipant.hpp>
#include <fastdds/dds/topic/TypeSupport.hpp>
#include <fastdds/dds/publisher/Publisher.hpp>
#include <fastdds/dds/publisher/DataWriter.hpp>
#include <fastdds/dds/publisher/DataWriterListener.hpp>
#include "SensorDataPubSubTypes.h"
#include <atomic>
#include <chrono>
#include <ctime>
#include <iostream>
#include <string>
#include <thread>
using namespace eprosima::fastdds::dds;
std::string get_now()
{
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
char buf[64] = {0};
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
return std::string(buf);
}
class PubListener : public DataWriterListener
{
public:
std::atomic<int> matched{0};
void on_publication_matched(DataWriter* writer, const PublicationMatchedStatus& info) override
{
(void)writer;
matched.store(info.current_count);
std::cout << "publication matched, current_count = " << info.current_count << std::endl;
}
};
int main()
{
DomainParticipant* participant = DomainParticipantFactory::get_instance()->create_participant(
0, PARTICIPANT_QOS_DEFAULT);
if (participant == nullptr)
{
std::cerr << "create participant failed" << std::endl;
return -1;
}
TypeSupport type(new SensorDataPubSubType());
type.register_type(participant);
Topic* topic = participant->create_topic(
"SensorDataTopic",
type.get_type_name(),
TOPIC_QOS_DEFAULT);
Publisher* publisher = participant->create_publisher(PUBLISHER_QOS_DEFAULT);
if (publisher == nullptr)
{
std::cerr << "create publisher failed" << std::endl;
return -1;
}
PubListener listener;
DataWriterQos wqos = publisher->get_default_datawriter_qos();
wqos.reliability().kind = RELIABLE_RELIABILITY_QOS;
wqos.history().kind = KEEP_LAST_HISTORY_QOS;
wqos.history().depth = 10;
DataWriter* writer = publisher->create_datawriter(topic, wqos, &listener);
if (writer == nullptr)
{
std::cerr << "create datawriter failed" << std::endl;
return -1;
}
SensorData msg;
uint32_t seq = 0;
while (true)
{
msg.id(1);
msg.sequence(seq++);
msg.content("Hello From Publisher Process");
msg.timestamp(get_now());
if (writer->write(&msg) == ReturnCode_t::RETCODE_OK)
{
std::cout << "sent seq=" << seq << " matched=" << listener.matched.load() << std::endl;
}
else
{
std::cerr << "write failed at seq=" << seq << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
return 0;
}
代码逻辑不复杂,核心是create_participant、register_type、create_topic、create_publisher、create_datawriter这样一条链路。DataWriterListener里的on_publication_matched回调很关键,它是判断发布端和订阅端是否配对成功的唯一可靠信号。
4.3 订阅端完整实现
订阅端的代码结构跟发布端基本对称:
cpp复制#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/domain/DomainParticipant.hpp>
#include <fastdds/dds/topic/TypeSupport.hpp>
