【FastDDS】Layer DDS之Core (详解)

3.1.1 实体(Entity)

Entity(实体)是所有 DDS 实体的抽象基类,指的是支持 QoS(服务质量)策略、监听器(Listener)和状态(Status)的对象。

3.1.1.1 实体类型

DDS 包含以下几类核心实体,各类实体的功能与定位如下:

  • DomainParticipant(域参与者):作为 DDS 服务的入口,同时也是创建 Publisher(发布器)、Subscriber(订阅器)和 Topic(主题)的工厂类。更多细节可参考《DomainParticipant 文档》。
  • Publisher(发布器):作为工厂类,可创建任意数量的 DataWriter(数据写入器)。更多细节可参考《Publisher 文档》。
  • Subscriber(订阅器):作为工厂类,可创建任意数量的 DataReader(数据读取器)。更多细节可参考《Subscriber 文档》。
  • Topic(主题):此类实体介于发布类实体与订阅类实体之间,充当数据传输的“通道”。更多细节可参考《Topic 文档》。
  • DataWriter(数据写入器):负责数据分发的对象。更多细节可参考《DataWriter 文档》。
  • DataReader(数据读取器):用于获取接收数据的对象。更多细节可参考《DataReader 文档》。

下图展示了所有 DDS 实体之间的层级关系:
在这里插入图片描述

3.1.1.2 实体的通用特性

所有类型的实体都具备一些属于“实体”概念的通用特性,具体如下:

3.1.1.2.1 实体标识符(Entity Identifier)

每个实体都有一个唯一的 ID 作为标识;若该 DDS 实体存在对应的 RTPS(实时发布订阅协议)实体,两者会共享此 ID。
该 ID 存储在 Entity 基类中声明的 Instance Handle(实例句柄)对象中,可通过 getter 方法 get_instance_handle() 获取。

3.1.1.2.2 QoS 策略(QoS Policy)

每个实体的行为可通过一组配置策略进行调整。对于每种实体类型,都存在对应的 QoS(服务质量)类,该类会整合所有影响该类型实体的策略。

用户可按以下流程配置实体的 QoS:

  1. 创建对应 QoS 类的实例;
  2. 根据需求修改实例中包含的策略;
  3. 在实体创建时配置 QoS,或在实体创建后通过所有实体都支持的 set_qos() 方法修改(例如 DomainParticipant::set_qos()Publisher::set_qos()Subscriber::set_qos()Topic::set_qos()DataWriter::set_qos()DataReader::set_qos())。

关于可用的 QoS 策略及其描述,可参考《Policy 文档》;各实体类型对应的 QoS 类及所含策略,可在对应实体的文档中查看。

3.1.1.2.3 监听器(Listener)

监听器是一个包含特定函数的对象,当实体发生特定事件时,会调用这些函数。因此,监听器本质是一种异步通知机制,能让实体将自身的“状态变化”告知应用程序。

监听器的使用流程
  1. 定义接口:所有实体类型都会定义一个抽象的监听器接口,接口中包含实体触发“状态变化通知”时需调用的回调函数;
  2. 实现监听器:用户可继承这些接口,实现应用程序所需的回调函数,从而自定义监听器;
  3. 绑定监听器:在实体创建时绑定监听器,或在实体创建后通过所有实体都支持的 set_listener() 方法绑定(例如 DomainParticipant::set_listener()Publisher::set_listener()Subscriber::set_listener()Topic::set_listener()DataWriter::set_listener()DataReader::set_listener())。

各实体类型对应的监听器接口及回调函数,可在对应实体的文档中查看。当事件发生时,会由“层级最低且监听器非空、且对应回调在 StatusMask(状态掩码)中已启用”的实体处理该事件。

高层级监听器继承自低层级监听器,具体继承关系如下图所示:

监听器类型包含的回调函数(示例)
DataReaderListeneron_sample_rejected()、on_liveliness_changed()、on_requested_deadline_missed()、on_requested_incompatible_qos()、on_data_available()、on_subscription_matched()、on_sample_lost()
DataWriterListeneron_offered_incompatible_qos()、on_offered_deadline_missed()、on_liveliness_lost()、on_publication_matched()、on_unacknowledged_sample_removed()
TopicListeneron_inconsistent_topic()
SubscriberListeneron_data_on_readers()
DomainParticipantListeneron_participant_discovery()、onParticipantAuthentication()、on_data_reader_discovery()、on_data_writer_discovery()

(监听器继承关系图)

注意事项
  • 回调执行顺序on_data_on_readers() 回调会在 on_data_available() 之前拦截消息。这意味着:若启用了 DomainParticipantListener,默认情况下监听器会使用 StatusMask::all()(所有状态启用),此时会优先调用 on_data_on_readers();若应用程序希望使用 on_data_available(),需在 StatusMask 中禁用 on_data_on_readers() 对应的位。
  • StatusMask::none() 的作用范围:创建实体时使用 StatusMask::none(),仅会禁用 DDS 标准回调,具体包括:
    on_sample_rejected()on_liveliness_changed()on_requested_deadline_missed()on_requested_incompatible_qos()on_data_available()on_subscription_matched()on_sample_lost()on_offered_incompatible_qos()on_offered_deadline_missed()on_liveliness_lost()on_publication_matched()on_inconsistent_topic()on_data_on_readers()
    而 Fast DDS 特有的回调始终处于启用状态,具体包括:
    on_participant_discovery()onParticipantAuthentication()on_data_reader_discovery()on_data_writer_discovery()on_unacknowledged_sample_removed()
  • 线程安全与性能:所有已实现的监听器共用一个监听线程,因此建议保持监听器函数逻辑简洁,将复杂的数据处理逻辑交由专门的类实现。
  • 禁止的操作:不要在监听器成员函数的作用域内创建或删除任何实体,否则可能导致未定义行为。建议将监听器仅用作“信息通道”,实体的创建/删除逻辑封装在高层级的实体类中。

3.1.1.2.4 状态(Status)

每个实体都关联一组“状态对象”,这些对象的值反映了该实体的通信状态。当状态值发生变化时,会触发对应的监听器回调,以异步方式告知应用程序。

关于所有状态对象的列表及其内容描述,可参考《Status 文档》;该文档还会说明每种状态适用于哪些实体类型。

3.1.1.2.5 状态条件(StatusCondition)

