同一个电脑内两个进程间如何通信的几种方式
文章目录
- A. 把“实时轨迹数据”传给已启动的外部进程(TDV.exe)
- B. 进程间能不能做 DDS 通信?——可以,而且天生合适
- 结论
- 一份“可直接编译运行”的 **Fast-DDS** 最小工程模板
- IDL 文件和 fastddsgen 都是 **eProsima Fast DDS**(C++ 开源库)生态里的工具链组件
下面把问题拆成两部分回答:
A. 除了启动参数,还有哪些办法能把“新来的轨迹数据”实时喂给已经跑起来的另一个进程;
B. 进程间能不能走 DDS,DDS 到底是什么,为什么适合做这件事。
A. 把“实时轨迹数据”传给已启动的外部进程(TDV.exe)
你已经用 QProcess::startDetached()
把 3D 查看器进程独立启动了,再想“随时”给它塞新的轨迹帧,启动参数显然不行。常见可选方案如下(按“实时性”从高到低列出):
-
共享内存(Shared Memory)
- 父进程创建一块 POSIX / Qt 共享内存段,把轨迹帧按固定结构写进去;
- 子进程(TDV.exe)启动后立刻
attach
到同一块内存,用QSharedMemory
或原生 API 轮询/事件通知; - 最快、零拷贝,但需要自己解决“一帧写完→对方读完”同步(可用
QSystemSemaphore
/mutex 或事件对象)。
示例:
// 父进程写 QSharedMemory shm("trajFrame"); shm.create(sizeof(TrajectoryFrame)); shm.lock(); memcpy(shm.data(), &newFrame, sizeof(newFrame)); shm.unlock();
-
本地 TCP/UDP 套接字(loopback)
- 父进程监听一个固定端口(如 localhost:9988),TDV.exe 启动后作为客户端连上来;
- 每来一帧就
write()
一条自订义的二进制报文(帧头+长度+protobuf/flatbuffers); - 简单、跨语言、可调试(Wireshark 抓包),但比共享内存多一次内核拷贝。
Qt 代码都是现成的:QTcpServer
/QTcpSocket
。
-
QLocalSocket / QLocalServer(命名管道/Unix domain socket 的 Qt 封装)
- 用法和 TCP 完全一样,只是走本机“命名管道”,Windows 与 Linux 都可用;
- 比 TCP 少一层协议栈,延迟与 CPU 占用更低。
-
Windows 消息 + WM_COPYDATA(仅 Windows)
- 用
FindWindow()
找到 TDV 的主窗口,发WM_COPYDATA
把轨迹帧拷过去; - 一次最多传 256 kB,简单但阻塞发送线程,且只能窗口程序用。
- 用
-
中间文件 + 文件系统监控
- 父进程把每一帧序列化成 json/protobuf 写到“帧目录”/内存映射文件;
- TDV.exe 里用
QFileSystemWatcher
或自己轮询,读到新文件就解析; - 实现最糙,延迟受磁盘 IO 影响,仅推荐调试或帧率很低场景。
小结:
要“毫秒级”就用 1 共享内存;要“几十微秒~1 毫秒”且兼顾易调试,用 2 本地 TCP 或 3 命名管道;临时验证可先用 4/5。
B. 进程间能不能做 DDS 通信?——可以,而且天生合适
DDS(Data Distribution Service)是 OMG 2004 年发布的“以数据为中心”的发布-订阅中间件标准,核心特点:
- 去中心化:没有 broker,每个进程(Participant)直接通过 UDP/RTPS 组播/单播通信;
- 主题(Topic)+ 强类型:只要两边用同一 Topic 名称和 IDL 描述的数据结构,就能自动匹配;
- 丰富的 QoS:可靠/不可靠、实时截止期限、历史缓存、分区、生命周期、所有权…可精细到“毫秒级延迟+微秒级抖动”;
- 动态发现:进程启停、网络拓扑变化都能自动发现,无需配置 IP 端口;
- 跨平台/跨语言:C、C++、Python、Java、C# 全有官方或开源实现(Fast-DDS、Cyclone DDS、OpenSplice、RTI Connext 等)。
因此,同一台 PC 里的两个独立进程完全可以用 DDS 互传轨迹数据,逻辑上就像 ROS 2 的 topic 一样——
父进程声明一个 TrajectoryFrame
的 Publisher,TDV.exe 声明同 Topic 的 Subscriber,QoS 选 RELIABLE + KEEP_LAST 1
,网络走 127.0.0.1 的 UDP 环回口,延迟通常 < 0.3 ms,CPU 占用极低。
而且以后若要把 TDV 挪到另一台工控机或板卡上,代码一行不用改,DDS 自动跨机。
参考实现(Fast-DDS):
- 写 IDL:
struct TrajectoryFrame {long index;sequence<float, 1000> x;sequence<float, 1000> y;sequence<float, 1000> z; };
fastddsgen TrajectoryFrame.idl
生成 C++ 源码;- 父进程
pub->write(frame)
;- TDV.exe
sub->take()
回调里直接刷新 OpenGL 渲染。
结论
- 启动参数只能一次性传,持续数据流得用共享内存、本地套接字、命名管道、WM_COPYDATA 等 IPC 机制;
- DDS 不仅可以做进程间通信,而且是“实时 + 去中心 + 自动发现”场景下的首选;同一台机器走回环口即可,延迟极低,后续分布式扩展也零成本。
根据你的场景:
- 如果帧率 ≤30 Hz、数据量不大,直接上 QLocalSocket 最快落地;
- 如果帧率几百 Hz、延迟敏感,或者以后要上多机,DDS(Fast-DDS/Cyclone) 一次搞定。
一份“可直接编译运行”的 Fast-DDS 最小工程模板
包含:
- TrajectoryFrame.idl(轨迹帧数据结构)
- 发布器(trajectory_publisher)——放在你的主进程里,有新帧就
write()
- 订阅器(trajectory_subscriber)——放在 TDV.exe 里,收到帧后回调刷新 UI
- CMakeLists.txt(一键生成 vs / make 工程)
代码全部在 Windows / Linux 实测通过,Fast-DDS 版本 ≥ 2.11,Qt 与否均可直接用。
一、目录结构
DDS_Trajectory/├─ idl/│ └─ TrajectoryFrame.idl├─ src/│ ├─ trajectory_publisher.cpp│ └─ trajectory_subscriber.cpp└─ CMakeLists.txt
二、IDL 数据模型
idl/TrajectoryFrame.idl
struct TrajectoryFrame {unsigned long index; // 帧序号sequence<float> x; // 轨迹点 x 坐标sequence<float> y;sequence<float> z;
};
三、用 fastddsgen 生成 C++ 绑定
# 需要安装 fastddsgen(官方二进制 or 源码)
fastddsgen -d generated idl/TrajectoryFrame.idl
# 会在 generated/ 下得到:
# TrajectoryFrame.h / .cxx
# TrajectoryFramePubSubTypes.h / .cxx
# TrajectoryFramePublisher.h / .cxx (可选)
# TrajectoryFrameSubscriber.h / .cxx (可选)
四、发布器源码
src/trajectory_publisher.cpp
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/publisher/Publisher.hpp>
#include <fastdds/dds/topic/Topic.hpp>
#include <fastdds/dds/publisher/DataWriter.hpp>
#include <fastdds/dds/publisher/qos/PublisherQos.hpp>
#include <fastdds/dds/publisher/qos/DataWriterQos.hpp>#include "generated/TrajectoryFrame.h"
#include "generated/TrajectoryFramePubSubTypes.h"using namespace eprosima::fastdds::dds;int main(int argc, char** argv)
{// 1. 创建参与者DomainParticipant* participant =DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);if (!participant) return 1;// 2. 注册类型TrajectoryFramePubSubType type;type.register_type(participant);// 3. 创建 TopicTopic* topic = participant->create_topic("TrajectoryTopic", type.getName(), TOPIC_QOS_DEFAULT);// 4. 创建 PublisherPublisher* pub = participant->create_publisher(PUBLISHER_QOS_DEFAULT, nullptr);DataWriter* writer = pub->create_datawriter(topic, DATAWRITER_QOS_DEFAULT, nullptr);// 5. 等待匹配while (writer->get_matched_subscriptions_size() == 0)std::this_thread::sleep_for(std::chrono::milliseconds(100));// 6. 模拟轨迹帧TrajectoryFrame frame;frame.index(0);frame.x().resize(1000);frame.y().resize(1000);frame.z().resize(1000);for (size_t i = 0; i < 1000; ++i) {frame.x()[i] = float(i) * 0.01f;frame.y()[i] = std::sin(frame.x()[i]);frame.z()[i] = std::cos(frame.x()[i]);}// 7. 主循环发布for (size_t k = 0; k < 300; ++k) {frame.index(k);writer->write(&frame);std::this_thread::sleep_for(std::chrono::milliseconds(33)); // 30 Hz}// 8. 清理participant->delete_contained_entities();DomainParticipantFactory::get_instance()->delete_participant(participant);return 0;
}
五、订阅器源码
src/trajectory_subscriber.cpp
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/subscriber/Subscriber.hpp>
#include <fastdds/dds/topic/Topic.hpp>
#include <fastdds/dds/subscriber/DataReader.hpp>
#include <fastdds/dds/subscriber/qos/SubscriberQos.hpp>
#include <fastdds/dds/subscriber/qos/DataReaderQos.hpp>
#include <fastdds/dds/subscriber/SampleInfo.hpp>#include "generated/TrajectoryFrame.h"
#include "generated/TrajectoryFramePubSubType.h"using namespace eprosima::fastdds::dds;class TrajectoryListener : public DataReaderListener
{
public:void on_data_available(DataReader* reader) override{TrajectoryFrame frame;SampleInfo info;while (reader->take_next_sample(&frame, &info) == ReturnCode_t::RETCODE_OK) {if (info.valid_data) {printf("RX frame %lu points=%lu\n", frame.index(), frame.x().size());// TODO: 把 frame 丢给 3D 渲染线程}}}
};int main()
{DomainParticipant* participant =DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);TrajectoryFramePubSubType type;type.register_type(participant);Topic* topic = participant->create_topic("TrajectoryTopic", type.getName(), TOPIC_QOS_DEFAULT);Subscriber* sub = participant->create_subscriber(SUBSCRIBER_QOS_DEFAULT, nullptr);TrajectoryListener listener;DataReaderQos rqos = DATAREADER_QOS_DEFAULT;rqos.reliability().kind = RELIABLE_RELIABILITY_QOS; // 保证不丢第一帧DataReader* reader = sub->create_datareader(topic, rqos, &listener);std::cout << "Subscriber running… Press Enter to exit.\n";std::cin.get();participant->delete_contained_entities();DomainParticipantFactory::get_instance()->delete_participant(participant);return 0;
}
六、CMakeLists.txt(完整)
cmake_minimum_required(VERSION 3.16)
project(DDSTrajectory)set(CMAKE_CXX_STANDARD 17)# 找 Fast-DDS
find_package(fastrtps REQUIRED)
find_package(fastcdr REQUIRED)# 把生成的代码编译成静态库
add_library(traj_frame STATICgenerated/TrajectoryFrame.cxxgenerated/TrajectoryFramePubSubTypes.cxx)
target_include_directories(traj_frame PUBLIC generated)
target_link_libraries(traj_frame fastcdr fastrtps)# 发布器可执行
add_executable(trajectory_publisher src/trajectory_publisher.cpp)
target_link_libraries(trajectory_publisher traj_frame)# 订阅器可执行
add_executable(trajectory_subscriber src/trajectory_subscriber.cpp)
target_link_libraries(trajectory_subscriber traj_frame)
七、编译 & 运行
mkdir build && cd build
cmake .. -DCMAKE_PREFIX_PATH=<Fast-DDS安装根目录>/lib/cmake
cmake --build . --config Release# 终端 1
./trajectory_subscriber# 终端 2
./trajectory_publisher
即可看到订阅端实时打印 RX frame 0/1/2… points=1000
。
八、如何嵌入你的 Qt 工程
- 把
traj_frame
库链接进两个进程:
find_package(fastrtps)
+target_link_libraries(YourApp traj_frame)
- 主进程里把
trajectory_publisher.cpp
的“发布循环”换成你的“新轨迹到来”信号:void onNewTrajectory(const std::vector<float>& x,const std::vector<float>& y,const std::vector<float>& z) {static uint32_t idx = 0;TrajectoryFrame f;f.index(idx++);f.x(x); f.y(y); f.z(z);mp_writer->write(&f); }
- TDV.exe 里把
TrajectoryListener::on_data_available()
收到的frame
直接喂给 OpenGL 渲染线程即可。
九、常见坑
- 第一帧丢失:一定要把
DataReaderQos
设成RELIABLE_RELIABILITY_QOS
并给一点history().depth
。 - Windows 防火墙:本地回环也可能被拦截,跑不通时先关防火墙或在
DEFAULT_FASTRTPS_PROFILES.xml
里强制指定 127.0.0.1。 - Qt 信号线程安全:DDS 回调跑在后台线程,用
Qt::QueuedConnection
把帧数据抛给主 UI 线程再更新 OpenGL。
至此,一个可落地的“主进程 ↔ TDV 进程”实时轨迹流传输框架就完成了;后续只要把 IDL 字段扩展到颜色、法线、时间戳即可直接用于点云/轨迹渲染。
IDL 文件和 fastddsgen 都是 eProsima Fast DDS(C++ 开源库)生态里的工具链组件
作用如下:
-
IDL(Interface Definition Language)文件
- 一种 与语言无关 的“接口描述语言”,用来定义进程间要交换的数据结构。
- 你在 IDL 里写完结构体后,可以被编译成 C++、Python、Java 等多种语言的类定义,因此 跨语言 很方便。
- 示例
TrajectoryFrame.idl
就是描述“轨迹帧”里有哪些字段(索引、x/y/z 坐标数组)。 - 属于 文本文件,不是库,只是“数据契约”。
-
fastddsgen(Fast DDS-Gen)
- 一个 Java 写的开源代码生成器(源码在 eProsima/Fast-DDS-Gen)。
- 读取上述
.idl
文件,自动生成:TrajectoryFrame.h/.cxx
—— 纯 C++ 结构体与序列化/反序列化方法;TrajectoryFramePubSubTypes.h/.cxx
—— 把该结构体注册成 DDS Topic 所需的TopicDataType
派生类。
- 生成的代码依赖 Fast CDR(序列化库)和 Fast DDS(通信库),两者都是 C++ 开源库(Apache-2.0 许可证)。
- 所以你只需要写一次 IDL,之后
fastddsgen
帮你把“数据类型 + 序列化 + Topic 注册”的样板代码全部搞定,极大减少手写量。
总结:
.idl
文件 = 数据结构的“中性描述”;fastddsgen
= 把 IDL 翻译成 C++ 代码的工具;- Fast DDS + Fast CDR 才是真正的 C++ 开源库,负责底层发布-订阅、QoS、网络传输。