
FastDDS做分布式通信对很多人来说有点“只闻其名不见其人”尤其在ROS 2成为主流之后大家知道机器人底层有一个叫DDS的东西在跑但真要自己动手去写一个发布订阅程序反而不知道从哪下手。我最早接触FastDDS是在一个多节点采集项目里一边是C写的高帧率数据采集端另一边有几个需要在PC上快速验证消息内容的Python脚本。当时第一时间想到的就是FastDDS官方提供的Python绑定因为它就是ROS 2默认的DDS实现性能靠谱生态也成熟没必要再造一套轮子。FastDDS加上Python等于把C那一层高性能、低延迟、支持可靠传输的分布式通信能力直接开放给了Python开发者。它能做的事情很明确让不同进程、甚至不同机器上的程序在同一个逻辑“通信空间”里用主题Topic交换消息。你不需要自己去写Socket重传、节点发现、序列化这些FastDDS都已经处理好了。这篇文章我想以一个完整跑通的示例为主线把从安装到调通、再到排查问题的整个过程都讲清楚。适合的读者很明确用过Python但没接触过DDS的人、想在ROS 2之外用DDS做设备通信的嵌入式/IoT开发者以及在机器人项目里需要快速做联调脚本的人。1. 整体设计与思路拆解1.1 FastDDS是什么从“快递驿站”理解分布式通信很多第一次接触FastDDS的人会被DDS这个名字吓到。Data Distribution Service听起来很高大上其实拆开看就是“分布在不同位置的数据服务”。FastDDS是eProsima团队维护的开源实现也是ROS 2在Foxy和Humble版本中的默认中间件。它遵循的是一个叫RTPS的线缆协议也就是说不管你是C、Java还是Python只要用的是FastDDS在网络上传输的数据格式是统一的跨语言互通没有问题。理解FastDDS可以从“快递驿站”这个类比入手。传统的Socket通信像两个人打电话你一句我一句必须先接通中间断了还得重拨。而DDS更像驿站模式发布者把消息放到一个“主题柜”里订阅者直接从同一个柜子里取两边不需要知道对方是谁、不需要建立点对点连接。FastDDS在这个模式里承担了驿站管理员的工作负责新节点的发现、消息的序列化、数据包的可靠传输以及各种服务质量参数QoS的匹配。这个设计带来的直接好处是松耦合。一个用C写的传感器采集程序和一个用Python写的可视化脚本只要它们使用相同的主题名和类型在任何一台局域网机器上运行都能自动发现对方完成数据交换。这就是为什么FastDDS在机器人、自动驾驶、工业物联网这类多节点异构系统里特别流行。1.2 Python绑定解决什么问题能快就不要绕FastDDS的核心代码是C写的这没问题但如果你只是想在项目里快速验证一个协议、写个数据回放工具、或者做一个带界面的调试面板每次都写C工程再编译效率太低。官方维护了一个Fast-DDS-python仓库用pybind11把FastDDS的核心API包装成了Python模块。你在Python里import fastdds创建Participant、Publisher、Subscriber、读写Topic底层实际在调用C实现性能损失主要发生在Python和C的边界层对于大多数中间件消息和工具类场景完全够用。我在实际项目里的习惯是性能敏感的实时链路用C外围工具脚本、测试桩、日志分析全部用Python。Python绑定解决的核心问题是开发效率。比如要模拟一个终端节点频发消息用Python写个循环几十行就搞定改成C要处理编译、链接、内存管理时间成本高了一个数量级。还有一个常见场景用Python脚本监听某个主题抓取实时数据做统计分析这在联调阶段非常实用。1.3 安装方案怎么选轮子优先源码保底FastDDS Python的安装方式有两类一类是直接使用预编译的wheel包另一类是从源码编译。优先用前者原因很简单省时间。官方在PyPI上发布的包名是fastdds-python在大多数主流平台上都有预编译轮子。安装命令很简单pip install fastdds-python如果这条命令顺利执行完在Python里执行import fastdds能成功说明环境就绪了。但这里有一个关键点它依赖的底层FastDDS核心库怎么处理预编译wheel通常会捆绑运行时依赖所以只要Python版本在支持范围内一般不需要单独安装C库。可一旦pip找不到匹配你当前Python版本的wheel就会退回源码构建那你就得先把FastDDS的C库编译安装好再编译Python绑定。源码编译比直接用预编译包麻烦不少不过也不是什么痛苦的事后面我详细写。我的建议是无论你是Linux还是Windows第一选择永远是用官方wheel遇到版本不匹配再考虑源码构建不要一上来就自己编译浪费时间。2. 核心细节解析与实操要点2.1 环境准备Python版本和系统依赖在动手之前先确认你的Python环境。FastDDS Python绑定对Python版本有要求不同版本的支持范围略有差异。就我使用的经验来说Python 3.8到3.12都是比较稳妥的选择。另外一个很重要的建议尽量用虚拟环境不要用系统自带的Python直接装。我之前在Ubuntu上做事系统Python是3.10直接pip install后把大量包装到了系统目录里后来系统升级出现了模块冲突。用venv或conda隔离环境踩坑概率低很多。创建虚拟环境python3 -m venv fastdds_env source fastdds_env/bin/activate然后升级pip再做安装pip install --upgrade pip pip install fastdds-python如果是纯内网离线环境或者你需要锁定FastDDS的某个版本比如和C端保持一致的版本号可以在PyPI上指定版本安装pip install fastdds-python2.14.0安装完成后用下面的命令快速验证python -c import fastdds; print(fastdds.__version__)能打印出版本号说明绑定模块没问题。这一步虽然简单但很值得做因为很多人安装时pip显示成功实际import时才报错提前验证能省不少排查时间。2.2 核心对象Participant、Publisher、Subscriber、TopicFastDDS的Python API结构和C接口基本一致。使用起来绕不开几个核心对象这里做一个完整的梳理。首先是DomainParticipant它是整个通信世界的入口。你可以把它理解成“加入某个微信群”。每个群有一个群号也就是Domain ID只有Domain ID相同的Participant才能互相发现。在同一个机器上甚至同一台机器多个进程如果Domain ID不同数据就是隔离的。默认情况下使用0但多个团队共用一个局域网时最好规划好Domain ID的分配避免不同项目之间互相“看到”对方的消息。然后是Topic它是数据传输的容器名称。比如一个摄像头节点发图像主题一个定位节点发位置主题。Topic必须同时绑定一个数据类型这个类型由IDL文件定义。Publisher和Subscriber是参与通信的角色它们分别创建DataWriter和DataReader这两个才是真正读写数据的对象。再往下的QoS配置决定了消息的可靠性等级、生命周期、历史缓存等行为。整个关系链是这样的DomainParticipant创建Topic、Publisher、SubscriberPublisher创建DataWriterSubscriber创建DataReader。代码写起来层级感很强只要记住这个容器关系用起来就不容易乱。2.3 类型系统用IDL定义自己的消息FastDDS原生帮不了你定类型你发什么结构的数据需要自己定义。这也符合DDS的规范通信双方必须使用相同的类型定义才能正确序列化和反序列化。类型定义文件用IDL格式写。比如我们要定义一个最简单的HelloWorld消息struct HelloWorld { long index; string message; };在C工程里你需要用fastddsgen工具生成C类型而在Python绑定里同样需要先把这个IDL生成Python模块。官方提供的fastddsgen工具可以直接生成Python代码pip install fastddsgen fastddsgen -python HelloWorld.idl执行之后会生成HelloWorld.py里面包含了HelloWorld数据类和一个HelloWorldPubSubType类型支撑类。这个PubSubType非常重要它里面封装了数据的序列化、反序列化和类型名信息参与注册的时候就要用到它。两端程序必须在同一个Topic上使用相同的类型名和相同的数据结构否则即使能发现对方也无法正确解析消息。2.4 QoS参数选型这步决定消息可不可靠QoSQuality of Service是FastDDS里最容易被忽视但最关键的部分。很多新手收不到消息不是代码写错而是两端的QoS不匹配。有三个参数需要重点关注。第一个是Reliability分为RELIABLE和BEST_EFFORT。RELIABLE表示可靠传输类似TCP数据丢失会重传BEST_EFFORT表示尽力而为类似UDP延迟更低但可能丢包。如果发布端配置了RELIABLE订阅端可以配置BEST_EFFORT或RELIABLE但如果发布端是BEST_EFFORT订阅端要求RELIABLE两端的QoS就不兼容协商失败订阅端看不到数据。第二个是Durability决定晚加入的订阅者能不能收到历史数据。TRANSIENT_LOCAL会保留数据给后来的订阅者VOLATILE则只给当前在线的人。第三个是History分为KEEP_LAST和KEEP_ALL前者仅保留最近N条数据后者保留全部具体保留多少条由深度depth决定。我用一个实际例子说明比如要传控制指令必须选RELIABLE加VOLATILE因为指令晚到或者重复发送没有意义当前的指令才重要。要传高频率传感器数据BEST_EFFORT加KEEP_LAST(1)更合适数据丢了上一帧就行但要保证最新状态能被拉到。这里做一个QoS参数速查表参数选项适用场景ReliabilityRELIABLE控制指令、状态切换、需要确定送达的场景ReliabilityBEST_EFFORT传感器流、点云、视频帧等高频海量数据DurabilityTRANSIENT_LOCAL希望晚加入的订阅者也能拿到最新状态DurabilityVOLATILE只关心在线实时数据的场景HistoryKEEP_LAST(depth)只保留最近N条消息最常见配置HistoryKEEP_ALL不允许丢弃任何消息适合精确回放场景3. 实操过程与核心环节实现3.1 步骤一定义IDL并生成Python类型我按一个完整示例来演示目标很简单一个发布端周期发HelloWorld消息一个订阅端收到并打印内容。先在工作目录下创建HelloWorld.idl内容就是上面那段struct HelloWorld { long index; string message; };然后生成Python代码fastddsgen -python HelloWorld.idl执行完目录下会出现HelloWorld.py。这个文件不要手动去改它是自动生成的。在发布端和订阅端代码里都用同一份HelloWorld.py两边类型定义就一致了。3.2 步骤二发布端实现先建publisher.py。流程就是创建Participant、注册类型、创建Topic、创建Publisher、创建DataWriter然后不断写入数据。import time from fastdds import ( DomainParticipantFactory, DomainParticipantQos, PublisherQos, DataWriterQos, TopicQos, DataWriterListener, RELIABLE_RELIABILITY_QOS, ) import HelloWorld TOPIC_NAME HelloWorldTopic TYPE_NAME HelloWorld class PubListener(DataWriterListener): def __init__(self): super().__init__() self.matched 0 def on_publication_matched(self, writer, info): super().on_publication_matched(writer, info) if info.status 1: self.matched 1 print(发现订阅端开始发送) def main(): qos DomainParticipantQos() participant DomainParticipantFactory.get_instance().create_participant(0, qos) if participant is None: raise RuntimeError(创建Participant失败) type_support HelloWorld.HelloWorldPubSubType() type_support.setName(TYPE_NAME) participant.register_type(type_support) topic participant.create_topic(TOPIC_NAME, TYPE_NAME, TopicQos()) publisher participant.create_publisher(PublisherQos()) writer_qos publisher.get_default_datawriter_qos() writer_qos.reliability().kind RELIABLE_RELIABILITY_QOS writer publisher.create_datawriter(topic, writer_qos, PubListener()) sample HelloWorld.HelloWorld() sample.message(hello from fastdds python) idx 0 try: while True: sample.index(idx) writer.write(sample) idx 1 time.sleep(0.2) except KeyboardInterrupt: pass participant.delete_contained_entities() DomainParticipantFactory.get_instance().delete_participant(participant) if __name__ __main__: main()代码不复杂但有四个地方需要特别注意。第一participant必须判断创建失败FastDDS在资源不足或配置错误时可能返回空对象。第二register_type必须在create_topic之前执行顺序反了后面创建Topic时会找不到类型。第三DataWriterQos最好从publisher.get_default_datawriter_qos()拿再在默认值基础上改不要凭空构造。第四writer.write(sample)返回的返回值要认真处理虽然示例里我没做分支判断但在生产环境里返回码不是RETCODE_OK说明写入被拒。这里简单加上判断逻辑ret writer.write(sample) if ret ! 0: print(f消息写入失败错误码: {ret})3.3 步骤三订阅端实现订阅端的代码结构和发布端很相似区别在于创建的是Subscriber和DataReader并且要注册一个监听器当收到数据时触发回调。from fastdds import ( DomainParticipantFactory, DomainParticipantQos, SubscriberQos, DataReaderQos, TopicQos, DataReaderListener, RELIABLE_RELIABILITY_QOS, ) import HelloWorld TOPIC_NAME HelloWorldTopic TYPE_NAME HelloWorld class SubListener(DataReaderListener): def __init__(self): super().__init__() self.sample HelloWorld.HelloWorld() self.info None def on_data_available(self, reader): super().on_data_available(reader) while reader.take_next_sample(self.sample, self.info) 0: if self.info.valid_data: print(f收到: index{self.sample.index()}, message{self.sample.message()}) def main(): qos DomainParticipantQos() participant DomainParticipantFactory.get_instance().create_participant(0, qos) if participant is None: raise RuntimeError(创建Participant失败) type_support HelloWorld.HelloWorldPubSubType() type_support.setName(TYPE_NAME) participant.register_type(type_support) topic participant.create_topic(TOPIC_NAME, TYPE_NAME, TopicQos()) subscriber participant.create_subscriber(SubscriberQos()) reader_qos subscriber.get_default_datareader_qos() reader_qos.reliability().kind RELIABLE_RELIABILITY_QOS reader subscriber.create_datareader(topic, reader_qos, SubListener()) print(订阅端已启动等待数据...) try: while True: time.sleep(0.1) except KeyboardInterrupt: pass participant.delete_contained_entities() DomainParticipantFactory.get_instance().delete_participant(participant) if __name__ __main__: main()这里有一个高频坑点on_data_available回调里不只要调take_next_sample一次而是要循环调用直到返回值不再是0。因为一次回调可能对应多条积压消息如果只取一条剩余消息可能一直滞留在队列里而且回调不会再触发一次看起来就像消息被吞了。这个细节我在早期调试时吃过亏值得写在代码注释里。3.4 步骤四运行验证与结果解读开两个终端分别激活虚拟环境先跑订阅端再跑发布端python subscriber.py python publisher.py正常情况下订阅端会持续打印类似下面这样的内容收到: index0, messagehello from fastdds python 收到: index1, messagehello from fastdds python 收到: index2, messagehello from fastdds python通过这个示例整个流程就完整跑通了。你观察到的行为可以把DDS的几个特性串起来理解两个进程互不知道对方启动顺序但随后通过发现协议自动找到彼此一旦数据链路建立QoS参数匹配消息就开始流动。你可以试着用CtrlC停掉订阅端再启动只要发布端还开着新订阅端能立刻收到后续消息。如果配置过TRANSIENT_LOCAL的Durability它甚至能收到启动前发布的最后一条历史数据。另一个值得尝试的验证是跨机器互通。在两台Linux机器上分别运行发布端和订阅端保持相同Domain ID和Topic只要处于同一局域网且没有防火墙阻隔组播程序不需要任何地址配置就能互相通信。如果跨网段或组播被禁止才需要配置发现协议的白名单或静态对端地址。4. 常见问题与排查技巧实录4.1 安装失败绝大多数是版本和依赖问题我见过最多的问题是pip install fastdds-python时提示找不到匹配版本。这种情况大概率是Python版本太新或太旧。解决办法是切换到官方支持的Python版本范围一般建议3.10或3.11这个区间兼容性最稳。另一个常见问题是import fastdds时报错ImportError: libfastdds.so: cannot open shared object file这个报错说明预编译包里捆绑的运行时库没有被正确加载或者你需要单独安装FastDDS依赖。在Ubuntu系系统上先安装系统依赖再做源码编译是最稳妥的解决路径sudo apt update sudo apt install python3-dev cmake g libasio-dev libtinyxml2-dev libssl-dev libxerces-c-dev然后从源码编译FastDDS C库再把Python绑定编译进去。这个过程依赖项比较多建议每一步都确认成功后再继续别一口气全跑到最后才看输出那样出错不好定位。4.2 两端收不到消息先查QoS再查Domain收发数据不互通90%以上出在这几个地方。首先查Domain ID是否一致一个用0一个用1就像进了不同群聊互相不可见。其次查QoS是否兼容特别是Reliability配置。发布端是RELIABLE而订阅端要求BEST_EFFORT两者可以协商反过来发布端BEST_EFFORT而订阅端RELIABLE往往协商失败订阅端一直等不到数据。最后查Topic名称和类型名注意大小写和空格一个字符不对就无法匹配。排查这类问题有个技巧在监听器的on_publication_matched和on_subscription_matched回调里打印事件能看到匹配状态变化。如果在回调里一直没打出来说明发现都没完成重点查网络和Domain ID如果打出来了但收不到数据说明问题出在数据链路或QoS上。4.3 性能不达预期从传输和配置两处入手有人会说Python绑定性能差我用下来觉得要区分场景。如果消息本身不大频率也不高Python绑定完全能扛住。但如果追求高吞吐比如视频帧、雷达点云这类数据就有几个调整手段。第一把QoS的Reliability改成BEST_EFFORT减少重传确认的额外开销。第二把History的depth调小比如KEEP_LAST(1)防止内存缓存被旧消息堆满。第三开启共享内存传输。FastDDS支持在局域网内自动协商共享内存传输特别是在同一台机器上的多个进程之间共享内存能避免数据拷贝吞吐提升非常明显。我记得之前在一个点云回放工具里同一份Python代码默认UDP传输大概每秒处理3000条消息开启共享内存通道后几乎翻了一倍CPU占用还更低了。这些优化在FastDDS的配置文档里都有对应参数按需开启就行。5. 一些后续可以继续折腾的方向跑通HelloWorld之后FastDDS还有不少值得深入尝试的方向。最直接的是把消息类型改复杂比如嵌套结构、数组、枚举、联合体体验一下IDL类型系统的表达能力这和Protobuf的体验很像但对二进制流和硬实时场景更友好。其次是尝试FastDDS的自动发现和动态发现在真实设备上跑两个C节点和一个Python节点感受异构通信的便利性。还有一个很实用的扩展如果你在ROS 2项目里工作可以把FastDDS Python绑定作为调试工具链的一部分。虽然直接订阅ROS 2话题需要处理命名空间和类型映射但借助FastDDS工具你能够看到RTPS层的数据流理解ROS 2底层到底在传什么。我自己在项目里就专门留了一个Python脚本用来嗅探机器人系统里继电器的状态主题排查问题效率远高于翻日志。最后分享一个小习惯我通常把fastdds-python固定安装在独立虚拟环境里并把生成的类型文件和自己的业务代码分目录管理IDL文件纳入版本控制。这样换电脑、换版本号的时候一整套环境几分钟就能复现不会出现“之前能跑重新配就炸了”的情况。FastDDS用起来其实没有想象中复杂从HelloWorld跑到实际应用一个下午足够。