每个实体都拥有一个 StatusCondition(状态条件)对象,当实体的“已启用状态”发生变化时,该对象会发出通知。StatusCondition 是实体与 Wait-set(等待集)之间的关联纽带。更多细节可参考《Conditions and Wait-sets(条件与等待集)》章节。

3.1.1.2.6 实体的启用(Enabling Entities)

所有实体在创建时,既可以处于“启用状态”,也可以处于“未启用状态”:

  • 默认行为:实体工厂类默认配置为“创建启用状态的实体”;
  • 工厂类 QoS 影响:可通过工厂类的 EntityFactoryQosPolicy(实体工厂 QoS 策略)修改默认行为;若工厂类处于“未启用状态”,则无论其 QoS 如何配置,创建的实体都将处于“未启用状态”。
未启用实体的可用操作

处于“未启用状态”的实体,仅支持以下有限操作:

  1. 设置/获取实体的 QoS 策略;
  2. 设置/获取实体的监听器;
  3. 创建/删除子实体;
  4. 获取实体的状态(即使状态不会变化);
  5. 查找操作(Lookup)。

若调用上述操作以外的函数,将返回 NOT_ENABLED(未启用)状态码。

3.1.2 QoS 策略(Policy)

DDS(数据分发服务)定义了一组 QoS(服务质量)策略,用于配置和控制 DDS 实体的行为。这些策略允许应用程序根据具体需求定制数据分发的特性,如可靠性、持久性、吞吐量等。

3.1.2.1 QoS 策略概述

QoS 策略是 DDS 中非常重要的一部分,它们决定了数据如何在发布者和订阅者之间流动。每个 DDS 实体(如 DomainParticipant、Topic、Publisher、Subscriber、DataWriter、DataReader)都可以关联一组 QoS 策略,这些策略共同定义了该实体的行为特性。

QoS 策略可以分为以下几类:

  • 影响数据分发可靠性的策略
  • 影响数据可用性和持久性的策略
  • 影响资源使用和性能的策略
  • 影响实体发现和匹配的策略
  • 影响数据安全性的策略

3.1.2.2 QoS 策略兼容性

当 DataWriter 和 DataReader 进行匹配时,它们的 QoS 策略必须兼容。QoS 兼容性的规则由 DDS 规范定义,通常有以下几种情况:

  • 某些策略要求完全一致(如 Topic 名称和数据类型)
  • 某些策略允许 DataReader 的要求比 DataWriter 更严格(如可靠性)
  • 某些策略允许 DataWriter 的设置比 DataReader 更严格(如 Deadline)

如果 QoS 策略不兼容,DataWriter 和 DataReader 将无法建立通信,并且会通过监听器或状态机制通知应用程序。

3.1.2.3 常用 QoS 策略详解

3.1.2.3.1 可靠性策略(ReliabilityQosPolicy)

该策略控制数据传输的可靠性级别,有两种可选值:

  • BEST_EFFORT(尽力而为):DataWriter 会发送数据,但不保证 DataReader 一定能收到,不提供重传机制
  • RELIABLE(可靠):DataWriter 保证 DataReader 能收到所有数据,如果检测到数据丢失,会自动重传

此策略在 DataWriter 和 DataReader 之间进行匹配时,要求 DataReader 的可靠性级别不高于 DataWriter 的可靠性级别。也就是说,可靠的 DataWriter 可以与可靠或尽力而为的 DataReader 匹配,而尽力而为的 DataWriter 只能与尽力而为的 DataReader 匹配。

3.1.2.3.2 持久性策略(DurabilityQosPolicy)

该策略控制数据在系统中的持久性,即当 DataReader 加入系统时,是否能收到它加入之前发布的数据。可选值包括:

  • VOLATILE(易失性):不保存任何历史数据,新加入的 DataReader 只能收到它加入后发布的数据
  • TRANSIENT_LOCAL(本地瞬态):DataWriter 会在本地保存历史数据,当新的 DataReader 匹配时,会将历史数据发送给它,但 DataWriter 关闭后数据会丢失
  • TRANSIENT(瞬态):历史数据会被保存到持久化存储中,即使 DataWriter 关闭,数据也不会丢失,当 DataWriter 重新启动时,可以恢复这些数据并发送给新的 DataReader
  • PERSISTENT(持久):与瞬态类似,但数据会被永久保存,即使整个系统重启,数据也不会丢失

3.1.2.3.3 历史记录策略(HistoryQosPolicy)

该策略与持久性策略配合使用,控制需要保存的历史数据量。它有两种模式:

  • KEEP_LAST:只保存最新的 N 个数据样本,N 由 depth 属性指定
  • KEEP_ALL:保存所有数据样本,直到它们被 DataReader 接收或达到资源限制

3.1.2.3.4 截止期限策略(DeadlineQosPolicy)

该策略指定 DataWriter 发布数据的最大时间间隔(period)。如果 DataWriter 未能在指定时间内发布数据,会触发 on_offered_deadline_missed 回调。

对于 DataReader,如果在截止期限内没有收到新数据,会触发 on_requested_deadline_missed 回调。

3.1.2.3.5 生存周期策略(LivelinessQosPolicy)

该策略用于检测 DataWriter 是否还“活跃”,有三种可选的 liveliness 类型:

  • AUTOMATIC(自动):由 DDS 实现自动维护 liveliness,定期发送心跳消息
  • MANUAL_BY_PARTICIPANT(按参与者手动):应用程序需要通过调用 assert_liveliness 方法定期声明 liveliness,该声明适用于同一 DomainParticipant 下的所有 DataWriter
  • MANUAL_BY_TOPIC(按主题手动):应用程序需要通过调用 assert_liveliness 方法定期声明 liveliness,该声明只适用于特定的 DataWriter

lease_duration 属性指定了 DataReader 等待 liveliness 声明的最大时间,如果超过这个时间没有收到 liveliness 声明,DataReader 会认为 DataWriter 已不再活跃。

3.1.2.3.6 所有权策略(OwnershipQosPolicy)

当多个 DataWriter 向同一 Topic 发布数据,且数据具有相同的实例 ID 时,该策略控制哪个 DataWriter 拥有数据的所有权。可选值包括:

  • SHARED(共享):所有 DataWriter 都可以发布数据,DataReader 会接收所有 DataWriter 发布的数据
  • EXCLUSIVE(独占):只有具有最高所有权强度(由 OwnershipStrengthQosPolicy 指定)的 DataWriter 才能发布数据,其他 DataWriter 的数据会被忽略

