Fast DDS v2.8.2 数据流程代码解析

Fast DDS v2.8.2 数据流程代码解析

本篇文章基于Fast DDS v2.8.2版本,官方的发布端(publisher)和订阅端(subscriber)示例程序,来梳理FastDDS源码实现中的数据收发流程。

概览

首先,我们给出基于Fast DDS库应用的架构图,便于理解Fast DDS库的概貌。

基于Fast DDS库应用的架构图

从上图我们可以看到,基于Fast DDS架构的应用程序主要分为四层:

  • 应用层:在 分布式系统 中使用 Fast DDS API 实现通信的用户应用程序。
  • Fast DDS层:DDS通信中间件的实现。 它允许部署一个或多个 DDS 域,其中在同一域中的 DomainParticipants 通过 发布(Publish)/订阅(Subscribe) Topic 来交换消息。
  • RTPS层:实时发布订阅 (RTPS) 协议的实现 与 DDS 应用程序的互操作性。 该层充当传输层的抽象层。
  • 传输层:Fast DDS 可基于各种传输协议收发数据, 例如不可靠的运输协议(UDP),可靠 传输协议(TCP)或共享内存传输协议(SHM)。

示例代码

下面介绍一下 fastdds 官网给出的示例代码,我们后面会根据这个示例来逐步分析fastdds 的源码:

官方给出了发布端(publisher)和订阅端(subscriber)示例程序,前者是发送端程序,后者是接收端程序。

如果跨网络通信,则两个程序运行在不同的机器上(两台机器在同一个局域网中)。

当然发布端(publisher)和订阅端(subscriber)程序也可以运行在同一台设备上,这时候就是使用了dds的跨进程通信(共享内存)的能力。

官方的发布(publishe)和订阅(subscribe)示例程序链接: Writing a simple C++ publisher and subscriber application
fast-dds.docs.eprosima.com/en/v2.8.2/f...

附录也会列出完整的示例源代码。

下图给出了发布端(Publisher)和订阅端(Subscriber)示例程序的Fast DDS API调用流程。

发布端(Publisher)的API流程如下:

  • 第一步创建了DomainParticipant对象,这个对象包含很多内容,包括RTPSParticipant对象,可以理解为Fast DDS里的根对象,管理其它所有的子对象。
  • 第二步调用了TypeSupport 的register_type,这里面主要是为了之后数据传输的过程中数据解析使用,约定传输数据的数据结构。如果type 不对,数据解析就无法进行。示例代码里对应的是HelloWorldPubSubType(通过HelloWorld.idl文件描述生成)
  • 第三步创建的Publisher对象,是在DomainParticipant内部创建的,DomainParticipant内部可以包含0到多个Publisher和Subsciber,Publisher对象是消息的发布者,Subsciber对象是消息的接收者。
  • 第四步创建了Topic 对象,topic 就是通信的主题,只有在同一topic下才能互相通信。
  • 第五步在第三步的基础上创建 DataWriter 对象。
  • 第六步通过 DataWriter 对象的write()接口发送数据,前提是有匹配的订阅端。

订阅端(subscriber)的API流程如下:其中第一、二、四步和发布端(Publisher)的API流程是一样的。

  • 第一步创建了DomainParticipant对象,这个对象包含很多内容,包括rtpsparticipant对象,可以理解为Fast DDS里的根对象,管理其它所有的子对象。
  • 第二步调用了TypeSupport 的register_type,这里面主要是为了之后数据传输的过程中数据解析使用,约定传输数据的数据结构。如果type 不对,数据解析就无法进行。示例代码里对应的是HelloWorldPubSubType(通过HelloWorld.idl文件描述生成)
  • 第三步创建的Subscriber对象,是在DomainParticipant内部创建的
  • 第四步创建了Topic 对象,topic 就是通信的主题,只有在同一topic下才能互相通信。
  • 第五步在第三步的基础上创建DataReader对象,并且注册了DataReaderListener的子类
  • 第六步在DataReader收到数据时,会回调on_data_available()通知到上层应用,上层应用再通过DataReader的take_next_sample()接口读取数据。

我们主要介绍发布端(Publisher)和订阅端(Subscriber)之间的数据传输在Fast DDS库中的代码实现。所以Fast DDS库中,发布端(Publisher)和订阅端(Subscriber)的初始化部分代码(以及PDP和EDP协议流程)不会在本文中描述。

发布端(Publisher)在Fast DDS库中的数据发送流程

为了后续更容易的看懂函数调用的时序图,我们先给出Fast DSS库中跟发布端相关的类的关系图。

发布端(publisher)涉及的类的静态关系图

粗略看一下,有30多个类,这还只是跟发布端(publisher)相关的主要类,我们可以把上图中的类划分为:

  • DDS域的类:
    • DomainParticipant和DomainParticipantImpl
    • Topic和TopicDescription
    • Publisher和PublisherImpl
    • DataWriter和DataWriterImpl
    • 缓存WriterHistory和CacheChange_t
  • RTPS域的类:
    • RTPSParticipant和RTPSParticipantImpl
    • RTPSWriter及其子类:StatefulWriter、StatelessWriter
    • RTPSMessageSenderInterface及其子类:LocatorSelectorSender
    • 网络传输相关的类:NetworkFactory,TransportInterface及其子类:UDPTransportInterface、UDPv4Transport、UDPv6Transport、TCPTransportInterface、TCPv4Transport、TCPv6Transport、SharedMemTransport
    • 发送相关的类:SenderResource及其子类:UDPSenderResource、TCPSenderResource、SharedMemSenderResource
    • 流控相关的类:FlowControllerFactory,FlowController及其子类:FlowControllerImpl
    • 消息相关的类:RTPSMessageGroup和CDRMessage_t

DSS域的类大部分都使用了Impl惯用法,我理解因为DSS域中的大部分类都是应用层(Applilcation)能看到的接口类,所以桥接模式可以减少耦合,避免内部定义(头文件)的暴露。

DSS域中的WriterHistory类作为DSS域和RTPS域之间的数据传输缓存很重要:

WriterHistory 主要作用

  1. 历史记录管理:维护已发送但尚未被所有订阅者确认接收的数据样本的历史记录
  2. 可靠性支持:在可靠通信模式下,确保数据能够被重新发送给未能成功接收的订阅者
  3. 样本生命周期管理:跟踪数据样本的状态(已发送、已确认等)

WriterHistory 核心功能

  • 存储已发布的数据样本:保存写入但尚未被所有订阅者确认的样本
  • 支持样本回收:当样本被所有订阅者确认后,可以回收内存
  • 提供重传机制:当检测到订阅者丢失数据时,可以从历史记录中重新发送
  • 管理历史深度:根据配置的历史深度限制,自动移除旧的样本

而RTPS域的类虽然众多,但在发布端(publisher)扮演的角色其实就分类两大类:

  • 数据组包
  • 数据发送

具体每个类的作用,我会在下面的时序图中,分别介绍。

发布端(publisher)数据发送时序图

上图是我根据代码调用栈,将我认为关键的函数调用放入时序图,看上去好像很复杂,其实干的就是中间件数据发送的标准动作:序列化、组包、发送。

接下来我将分段介绍整个发送流程。

1. 序列化

数据发送从writer_->write(&hello_)开始,对应于HelloWorldPublisher.cpp中的如下代码:

cpp 复制代码
    //!Send a publication
    bool publish()
    {
        if (listener_.matched_ > 0)
        {
            hello_.index(hello_.index() + 1);
            writer_->write(&hello_);
            return true;
        }
        return false;
    }
  • write调用被DataWriter类转发給DataWriterImpl类
  • 通过HelloWorldPubSubType类完成数据序列化
  • 通过RTPSWriter类创建CacheChange_t对象,并将序列化的数据保存在其中。
  • 将CacheChange_t对象放入DataWriterHistory缓存。

2. 组包

这块的流程看上去最为复杂,各种来回调用,其实最关键的就是为了创建RTPSMessageGroup对象,进行组包,而FlowControlImpl类的主要作用就是为了限制发送速率和支持 QoS 策略。

3. 数据发送