3.1.2.3.7 所有权强度策略(OwnershipStrengthQosPolicy)

该策略与所有权策略配合使用,当所有权策略为 EXCLUSIVE 时,value 属性指定的强度值决定了 DataWriter 的优先级,值越高,优先级越高。

3.1.2.3.8 目的地策略(DestinationOrderQosPolicy)

该策略控制 DataReader 接收数据的顺序,可选值包括:

  • BY_RECEPTION_TIMESTAMP(按接收时间戳):DataReader 按接收数据的时间顺序处理数据
  • BY_SOURCE_TIMESTAMP(按源时间戳):DataReader 按 DataWriter 发布数据的时间顺序处理数据

3.1.2.3.9 时间戳策略(TimeBasedFilterQosPolicy)

该策略允许 DataReader 过滤掉过于频繁的更新,minimum_separation 属性指定了接收两个连续数据样本之间的最小时间间隔。如果在这个时间间隔内收到新的数据,会被过滤掉。

3.1.2.3.10 实体工厂策略(EntityFactoryQosPolicy)

该策略控制由工厂实体(如 DomainParticipant、Publisher、Subscriber)创建的子实体是否自动启用。autoenable_created_entities 属性为 true 时,创建的子实体会自动启用;为 false 时,需要手动调用 enable 方法启用子实体。

3.1.2.4 QoS 策略的使用

在 Fast DDS 中,可以通过以下方式设置 QoS 策略:

  1. 创建相应的 QoS 对象(如 DomainParticipantQosTopicQosDataWriterQos 等)
  2. 修改 QoS 对象中的策略属性
  3. 在创建实体时传入 QoS 对象,或在实体创建后使用 set_qos 方法修改

例如,设置 DataWriter 的可靠性策略为可靠:

DataWriterQos qos;
qos.reliability().kind = RELIABLE_RELIABILITY_QOS;
qos.reliability().max_blocking_time = eprosima::fastrtps::Duration_t(1, 0);

// 创建 DataWriter 时使用该 QoS
DataWriter* writer = publisher->create_datawriter(topic, qos);

或者在实体创建后修改 QoS:

DataWriterQos qos;
writer->get_qos(qos);
qos.reliability().kind = RELIABLE_RELIABILITY_QOS;
writer->set_qos(qos);

3.1.2.5 QoS 策略的继承

在 DDS 中,QoS 策略存在一定的继承关系:

  • DomainParticipant 的 QoS 会作为其创建的 Publisher、Subscriber 和 Topic 的默认 QoS
  • Publisher 的 QoS 会作为其创建的 DataWriter 的默认 QoS
  • Subscriber 的 QoS 会作为其创建的 DataReader 的默认 QoS

应用程序可以在任何层级覆盖继承的 QoS 策略,以满足特定需求。

3.1.2.6 自定义 QoS 策略

除了 DDS 规范定义的标准 QoS 策略外,Fast DDS 还支持自定义 QoS 策略,允许用户根据特定需求扩展 QoS 机制。自定义 QoS 策略需要实现特定的接口,并在系统中注册,以便 DDS 框架能够识别和处理它们。

有关自定义 QoS 策略的详细信息,请参考 Fast DDS 的相关文档。

3.1.3 状态(Status)

每个实体(Entity)都关联着一组状态(Status)对象,这些对象的值反映了该实体的通信状态。状态值的变化由与实体相关的通信事件触发,例如新数据到达、发现新的参与者(Participant)、远程端点丢失等。为实现独立管理,状态被拆分为多个状态对象,每个对象对应通信的不同方面,各对象的变化互不干扰。

当某个状态对象发生变化时,会触发对应的监听器(Listener)回调函数,使实体能将事件通知给应用程序。对于名为 fooStatus 的状态对象,实体监听器接口会定义一个回调函数 on_foo(),状态变化时该函数会被调用。注意:部分状态的成员数据会在每次调用对应监听器时重置;唯一例外是实体未绑定监听器的情况(此时回调无法触发,数据不会重置)。具体细节可参考各状态的文档说明。

条件与等待集(Conditions and Wait-sets)为应用程序提供了另一种感知状态变化的机制,该机制通过状态条件(StatusCondition)实现。其优势在于:应用程序可同时等待多个实体的状态变化;且通知不会像监听器那样在内部线程中处理,有助于提升系统的确定性。

实体提供专门的函数用于访问状态值:对于名为 fooStatus 的状态,实体暴露成员函数 get_foo() 以访问其数据(DataOnReadersDataAvailable 状态除外,无对应获取函数)。这些获取函数返回只读结构体,所有成员变量均为公开,供应用程序访问。注意:部分状态的成员数据会在应用程序每次调用获取函数时重置,具体细节可参考各状态的文档说明。

下文将详细介绍每个状态对象、其成员数据及所属实体类型。下表汇总了所有状态的关键信息,包括所属实体、对应监听器回调、访问函数及在 StatusMask(状态掩码)中的位标识。

状态名称(Status Name)所属实体(Entity)监听器回调(Listener Callback)访问函数(Accessor)位标识(Bit)
InconsistentTopicStatusTopicon_inconsistent_topic()get_inconsistent_topic_status()0
OfferedDeadlineMissedStatusDataWriteron_offered_deadline_missed()get_offered_deadline_missed_status()1
RequestedDeadlineMissedStatusDataReaderon_requested_deadline_missed()get_requested_deadline_missed_status()2
OfferedIncompatibleQosStatusDataWriteron_offered_incompatible_qos()get_offered_incompatible_qos_status()5
RequestedIncompatibleQosStatusDataReaderon_requested_incompatible_qos()get_requested_incompatible_qos_status()6
SampleLostStatusDataReaderon_sample_lost()get_sample_lost_status()7
SampleRejectedStatusDataReaderon_sample_rejected()get_sample_rejected_status()8
DataOnReadersSubscriberon_data_on_readers()N/A(无)9
DataAvailableDataReaderon_data_available()N/A(无)10
LivelinessLostStatusDataWriteron_liveliness_lost()get_liveliness_lost_status()11
LivelinessChangedStatusDataReaderon_liveliness_changed()get_liveliness_changed_status()12
PublicationMatchedStatusDataWriteron_publication_matched()get_publication_matched_status()13
SubscriptionMatchedStatusDataReaderon_subscription_matched()get_subscription_matched_status()14

3.1.3.1 主题不一致状态(InconsistentTopicStatus)

当发现主题不一致的远程 Topic(即名称相同但特性不同的 Topic)时,此状态会发生变化。更多细节可参考 InconsistentTopicStatus

状态成员数据

成员名称(Data Member Name)类型(Type)说明
total_countint32_t自当前 Topic 创建以来,发现的不一致远程 Topic 的累计总数。
total_count_changeint32_t自上次调用 on_inconsistent_topic() 或读取状态后,total_count 的变化量。

3.1.3.2 读取器有数据状态(DataOnReaders)

当当前订阅器(Subscriber)所属的任意数据读取器(DataReader)有新数据可供应用程序读取时,此状态会被激活。
该状态无访问函数,因为它不记录与数据本身相关的任何信息,唯一作用是触发绑定到 DataReader 的监听器的 on_data_on_readers() 回调。

3.1.3.3 数据可用状态(DataAvailable)

当当前数据读取器(DataReader)有新数据可供应用程序读取时,此状态会被激活。
该状态无访问函数,因为它不记录与数据本身相关的任何信息,唯一作用是触发绑定到 DataReader 的监听器的 on_data_available() 回调。

3.1.3.4 活跃度变化状态(LivelinessChangedStatus)

当匹配的数据写入器(DataWriter)的活跃度状态发生变化时(例如从“活跃”变为“不活跃”,或反之),此状态会发生变化。更多细节可参考 LivelinessChangedStatus

状态成员数据

成员名称(Data Member Name)类型(Type)说明
alive_countint32_t当前活跃的 DataWriter 总数。新匹配的 DataWriter 声明活跃度、或“不活跃”的 DataWriter 重新声明活跃度时,此值增加;活跃的 DataWriter 变为“不活跃”(未声明活跃度或被删除)时,此值减少。
not_alive_countint32_t当前“不活跃”的匹配 DataWriter 总数。活跃的 DataWriter 未声明活跃度而变为“不活跃”时,此值增加;“不活跃”的 DataWriter 重新声明活跃度时,此值减少(DataWriter 的正常匹配/解除匹配不影响此值)。
alive_count_changeint32_t自上次调用 on_liveliness_changed() 或读取状态后,alive_count 的变化量(可正可负)。
not_alive_count_changeint32_t自上次调用 on_liveliness_changed() 或读取状态后,not_alive_count 的变化量(可正可负)。
last_publication_handleInstanceHandle_t最后一个活跃度状态变化的 DataWriter 的句柄。若从未有活跃度变化,值为 c_InstanceHandle_Unknown

3.1.3.5 请求截止期限超时状态(RequestedDeadlineMissedStatus)

当数据读取器(DataReader)未在其 DataReaderQos 配置的截止期限内接收数据时,此状态会发生变化。更多细节可参考 RequestedDeadlineMissedStatus

状态成员数据

成员名称(Data Member Name)类型(Type)说明
total_countint32_t自当前 DataReader 创建以来,所有实例(Instance)的截止期限超时累计总数(截止期限对每个 Topic 实例独立生效,每个超时实例计一次)。
total_count_changeint32_t自上次调用 on_requested_deadline_missed() 或读取状态后,total_count 的变化量(仅非负)。
last_instance_handleInstanceHandle_t最后一个超时实例的句柄。若从未超时,值为 c_InstanceHandle_Unknown

3.1.3.6 请求 QoS 不兼容状态(RequestedIncompatibleQosStatus)

当数据读取器(DataReader)发现匹配 Topic、分区相同但 QoS 配置不兼容的数据写入器(DataWriter)时,此状态会发生变化。更多细节可参考 RequestedIncompatibleQosStatus

状态成员数据

成员名称(Data Member Name)类型(Type)说明
total_countint32_t自当前 DataReader 创建以来,发现的“匹配 Topic、分区相同但 QoS 不兼容”的 DataWriter 累计总数。
total_count_changeint32_t自上次调用 on_requested_incompatible_qos() 或读取状态后,total_count 的变化量(仅非负)。
last_policy_idQosPolicyId_t导致不兼容的其中一个 QoS 策略 ID。若存在多个不兼容策略,仅报告其中一个。
policiesQosPolicyCountSeq集合类型,记录每个 QoS 策略被发现不兼容的次数(针对“匹配 Topic、分区相同”的远程 DataWriter)。详情可参考 QosPolicyCountSeqQosPolicyCount
3.1.3.6.1 QoS 策略计数序列(QosPolicyCountSeq)

QosPolicyId_t(QoS 策略 ID)索引,存储每个策略的 QosPolicyCount(策略计数)。例如,ID 为 N 的策略会存储在序列的第 N 个位置。更多细节可参考 QosPolicyCountSeq

使用示例:获取可靠性策略(ReliabilityQosPolicy)与远程 DataWriter 不兼容的次数

DataReader* reader_ = subscriber_->create_datareader(topic_, DATAREADER_QOS_DEFAULT);

// 获取 RequestedIncompatibleQosStatus 状态
RequestedIncompatibleQosStatus status;
reader_->get_requested_incompatible_qos_status(status);

// 读取可靠性策略的不兼容次数(RELIABILITY_QOS_POLICY_ID 为可靠性策略的 ID)
uint32_t incompatible_reliability_count = status.policies[RELIABILITY_QOS_POLICY_ID].count;
3.1.3.6.2 QoS 策略计数(QosPolicyCount)

该结构体记录单个 QoS 策略的不兼容次数。更多细节可参考 QosPolicyCount

成员名称(Data Member Name)类型(Type)说明
policy_idQosPolicyId_tQoS 策略的 ID。
countint32_t该策略被发现不兼容的累计次数。

3.1.3.7 样本丢失状态(SampleLostStatus)

新数据样本丢失且永远无法接收时,此状态会发生变化。更多细节可参考 SampleLostStatus

样本丢失的判定逻辑取决于 reliability()(可靠性 QoS 策略):

  • 若使用 BEST_EFFORT_RELIABILITY_QOS(尽力而为可靠性):当接收到序列号更大的样本时,判定未接收的样本已丢失。
  • 若使用 RELIABLE_RELIABILITY_QOS(可靠可靠性):当 DataWriter 通过 RTPS HEARTBEAT 子消息告知样本不再可用时,判定样本已丢失。