RTPSMessageGroup类会在析构函数中调用send函数,通过RTPSMessageSenderInterface(LocatorSelectorSender)对象,沿着

  • RTPSWriter(StatefulWriter)
  • RTPSParticipantImpl
  • SenderResource(UDPSenderResource)
  • TransportInterface(UDPTransportInterface)
  • eProsimaUDPSocket

最终通过UDP socket发送到对端。

这里要说明的一点是LocatorSelectorSender类,这个类是 Fast DDS 中用于 管理数据发送的目标网络定位器(Locator)选择策略 的核心辅助类。

它负责在发送 RTPS 消息时,从多个可用的网络定位器(如 UDP、TCP、SHM 等)中选择最优的传输路径,以提高通信效率和可靠性。

如果匹配的订阅端(subscriber)程序是在同一主机,那么默认就会通过共享内存(SHM)方式发送,如果匹配的订阅端(subscriber)程序是在局域网里的另外一台主机上,那么默认就会通过UDP方式发送。

本篇文章主要是介绍通过UDP通信的调用过程。

订阅端(Subscriber)在Fast DDS库中的数据接收流程

为了后续更容易的看懂函数调用的时序图,我们先给出Fast DSS库中跟发布端相关的类的关系图。

订阅端(Subscriber)涉及的类的静态关系图

订阅端(subscriber)跟发布端(publisher)有很大一部分类是公共的,另外一部分类是对应关系,即Publisher vs Subscriber,writer vs reader等,我们按照之前类似的方式将上图的类划分为:

  • DDS域的类:
    • DomainParticipant和DomainParticipantImpl
    • Topic和TopicDescription
    • Subscriber和SubscriberImpl
    • DataReader和DataReaderImpl,以及相关的listener类(观察者模式):InnerDataReaderListener(ReaderListener的子类)
    • 缓存ReaderHistory和CacheChange_t
  • RTPS域的类:
    • RTPSParticipant和RTPSParticipantImpl
    • RTPSReader及其子类:StatefulReader、StatelessReader
    • 网络传输相关的类:NetworkFactory,TransportInterface及其子类:UDPTransportInterface、UDPv4Transport、UDPv6Transport、TCPTransportInterface、TCPv4Transport、TCPv6Transport、SharedMemTransport
    • 消息接收相关的类:ReceiverControlBlock、ReceiverResource、MessageReceiver、UDPChannelResource

缓存ReaderHistory的作用和WriterHistory类似,只不过是用在数据接收端:

  • 存储 DataReader 接收到的所有数据样本(CacheChange_t),形成接收历史记录。
  • 维护数据的 顺序性 和 完整性(特别是在可靠传输模式下)。

在订阅端(subscriber)的RTPS层里,最主要的两个类应该就是ReceiverResource 和 MessageReceiver了。

ReceiverResource 是 网络接收资源的封装,主要功能包括:

  1. 管理底层传输层资源
    • 绑定到特定的传输协议(如 UDP、TCP、共享内存等)。
    • 监听指定的网络端口,等待数据到达。
  2. 接收原始网络数据
    • 从传输层读取字节流。
  3. 委托消息解析
    • 将接收到的数据传递给关联的 MessageReceiver 进行解析。

MessageReceiver 是 RTPS 消息的解析处理器,主要功能包括:

  1. 解析 RTPS 消息
    • 解码接收到的二进制数据,提取 RTPS 子消息(如 DATA、HEARTBEAT、ACKNACK 等)。
  2. 分发给对应的 RTPS 端点
    • 根据消息中的 GUID,将数据传递给匹配的 RTPSReader 或 RTPSWriter。
  3. 处理消息逻辑
    • 执行订阅匹配、可靠性控制(如 ACK 回复)等。

两者协作完成 "接收字节流 → 解析 RTPS 消息 → 触发业务逻辑" 的完整链路

下面根据时序图,我们看一下订阅端(Subscriber)数据接收的整个流程。

订阅端(Subcriber)数据接收时序图(1)

订阅端(Subcriber)数据接收时序图(2)

可能大家会好奇,为什么订阅端(Subcriber)的时序图会被拆成两个呢?其实我们看了示例代码就会很清楚了。