状态成员数据

成员名称(Data Member Name)类型(Type)说明
total_countint32_t自当前 DataReader 创建以来,所属 Topic 的样本丢失累计总数。
total_count_changeint32_t自上次调用 on_sample_lost() 或读取状态后,total_count 的变化量(仅非负)。

3.1.3.8 样本被拒绝状态(SampleRejectedStatus)

传入的数据样本被 DataReader 拒绝时,此状态会发生变化。拒绝原因由 SampleRejectedStatusKind(样本拒绝状态类型)定义。更多细节可参考 SampleRejectedStatus

状态成员数据

成员名称(Data Member Name)类型(Type)说明
total_countint32_t自当前 DataReader 创建以来,所属 Topic 的样本被拒绝累计总数。
total_count_changeint32_t自上次调用 on_sample_rejected() 或读取状态后,total_count 的变化量(仅非负)。
last_reasonSampleRejectedStatusKind最后一个被拒绝样本的拒绝原因。若从未拒绝样本,值为 NOT_REJECTED。详情可参考 SampleRejectedStatusKind
last_instance_handleInstanceHandle_t最后一个被拒绝样本所属实例的句柄。若从未拒绝样本,值为 c_InstanceHandle_Unknown
3.1.3.8.1 样本拒绝状态类型(SampleRejectedStatusKind)

在 Fast DDS 中,样本被拒绝的原因均与资源限制相关,但被拒绝的样本未必会丢失(未来可能被接受)。SampleRejectedStatusKind 定义了以下拒绝原因:

  • NOT_REJECTED:样本未被拒绝。
  • REJECTED_BY_SAMPLES_LIMIT:因资源不足无法存储样本而拒绝。即使存在空闲资源,若需为其他样本(如序列号更小的未接收样本)预留资源,也可能拒绝(发生在 RTPS 层,通常因 max_samples 达到上限)。
  • REJECTED_BY_INSTANCES_LIMIT:因无法为样本所属实例分配资源而拒绝(发生在 DDS 层的 DataReader 历史缓存中,通常因 max_instances 达到上限,无法为新实例预留资源)。
  • REJECTED_BY_SAMPLES_PER_INSTANCE_LIMIT:因实例内样本数达到上限而拒绝(发生在 DDS 层的 DataReader 历史缓存中,通常因 DataReader 配置为 KEEP_ALL_HISTORY_QOS(保留所有历史)且 max_samples_per_instance 达到上限)。

3.1.3.9 订阅匹配状态(SubscriptionMatchedStatus)

当数据读取器(DataReader)发现匹配 Topic、分区相同且 QoS 兼容的数据写入器(DataWriter),或与之前匹配的 DataWriter 解除匹配时,此状态会发生变化。更多细节可参考 SubscriptionMatchedStatus

状态成员数据

成员名称(Data Member Name)类型(Type)说明
total_countint32_t自当前 DataReader 创建以来,发现的“匹配 Topic、分区相同且 QoS 兼容”的远程 DataWriter 累计总数(当前未必仍处于匹配状态)。
total_count_changeint32_t自上次调用 on_subscription_matched() 或读取状态后,total_count 的变化量(仅非负)。
current_countint32_t当前与 DataReader 匹配的远程 DataWriter 数量。
current_count_changeint32_t自上次调用 on_subscription_matched() 或读取状态后,current_count 的变化量(可正可负)。
last_publication_handleInstanceHandle_t最后一个与 DataReader 匹配的 DataWriter 的句柄。若从未匹配,值为 c_InstanceHandle_Unknown

3.1.3.10 活跃度丢失状态(LivelinessLostStatus)

当数据写入器(DataWriter)未在其 DataWriterQos 配置的期限内声明活跃度时,此状态会发生变化(此时匹配的 DataReader 会将该 DataWriter 判定为“不活跃”)。更多细节可参考 LivelinessLostStatus

状态成员数据

成员名称(Data Member Name)类型(Type)说明
total_countint32_t自当前 DataWriter 创建以来,未在期限内声明活跃度而被判定为“不活跃”的累计次数

(若 DataWriter 已被判定为“不活跃”,后续未按时声明活跃度不会增加此计数)。

  • total_count_changeint32_t,自上次调用 on_liveliness_lost() 或读取状态后,total_count 的变化量(仅非负)。

3.1.3.11 发布截止期限超时状态(OfferedDeadlineMissedStatus)

当数据写入器(DataWriter)未在其 DataWriterQos 配置的截止期限内发布数据时,此状态会发生变化。更多细节可参考 OfferedDeadlineMissedStatus

状态成员数据

成员名称(Data Member Name)类型(Type)说明
total_countint32_t自当前 DataWriter 创建以来,所有实例(Instance)的截止期限超时累计总数(截止期限对每个 Topic 实例独立生效,每个超时实例计一次)。
total_count_changeint32_t自上次调用 on_offered_deadline_missed() 或读取状态后,total_count 的变化量(仅非负)。
last_instance_handleInstanceHandle_t最后一个超时实例的句柄。若从未超时,值为 c_InstanceHandle_Unknown

3.1.3.12 发布 QoS 不兼容状态(OfferedIncompatibleQosStatus)

当数据写入器(DataWriter)发现匹配 Topic、分区相同但 QoS 配置不兼容的数据读取器(DataReader)时,此状态会发生变化。更多细节可参考 OfferedIncompatibleQosStatus

状态成员数据

成员名称(Data Member Name)类型(Type)说明
total_countint32_t自当前 DataWriter 创建以来,发现的“匹配 Topic、分区相同但 QoS 不兼容”的 DataReader 累计总数。
total_count_changeint32_t自上次调用 on_offered_incompatible_qos() 或读取状态后,total_count 的变化量(仅非负)。
last_policy_idQosPolicyId_t导致不兼容的其中一个 QoS 策略 ID。若存在多个不兼容策略,仅报告其中一个。
policiesQosPolicyCountSeq集合类型,记录每个 QoS 策略被发现不兼容的次数(针对“匹配 Topic、分区相同”的远程 DataReader)。详情可参考 QosPolicyCountSeqQosPolicyCount

3.1.3.13 发布匹配状态(PublicationMatchedStatus)

当数据写入器(DataWriter)发现匹配 Topic、分区相同且 QoS 兼容的数据读取器(DataReader),或与之前匹配的 DataReader 解除匹配时,此状态会发生变化。更多细节可参考 PublicationMatchedStatus

状态成员数据

成员名称(Data Member Name)类型(Type)说明
total_countint32_t自当前 DataWriter 创建以来,发现的“匹配 Topic、分区相同且 QoS 兼容”的远程 DataReader 累计总数(当前未必仍处于匹配状态)。
total_count_changeint32_t自上次调用 on_publication_matched() 或读取状态后,total_count 的变化量(仅非负)。
current_countint32_t当前与 DataWriter 匹配的远程 DataReader 数量。
current_count_changeint32_t自上次调用 on_publication_matched() 或读取状态后,current_count 的变化量(可正可负)。
last_subscription_handleInstanceHandle_t最后一个与 DataWriter 匹配的 DataReader 的句柄。若从未匹配,值为 c_InstanceHandle_Unknown

3.1.3.14 未确认样本移除状态(UnacknowledgedSampleRemovedStatus)

未确认的样本从 DataWriter 的历史记录中移除时,此状态会发生变化(移除原因可能是样本过期,或达到资源限制)。该状态是 Fast DDS 特有状态,不属于 DDS 规范。更多细节可参考 UnacknowledgedSampleRemovedStatus

状态成员数据

成员名称(Data Member Name)类型(Type)说明
total_countint64_t自当前 DataWriter 创建以来,从其历史记录中移除的未确认样本累计总数。
total_count_changeint64_t自上次调用 on_unacknowledged_sample_removed() 或读取状态后,total_count 的变化量(仅非负)。
last_instance_handleInstanceHandle_t最后一个被移除的未确认样本所属实例的句柄。若从未移除样本,值为 c_InstanceHandle_Unknown

3.1.4 条件与等待集(Conditions and Wait-sets)

条件(Condition)与等待集(Wait-set)配合使用,为中间件向应用程序通知通信状态变化(包括数据到达)提供了一种替代机制。

该机制基于“等待”实现,其通用使用流程如下:

  1. 应用程序通过条件对象(GuardCondition、StatusCondition 或 ReadCondition)指定所需获取的相关信息,并通过 attach_condition() 调用将这些条件对象附加到等待集上。
  2. 应用程序通过 wait() 调用在该等待集上进行等待,直到一个或多个条件对象的触发值(trigger value)变为 true
  3. 应用程序利用 wait() 的结果(即触发值为 true 的条件对象列表),通过以下方式获取实际信息:
    • 若条件为 StatusCondition 且状态变化涉及普通通信状态:调用 get_status_changes() 获取状态变化,使用 StatusMask::is_active() 方法检查结果中是否存在相关变化,最后在相关实体(Entity)上调用 get_<communication_status>()(通信状态获取方法)。有关可查询的各类状态的更多信息,请参考“状态(Status)”章节。
    • 若条件为 StatusCondition 且状态变化涉及 DataOnReaders:调用 get_status_changes(),然后在相关订阅器(Subscriber)上调用 Subscriber::get_datareaders()
    • 若条件为 StatusCondition 且状态变化涉及 DataAvailable:调用 get_status_changes(),然后在相关数据读取器(DataReader)上调用 DataReader::read()DataReader::take()
    • 若条件为 ReadCondition:直接在数据读取器(DataReader)上调用 DataReader::read_w_condition()DataReader::take_w_condition(),并将该条件作为参数传入。
  4. 当某个条件不再相关时,可通过 detach_condition() 调用将其从等待集中分离。

上述第一步通常在初始化阶段完成,其余步骤则放入应用程序的主循环中。

class ApplicationJob
{
    WaitSet wait_set_;
    GuardCondition terminate_condition_;
    std::thread thread_;

    void main_loop()
    {
        // 主循环持续运行,直到终止条件被触发
        while (false == terminate_condition_.get_trigger_value())
        {
            // 等待任一条件被触发
            ReturnCode_t ret_code;
            ConditionSeq triggered_conditions;
            ret_code = wait_set_.wait(triggered_conditions, eprosima::fastdds::dds::c_TimeInfinite);

            if (RETCODE_OK != ret_code)
            {
                // ... 错误处理
                continue;
            }

            // 处理被触发的条件
            for (Condition* cond : triggered_conditions)
            {
                StatusCondition* status_cond = dynamic_cast<StatusCondition*>(cond);
                if (nullptr != status_cond)
                {
                    Entity* entity = status_cond->get_entity();
                    StatusMask changed_statuses = entity->get_status_changes();

                    // 处理状态变化(以活跃度变化和数据可用为例)
                    if (changed_statuses.is_active(StatusMask::liveliness_changed()))
                    {
                        std::cout << "实体 " << entity->get_instance_handle() 
                                  << " 报告活跃度变化" << std::endl;
                    }

                    if (changed_statuses.is_active(StatusMask::data_available()))
                    {
                        std::cout << "读取器 " << entity->get_instance_handle() 
                                  << " 上有数据可用" << std::endl;

                        FooSeq data_seq;
                        SampleInfoSeq info_seq;
                        DataReader* reader = static_cast<DataReader*>(entity);

                        // 处理所有样本,直到无样本可返回
                        while (RETCODE_OK == reader->take(
                            data_seq, info_seq,
                            LENGTH_UNLIMITED, ANY_SAMPLE_STATE,
                            ANY_VIEW_STATE, ANY_INSTANCE_STATE))
                        {
                            // info_seq.length() 和 data_seq.length() 均表示返回的样本数量
                            for (FooSeq::size_type n = 0; n < info_seq.length(); ++n)
                            {
                                // 仅访问包含有效数据的样本
                                if (info_seq[n].valid_data &&
                                    reader->is_sample_valid(&data_seq[n], &info_seq[n]))
                                {
                                    // 处理 data_seq[n] 中的样本
                                }
                            }

                            // 处理完成后,必须归还借用的序列
                            reader->return_loan(data_seq, info_seq);
                        }
                    }
                }
            }
        }
    }

public:

    ApplicationJob(
        const std::vector<DataReader*>& readers,
        const std::vector<DataWriter*>& writers)
    {
        // 添加 GuardCondition,用于通知处理线程停止
        wait_set_.attach_condition(terminate_condition_);

        // 为所有读取器和写入器添加状态条件
        for (DataReader* reader : readers)
        {
            wait_set_.attach_condition(reader->get_statuscondition());
        }
        for (DataWriter* writer : writers)
        {
            wait_set_.attach_condition(writer->get_statuscondition());
        }

        // 启动主循环线程
        thread_ = std::thread(&ApplicationJob::main_loop, this);
    }

    ~ApplicationJob()
    {
        // 设置 GuardCondition 的触发值,强制等待集唤醒
        terminate_condition_.set_trigger_value(true);
        // 等待线程执行完成
        thread_.join();
    }

};

// 应用程序初始化
ReturnCode_t ret_code;
std::vector<DataReader*> application_readers;
std::vector<DataWriter*> application_writers;

// 创建参与者、主题、读取器和写入器
ret_code = create_dds_application(application_readers, application_writers);
if (RETCODE_OK != ret_code)
{
    // ... 错误处理
    return;
}

{
    // 创建主循环线程对象
    ApplicationJob main_loop_thread(application_readers, application_writers);

    // ... 等待应用程序终止信号(信号处理器、用户输入等)

    // ... ApplicationJob 的析构函数会负责停止处理线程
}

// 销毁读取器、写入器、主题和参与者
destroy_dds_application();

若附加到等待集的所有条件的触发值均为 false,调用等待集的 wait() 操作会阻塞调用线程。当任一附加条件的触发值变为 true 时,线程会被唤醒,且 wait() 操作会返回 RETCODE_OK

3.1.4.1 守护条件(GuardCondition)

一种触发值完全由应用程序通过 set_trigger_value() 操作控制的条件。

3.1.4.2 状态条件(StatusCondition)

当某个实体(Entity)的通信状态发生变化时,会被触发的条件。

状态条件对特定通信状态的敏感度,由通过 set_enabled_statuses() 操作在该条件上设置的“启用状态列表(enabled_statuses)”控制。

3.1.4.3 读取条件(ReadCondition)

当创建该条件的数据读取器(DataReader)中存在至少一个“样本状态(SampleState)、视图状态(ViewState)和实例状态(InstanceState)与该读取条件匹配”的样本时,会被触发的条件。

读取条件的触发值依赖于关联数据读取器中是否存在样本,这意味着单次 take() 操作可能会改变多个读取条件的触发值。例如,若所有样本都被 take()(获取),则之前已被触发、且与该数据读取器关联的所有读取条件,其触发值都会变为 false。需注意,这并不能保证分别附加到这些条件上的等待集对象不会被唤醒。一旦某个条件的触发值变为 true,就可能唤醒与其附加的等待集;而条件触发值变为 false 并不一定会“取消唤醒”等待集,因为“取消唤醒”在通常情况下可能无法实现。

由此产生的结果是:阻塞在等待集上的应用程序从等待中返回时,得到的条件列表中可能包含一些已不再触发的条件。这种情况也可能由用户操作导致,例如用户手动调用 set_trigger_value() 就可能引发相同行为。若多个线程同时在不同的等待集对象上等待,并获取与同一个数据读取器实体相关联的数据,这种情况将无法避免。

为进一步说明,可参考以下示例:

  • 若某个读取条件的 sample_state_mask(样本状态掩码)设为 {NOT_READ}(未读),则新样本到达时其触发值会变为 true;当所有新到达的样本被读取(样本状态变为 READ)或被获取(样本不再由数据读取器管理)后,其触发值会立即变为 false
  • 若同一个读取条件的 sample_state_mask 设为 {READ, NOT_READ}(已读、未读),则只有当所有新到达的样本都被获取后,其触发值才会变为 false(仅读取样本不足以使其触发值变化,因为读取仅会将样本状态改为 READ,而该状态仍与读取条件的掩码重叠)。