作为网络通信程序,订阅端(Subcriber)的处理逻辑应该和发布端(Publisher)正好相反,也就是:

  • 数据接收
  • 数据解包
  • 反序列化

而Fast DDS的订阅端(Subcriber)数据接收是由RTPS层的接收线程完成的,然后RTPS层完成数据接收和数据解包后,通过回调函数(listener)的方式通知上层应用,然后上层应用再通过相应的接口,从缓存里读取和反序列化数据。

cpp 复制代码
    class SubListener : public DataReaderListener
    {
    public:
        // ...
        void on_data_available(
                DataReader* reader) override
        {
            SampleInfo info;
            if (reader->take_next_sample(&hello_, &info) == eprosima::fastrtps::types::ReturnCode_t::RETCODE_OK)
            {
                if (info.valid_data)
                {
                    samples_++;
                    std::cout << "Message: " << hello_.message() << " with index: " << hello_.index()
                              << " RECEIVED." << std::endl;
                }
            }
        }

        HelloWorld hello_;

        std::atomic_int samples_;

    }

上面的代码中,SubListener::on_data_available就是RTPS层会回调的函数,然后reader->take_next_sample(&hello_, &info)这行代码就是上层应用从缓存中读取和反序列化的过程。

所以我们的序列图就自然的以SubListener::on_data_available函数为分界线,序列图(1)描述的就是数据接收和数据解包的过程,序列图(2)描述的就是上层应用主动读取和反序列化的过错。

接下来我们逐步说明订阅端(Subcriber)的时序图

1. 数据接收

首先是UDPChannelResource::perform_listen_operation这个函数(图中没体现)是作为一个独立线程函数运行的,它里面是个无限循环,不断的调用socket的receive_from函数,从网络获取原始数据,然后把message_buffer发给ReceiverResource,而ReceiverResource将网络字节流转成CDRMessage_t对象,再传给MessageReceiver做后续处理。

这段流程主要完成数据接收和传给数据处理模块的工作。

2. 数据接收和分发

上图流程主要是两大块:

  • MessageReceiver解包,提取submessage的过程
  • MessageReceiver将数据分发给RTPSReader的过程

经过这两步,数据就从DRMessage_t对象 转变成 CacheChange_t对象。

3. 通知上层应用读取数据

上图流程主要是两大块:

  • RTPSReader将CacheChange_t对象缓存到ReaderHistroy中
  • 回调通知上层应用

然后上层应用就可以通过reader->take_next_sample(&hello_, &info)语句从缓存里读取并反序列化数据。

4. 读取和反序列化数据

上图流程主要是两大块:

  • 从ReaderHistroy中读取缓存数据,创建ReadTakeCommand
  • ReadTakeCommand完成数据反序列化,并从ReaderHistory中删除已读的缓存数据

至此,整个订阅端(Subcriber)程序接收流程基本完成。

参考文档:

相关推荐
天天摸鱼的java工程师26 分钟前
如何实现一个红包系统,支持并发抢红包?
后端
稳妥API26 分钟前
Gemini 2.5 Pro vs Flash API:正式版对比选择指南,深度解析性能与成本平衡 - API易-帮助中心
后端
深栈解码30 分钟前
OpenIM 源码深度解析系列(十一):群聊系统架构与业务设计
后端
AI+程序员在路上34 分钟前
ABI与API定义及区别
c语言·开发语言·c++
trow34 分钟前
Spring 手写简易IOC容器
后端·spring
山城小辣椒35 分钟前
spring-cloud-gateway使用websocket出现Max frame length of 65536 has been exceeded
后端·websocket·spring cloud
天天摸鱼的java工程师37 分钟前
谈谈你对 AQS(AbstractQueuedSynchronizer)的理解?
后端
鸡窝头on37 分钟前
Spring Boot 多 Profile 配置详解
spring boot·后端
风之旅人39 分钟前
开发必备"节假日接口"
java·后端·开源
鑫有灵溪41 分钟前
Redis 8 架构评估:企业级缓存方案的技术选型与实践指南
后端