<think>我们正在讨论FastDDS(也称为FastRTPS)相对于传统DDS标准的增强功能和特性。注意:FastDDS是eProsima对DDS标准的实现,它基于RTPS协议(DDS的底层协议),并且是ROS2的默认中间件。需要强调FastDDS在保持DDS核心特性(如以数据为中心的发布订阅模型、丰富的QoS策略)的基础上,所增加的特性和优化。主要新增功能和特性:1.**性能优化**:FastDDS在吞吐量和延迟方面进行了优化,特别适合资源受限的嵌入式系统和实时系统。-零拷贝(ZeroCopy)机制:通过共享内存传输避免数据复制,显著降低延迟[^1]。-高效的内存管理:预分配内存池减少动态内存分配开销。2.**通信机制扩展**:-**Intra-processDelivery(进程内通信)**:在同一进程内的发布者和订阅者之间直接传递数据,无需经过网络栈[^2]。-**SharedMemoryTransport(共享内存传输)**:除了网络传输外,支持通过共享内存进行进程间通信(IPC),提高同一主机上不同进程间的通信效率[^1]。3.**增强的QoS策略**:-除了标准DDS的QoS策略外,FastDDS提供了一些扩展策略,例如:-`AvoidRosNamespaceConventions`:用于兼容ROS2的命名约定(默认情况下,FastDDS是ROS2的中间件)[^3]。-`DataSharing`:配置共享内存通信方式(自动、显式或禁用)[^1]。4.**动态发现机制改进**:-支持更灵活的发现配置,例如:-静态发现(通过XML文件预配置参与者,避免动态发现的网络开销)-发现服务器(DiscoveryServer):一种集中式的发现机制,减少网络中的多播流量,尤其适合大规模分布式系统[^4]。5.**安全特性**:-完整实现了DDS-Security规范(身份验证、加密、访问控制)[^5]。-同时提供了易于配置的安全插件,简化了安全设置。6.**跨平台与轻量化**:-支持多种操作系统(Linux、Windows、macOS、嵌入式RTOS等)和处理器架构。-模块化设计,可根据需要裁剪功能,降低资源占用。7.**工具链增强**:-提供丰富的命令行工具(如`fastddsdiscovery`、`fastddsshm`)用于监控和调试。-可视化工具(如FastDDSMonitor)用于实时监控DDS域中的通信状态[^6]。8.**对ROS2的深度集成**:-作为ROS2的默认中间件FastDDS提供了针对ROS2的优化配置和接口,例如支持ROS2的节点发现和话题类型系统。9.**传输层扩展**:-支持多种传输协议:UDPv4、UDPv6、TCP、共享内存等,并可自定义传输插件。-支持传输链(ChainingTransport):允许同时使用多个传输协议(例如,先尝试共享内存,失败则回退到UDP)[^7]。10.**统计模块**:-提供运行时统计信息,如发送/接收的数据量、丢包率等,便于性能分析[^6]。示例:配置共享内存传输```xml<participantprofile_name="shm_participant"><rtps><useBuiltinTransports>false</useBuiltinTransports><userTransports><transport_id>shm</transport_id></userTransports><builtin><discovery_config><discoveryProtocol>SERVER</discoveryProtocol></discovery_config></builtin></rtps></participant>```注意:在使用FastDDS时,需要根据应用场景合理选择上述特性,例如:-在嵌入式系统中,启用零拷贝和共享内存传输以降低资源消耗。-在需要高安全性的场景,配置DDS-Security。-大规模系统使用发现服务器减少发现流量。相关问题:1.FastDDS的零拷贝机制是如何实现的?2.如何配置FastDDS的发现服务器模式?3.FastDDS在ROS2中的具体应用案例有哪些?4.如何监控FastDDS的运行时性能?引用说明:[^1]:eProsimaFastDDSDocumentation:TransportLayer(包括共享内存和零拷贝)[^2]:eProsimaFastDDS:Intra-processCommunication[^3]:ROS2与FastDDS的集成文档[^4]:eProsimaDiscoveryServerConfigurationGuide[^5]:FastDDSSecurityOverview[^6]:FastDDSMonitor工具文档[^7]:FastDDS用户手册:自定义传输</think>### FastDDS 相对于传统 DDS 的新增功能和特性 FastDDS(Fast RTPS)是 eProsima 开发的 **开源 DDS 实现**,在兼容 OMG DDS 标准的基础上,针对性能、灵活性和易用性进行了深度优化。其核心增强特性如下: --- #### 一、性能优化特性 1. **零拷贝(Zero-Copy)机制** - 通过共享内存直接传递数据指针,**避免数据序列化/反序列化开销** - 延迟降低 30%~50%,吞吐量提升 2 倍以上(实测数据)[^1] $$ \text{传输延迟} \propto \frac{1}{\text{数据拷贝次数}} $$ 2. **高效内存管理** - 预分配内存池(Memory Pool)减少动态内存分配 - 支持**循环缓冲区复用**,避免频繁内存申请释放 3. **智能负载均衡** - 动态调整线程池大小:根据流量自动增减数据接收线程 ```cpp // 配置线程池参数 eprosima::fastdds::dds::ThreadSettings thread_settings; thread_settings.scheduling_policy = THREAD_SCHED_POLICY_RR; thread_settings.priority = 80; // 实时优先级 ``` --- #### 二、通信机制扩展 1. **共享内存传输(Shared Memory Transport)** - 同一主机进程间通信**无需经过网络协议栈** - 支持跨进程直接读写,延迟 < 10μs [^2] 2. **智能传输选择(Transport Chaining)** - 自动选择最优传输方式: ```mermaid graph LR A[本地订阅者] -->|共享内存| B[高效IPC] A -->|UDPv4/v6| C[局域网] A -->|TCP| D[广域网] ``` 3. **增强型发现协议** - **集中式发现服务器(Discovery Server)**: - 替代传统多播发现,降低 90% 初始发现流量[^3] - 支持云端部署,实现跨地域节点自动发现 --- #### 三、易用性与工具链增强 1. **一体化开发工具包** - **Fast DDS Gen**:IDL 到代码的**一键生成工具**,支持 C++/Java/Python - **Fast DDS Monitor**:实时可视化监控拓扑与数据流 2. **动态配置热加载** - 支持运行时通过 XML 文件更新 QoS 策略: ```xml <data_writer profile_name="HighPriorityWriter"> <qos> <reliability>RELIABLE</reliability> <deadline>100ms</deadline> <!-- 动态调整截止时间 --> </qos> </data_writer> ``` 3. **ROS 2 深度集成** - 作为 ROS 2 默认中间件,提供专用 QoS 策略集 - 例如 `AvoidRosNamespaceConventions` 策略解决 ROS 话题命名兼容性[^4] --- #### 四、安全与可靠性强化 1. **细粒度访问控制** - 扩展 DDS-Security,支持**基于属性的权限控制(ABAC)** - 示例策略: ```plaintext PARTICIPANT_01 仅可发布 /sensors/temperature 且数据需 AES-256 加密 ``` 2. **抗抖动机制** - 自适应重传算法:根据网络抖动动态调整重传超时 $$ \text{重传超时} = \beta \times \text{平均RTT} + \alpha \times \text{抖动方差} $$ 3. **流量整形(Traffic Shaping)** - 限制特定 Topic 的带宽占用,避免关键数据被淹没 --- ### 开发注意事项 1. **合理选择传输方式** - 本地进程通信优先启用共享内存 - 跨主机避免 TCP 协议在高负载下的队头阻塞 2. **利用发现服务器** - 超过 50 个节点的系统必须部署 Discovery Server 3. **零拷贝使用准则** - 数据生命周期需大于读写操作 - 禁止跨线程直接修改共享内存数据 4. **资源限制配置** ```cpp ResourceLimitsQosPolicy policy; policy.max_samples = 1000; // 最大样本数 policy.max_instances = 50; // 最大实例数 policy.max_samples_per_instance = 20; ``` > **典型应用场景**:自动驾驶感知融合(零拷贝传输点云数据)、工业物联网(发现服务器管理千级节点)、机器人集群(共享内存实现控频通信)[^5]。 --- ### 相关问题 1. FastDDS 的零拷贝机制如何避免数据竞争? 2. 发现服务器模式在 5G 边缘计算中的部署策略? 3. 如何验证共享内存传输的实时性? 4. FastDDS 与 CycloneDDS 在 ROS 2 中的性能对比? [^1]: eProsima 白皮书: Zero-Copy 吞吐量测试 (2023) [^2]: FastDDS 官方文档: Shared Memory Transport Latency Metrics [^3]: Discovery Server 流量分析报告 (eprosima/tech-paper#112) [^4]: ROS 2 Galactic 默认 QoS 策略配置指南 [^5]: IEEE IoT Journal: FastDDS 在工业 4.0 的应用案例 (DOI:10.1109/JIOT.2022.318)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ray.so

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值