3.5 主题(Topic)

3.5 主题(Topic)

在概念上,主题(Topic)是连接发布端(publications)与订阅端(subscriptions)的中间环节。为确保订阅端只接收自身关注的数据流,而非其他发布端的数据,每个发布通道都必须被订阅端明确识别。主题(Topic)正是为实现这一目的而生 —— 拥有相同主题的发布端与订阅端能够完成匹配并开始通信。从这个角度来说,主题(Topic)相当于对某一数据流的描述。

发布端始终与单个主题(Topic)绑定,而订阅端则与范围更广的 “主题描述(TopicDescription)” 概念相关联。

3.5.1 主题(Topics)、键(keys)与实例(instances)

根据定义,一个主题(Topic)与一种数据类型绑定,因此所有与该主题相关的数据样本,都可理解为对该数据类型所描述信息的更新。但通过 “逻辑划分”,可在同一主题下包含多个指向同一数据类型的实例(Instance)。如此一来,接收的数据样本将成为该主题下某个特定实例的更新。

综上,主题(Topic)标识单一数据类型的数据,其包含的实例数量可从 1 个到该数据类型的完整实例集合不等,具体如图所示(原文提及的图示:主题 “Topic” 下包含 3 个实例 “Instance1~3”,均为 “Foo” 类型且各有唯一键 “key1~3”)。

同一主题下的不同实例,可通过一个或多个数据字段(即构成数据集 “键” 的字段)进行区分。需向中间件指明 “键” 的描述规则,具体逻辑如下:

  • 键值相同的不同数据值,代表同一实例的连续数据样本。
  • 键值不同的不同数据值,代表不同的主题实例。

若未指定键,则该主题关联的数据集仅包含一个实例。有关如何在 eProsima Fast DDS 中设置键的更多信息,请参考 “带键的数据类型(Data types with a key)” 章节。

3.5.1.1 实例的优势(Instance advantages)

使用实例(而非新建DataWriterDataReaderTopic)的优势在于:相关实体已完成创建和发现流程,因此可减少内存占用,且无需执行新的发现操作(避免发现过程中产生的元数据流量,详情见 “发现机制(Discovery)” 章节)。

另一优势是,部分服务质量(QoS)可按主题实例单独应用。例如,HistoryQosPolicy(历史记录策略)会为DataWriter中的每个实例单独保留,使得实例能适配各类应用场景。

3.5.1.2 实例的生命周期(Instance lifecycle)

DataReader中读取或提取数据时(详情见 “访问接收的数据(Accessing received data)” 章节),还会返回SampleInfo(样本信息)实例。该实例包含实例生命周期的附加信息,具体通过view_state(视图状态)、instance_state(实例状态)、disposed_generation_count(销毁代计数)和no_writers_generation_count(无写入器代计数)体现。

下图(原文提及的状态图)展示了单个实例的instance_stateview_state状态流转逻辑:

  1. 接收 “从未见过” 的实例样本 → view_state设为NEW,代计数初始化为 0;
  2. 接收实例样本 → 若此前实例状态为NOT_ALIVE(非活跃),且检测到 “活跃” 写入器,no_writers_generation_count递增;若实例因销毁后重新活跃,disposed_generation_count递增;
  3. 读取 / 提取样本 → view_state转为NOT_NEW(非新视图);
  4. 无 “活跃” 写入器 → 实例状态转为NOT_ALIVE_NO_WRITERS(无写入器);
  5. 写入器销毁实例 → 实例状态转为NOT_ALIVE_DISPOSED(已销毁);
  6. 数据读取器中无样本,且实例状态为ALIVE(活跃)/ 无 “活跃” 写入器 → 触发对应状态变更。

3.5.1.3 实际应用场景(Practical applications)

本节通过两个示例,帮助理解 DDS 实例的使用方式。

3.5.1.3.1 商业航班跟踪(Commercial flights tracking)

空域及空域内的航班通常由空中交通管制员管理,管制员负责调度航班、防止碰撞并提供航班信息。在此场景中,每个空中交通管制中心负责特定飞行区域,并将数据传输至空域交通管理系统,由该系统统一整合航班信息。

每当管制中心发现有航班进入其管控区域,会将该航班的跟踪信息通知给空域交通管理中心。若通过 DDS 实现此信息流,常规方案是创建特定主题(Topic)用于发布航班位置信息 —— 管理中心需创建对应的主题和DataReader(若此前未创建)以获取航班信息,但会产生相应的内存消耗和发现元数据流量。

更优的实现方案是利用 “主题实例”,将管制中心的信息转发至管理中心。可将 “航空公司名称 + 航班号”(如 “IBERIA 1234”)作为主题实例的键(Key),用于标识不同实例;转发的样本数据则为各航班在实时跟踪中的位置信息。以下 IDL 代码定义了该数据模型:

idl

struct FlightPosition
{
    // 唯一标识:航空公司名称
    @key string<256> airline_name;

    // 唯一标识:航班号
    @key short flight_number;

    // 坐标信息
    double latitude;   // 纬度
    double longitude;  // 经度
    double altitude;   // 高度
};

当管制中心发现新航班时,需将对应实例注册到系统中:

cpp

运行

// 创建数据样本
FlightPosition first_flight_position;

// 指定航班实例(设置键值)
first_flight_position.airline_name("IBERIA");
first_flight_position.flight_number(1234);

// 注册实例
eprosima::fastdds::rtps::InstanceHandle_t first_flight_handle =
        data_writer->register_instance(&first_flight_position);

register_instance()会返回一个InstanceHandle_t(实例句柄),可用于高效调用后续针对该实例的操作(如write()dispose()unregister_instance())。返回的InstanceHandle_t包含实例的键哈希值,无需从数据样本中重新计算。采用此方案时,应用程序需自行维护 “实例句柄与实例” 的映射关系。

cpp

运行

// 更新从飞机接收的位置信息
first_flight_position.latitude(39.08);   // 纬度39.08
first_flight_position.longitude(-84.21); // 经度-84.21
first_flight_position.altitude(1500);    // 高度1500

// 向实例写入样本数据
data_writer->write(&first_flight_position, first_flight_handle);

此外,用户应用也可直接调用DataWriter的实例操作,并传入NIL(空)实例句柄。此时,每次对实例执行操作时都会重新计算实例句柄 —— 计算耗时取决于所用数据类型的具体情况。

cpp

运行

// 创建新数据样本
FlightPosition second_flight_position;

// 设置新实例(键值)
second_flight_position.airline_name("RYANAIR");
second_flight_position.flight_number(4321);

// 更新飞机位置
second_flight_position.latitude(40.02);   // 纬度40.02
second_flight_position.longitude(-84.32); // 经度-84.32
second_flight_position.altitude(5000);    // 高度5000

// 不注册实例,直接写入样本
data_writer->write(&second_flight_position);

警告:应用程序中必须正确管理实例句柄。否则,若传入非NIL的实例句柄,中间件不会重新计算句柄(默认用户传入的句柄正确),可能导致 “某实例的样本错误更新另一实例”。例如以下代码中,会用第二个实例的信息更新第一个实例:

cpp

运行

data_writer->write(&second_flight_position, first_flight_handle);

当飞机离开管控区域时,管制中心可注销(unregister)该实例。注销意味着该管制中心的DataWriter不再提供此实例的信息,管理中心中匹配的DataReader会收到通知。此时航班仍在飞行,但已超出该DataWriter的管控范围 —— 实例仍处于活跃状态,但不再被该管制中心跟踪。

cpp

运行

// 注销实例(使用已注册的句柄)
data_writer->unregister_instance(&first_flight_position, first_flight_handle);
// 注销实例(使用NIL句柄,需重新计算)
data_writer->unregister_instance(&second_flight_position, HANDLE_NIL);

最终,当航班降落时,可销毁(dispose)该实例。在本场景中,销毁实例表示:就该DataWriter所知,此实例已不存在,应视为非活跃状态。DataWriter会将此信息通知给匹配的DataReader

cpp

运行

// 销毁实例(使用已注册的句柄)
data_writer->dispose(&first_flight_position, first_flight_handle);
// 销毁实例(使用NIL句柄,需重新计算)
data_writer->dispose(&second_flight_position, HANDLE_NIL);

从管理中心的角度,只需通过订阅该实例发布主题的同一个DataReader,即可读取所有样本。但需检查valid_data(有效数据)字段,确认接收的样本是否包含数据值 —— 若不包含,则表示实例状态发生变更。实例生命周期的状态流转详情可参考前文提及的状态图。

cpp

运行

if (RETCODE_OK == data_reader->take_next_sample(&data, &info))
{
    if (info.valid_data)
    {
        // 已接收数据样本
    }
    else if (info.instance_state == NOT_ALIVE_DISPOSED_INSTANCE_STATE)
    {
        // 远程DataWriter已销毁该实例
    }
    else if (info.instance_state == NOT_ALIVE_NO_WRITERS_INSTANCE_STATE)
    {
        // 所有匹配的DataWriter均未向该实例写入数据
        // 可安全销毁该实例
    }
}

3.5.1.3.2 关系型数据库(Relational databases)

假设空域交通管理中心需维护一个跟踪航班的数据库,使用 DDS 实例可直接适配关系型数据库的设计逻辑:实例的键(实例的唯一标识)类似于数据库表的主键(Primary Key)。因此,管理中心可在表中为每个实例保留最新更新,示例表如下:

实例句柄 [主键](Instance handle [PK])数据(Data)
1Position1
2Position2
3Position3
4Position4
5Position5

在此场景中,每当接收新样本,数据库中对应实例的条目会更新为最新已知位置;销毁实例可对应删除数据库中的相关数据。注册和注销实例的操作不会直接反映在数据库中,但如果同时持久化instance_stateview_state,也可跟踪实例的生命周期。

DataWriter通知 “将发布某实例数据” 这一行为,对数据库无实际意义 —— 只有当接收新数据时,数据库才会为新发现的实例执行插入操作。

关系型数据库也可存储历史数据,但根据具体场景,可能需要使用时序数据库以提升效率。在本航班跟踪场景中,除实例句柄外,可将样本时间戳(source timestamp)也设为主键,从而能查询特定航班的历史跟踪数据。示例表如下:

实例句柄 [主键](Instance handle [PK])源时间戳 [主键](Source Timestamp [PK])数据(Data)
11Position1
21Position2
12Position3
13Position4
22Position5

基于此表,查询特定实例句柄可获取该航班的跟踪记录:

实例句柄 [固定](Instance handle [Fixed])源时间戳(Source Timestamp)数据(Data)
11Position1
12Position3
13Position4

查询特定时间戳可获取该时刻所有航班的位置快照:

实例句柄(Instance handle)源时间戳 [固定](Source Timestamp [Fixed])数据(Data)
12Position3
22Position5

3.5.2 主题描述(TopicDescription)

TopicDescription(主题描述)是一个抽象类,用作所有描述数据流的类的基类。应用程序不会直接创建TopicDescription的实例,而必须创建其某个特化类的实例。目前已实现的特化类仅有Topic(普通主题)和ContentFilteredTopic(内容过滤主题)两种。

3.5.3 主题(Topic)

3.5.3 主题(Topic)

“主题(Topic)” 是 “主题描述(TopicDescription)” 这一更广概念的特化形式。它代表发布者(Publisher)与订阅者(Subscriber)之间的单一数据流,包含以下核心信息:

  • 用于标识数据流的名称
  • 在该数据流中传输的数据类型
  • 与数据本身相关的服务质量(QoS)参数

主题的行为可通过 “主题服务质量(TopicQos)” 中指定的 QoS 参数进行修改。这些 QoS 参数可在创建主题时设置,也可后续通过Topic::set_qos()成员函数修改。

与其他实体(Entities)类似,主题可关联一个 “监听器(Listener)”。当主题的状态发生变化时,监听器会收到通知。

有关如何创建 “主题(Topic)” 的更多信息,请参考 “创建主题(Creating a Topic)” 章节。

3.5.3.1 主题服务质量(TopicQos)

“主题服务质量(TopicQos)” 用于控制主题的行为。其内部包含以下 “服务质量策略(QosPolicy)” 对象:

服务质量策略类(QosPolicy class)访问器(Accessor)是否可修改(Mutable)
TopicDataQosPolicy(主题数据服务质量策略)topic_data()
DurabilityQosPolicy(持久性服务质量策略)durability()
DurabilityServiceQosPolicy(持久化服务质量策略)durability_service()
DeadlineQosPolicy(截止期限服务质量策略)deadline()
LatencyBudgetQosPolicy(延迟预算服务质量策略)latency_budget()
LivelinessQosPolicy(活跃度服务质量策略)liveliness()
ReliabilityQosPolicy(可靠性服务质量策略)reliability()
DestinationOrderQosPolicy(目标顺序服务质量策略)destination_order()
HistoryQosPolicy(历史记录服务质量策略)history()
ResourceLimitsQosPolicy(资源限制服务质量策略)resource_limits()
TransportPriorityQosPolicy(传输优先级服务质量策略)transport_priority()
LifespanQosPolicy(生存周期服务质量策略)lifespan()
OwnershipQosPolicy(所有权服务质量策略)ownership()
DataRepresentationQosPolicy(数据表示服务质量策略)representation()

有关各 “服务质量策略(QosPolicy-api)” 类的用法及默认值,请参考其详细说明。

已创建主题的 QoS 参数可通过Topic::set_qos()成员函数修改。

cpp

运行

// 在指定域中创建一个域参与者(DomainParticipant)
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 使用默认的TopicQos创建主题
Topic* topic =
        participant->create_topic("TopicName", "DataTypeName", TOPIC_QOS_DEFAULT);
if (nullptr == topic)
{
    // 错误处理
    return;
}

// 获取当前QoS参数,或从头创建新的QoS参数
TopicQos qos = topic->get_qos();

// 修改QoS属性
// (...)

// 为主题对象分配新的QoS参数
topic->set_qos(qos);

3.5.3.1.1 默认主题服务质量(Default TopicQos)

“默认主题服务质量(Default TopicQos)” 指的是通过 “域参与者(DomainParticipant)” 实例的get_default_topic_qos()成员函数返回的值。

特殊值TOPIC_QOS_DEFAULT可作为create_topic()Topic::set_qos()成员函数的 QoS 参数,用于表示应使用当前的 “默认主题服务质量(Default TopicQos)”。

系统启动时,“默认主题服务质量(Default TopicQos)” 与默认构造值TopicQos()等效。可通过 “域参与者(DomainParticipant)” 实例的get_default_topic_qos()成员函数随时修改 “默认主题服务质量(Default TopicQos)”。修改 “默认主题服务质量(Default TopicQos)” 不会影响已创建的 “主题(Topic)” 实例。

cpp

运行

// 在指定域中创建一个域参与者(DomainParticipant)
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 获取当前QoS参数,或从头创建新的QoS参数
TopicQos qos_type1 = participant->get_default_topic_qos();

// 修改QoS属性
// (...)

// 将其设置为新的“默认主题服务质量(Default TopicQos)”
if (participant->set_default_topic_qos(qos_type1) != RETCODE_OK)
{
    // 错误处理
    return;
}

// 使用新的“默认主题服务质量(Default TopicQos)”创建主题
Topic* topic_with_qos_type1 =
        participant->create_topic("TopicName", "DataTypeName", TOPIC_QOS_DEFAULT);
if (nullptr == topic_with_qos_type1)
{
    // 错误处理
    return;
}

// 获取当前QoS参数,或从头创建新的QoS参数
TopicQos qos_type2;

// 修改QoS属性
// (...)

// 将其设置为新的“默认主题服务质量(Default TopicQos)”
if (participant->set_default_topic_qos(qos_type2) != RETCODE_OK)
{
    // 错误处理
    return;
}

// 使用新的“默认主题服务质量(Default TopicQos)”创建主题
Topic* topic_with_qos_type2 =
        participant->create_topic("TopicName", "DataTypeName", TOPIC_QOS_DEFAULT);
if (nullptr == topic_with_qos_type2)
{
    // 错误处理
    return;
}

// 将“默认主题服务质量(Default TopicQos)”重置为原始的默认构造值
if (participant->set_default_topic_qos(TOPIC_QOS_DEFAULT)
        != RETCODE_OK)
{
    // 错误处理
    return;
}

// 上述指令与以下指令等效
if (participant->set_default_topic_qos(TopicQos())
        != RETCODE_OK)
{
    // 错误处理
    return;
}

get_default_topic_qos()成员函数也接受TOPIC_QOS_DEFAULT作为输入参数。这会将当前的 “默认主题服务质量(Default TopicQos)” 重置为默认构造值TopicQos()

cpp

运行

// 在指定域中创建一个域参与者(DomainParticipant)
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 创建自定义的TopicQos
TopicQos custom_qos;

// 修改QoS属性
// (...)

// 使用自定义的TopicQos创建主题
Topic* topic = participant->create_topic("TopicName", "DataTypeName", custom_qos);
if (nullptr == topic)
{
    // 错误处理
    return;
}

// 将主题的QoS参数设置为默认值
if (topic->set_qos(TOPIC_QOS_DEFAULT) != RETCODE_OK)
{
    // 错误处理
    return;
}

// 上述指令与以下指令等效:
if (topic->set_qos(participant->get_default_topic_qos())
        != RETCODE_OK)
{
    // 错误处理
    return;
}

注意TOPIC_QOS_DEFAULT的值在不同使用场景下含义不同:

  • create_topic()Topic::set_qos()中,它指代get_default_topic_qos()返回的 “默认主题服务质量(Default TopicQos)”。
  • get_default_topic_qos()中,它指代默认构造的TopicQos()

3.5.4 内容过滤主题(ContentFilteredTopic)

“内容过滤主题(ContentFilteredTopic)” 是 “主题描述(TopicDescription)” 这一更广概念的特化形式。它是一种带有过滤属性的主题,能够让用户在订阅某个主题的同时,仅指定对该主题数据子集的兴趣。

重要提示:内容过滤主题(ContentFilteredTopic)仅可用于创建数据读取器(DataReader),不可用于创建数据写入器(DataWriter)。

内容过滤主题(ContentFilteredTopic)会在一个 “关联主题(related topic)” 和若干用户自定义过滤属性之间建立关联,这些过滤属性包括:

  • 过滤表达式(filter expression):对关联主题的内容建立逻辑表达式,类似 SQL 语句中的 WHERE 子句。
  • 表达式参数列表(list of expression parameters):为过滤表达式中的参数提供具体值。过滤表达式中的每个参数都必须对应一个参数字符串。

注意:内容过滤主题(ContentFilteredTopic)不属于 “实体(Entity)” 范畴,因此它既没有服务质量(QoS)配置,也没有监听器(Listener)。通过内容过滤主题(ContentFilteredTopic)创建的数据读取器(DataReader),会沿用其关联主题(related topic)的服务质量(QoS)配置。多个数据读取器(DataReader)可基于同一个内容过滤主题(ContentFilteredTopic)创建,且修改该内容过滤主题的过滤属性时,会对所有使用它的数据读取器产生影响。

有关如何使用 “内容过滤主题(ContentFilteredTopic)” 的更多信息,请参考 “主题数据过滤(Filtering data on a Topic)” 和 “过滤在何处应用:写入端 vs 读取端(Where is filtering applied: writer vs reader side)” 章节。

3.5.5 主题监听器(TopicListener)

3.5.5 主题监听器(TopicListener)

TopicListener是一个抽象类,用于定义主题(Topic)状态发生变化时触发的回调函数。默认情况下,所有这些回调函数均为空实现,不执行任何操作。用户需实现该类的特化版本,重写应用程序所需的回调函数;未重写的回调函数将保持空实现。

TopicListener包含以下回调函数:

  • on_inconsistent_topic():当发现一个远程主题(Topic)与本地已创建的另一个主题名称相同,但特性不同时,会触发此回调。

警告:目前on_inconsistent_topic()尚未实现(该函数永远不会被调用),将在 Fast DDS 未来的版本中完成实现。

cpp

运行

class CustomTopicListener : public TopicListener
{

public:

    CustomTopicListener()
        : TopicListener()
    {
    }

    virtual ~CustomTopicListener()
    {
    }

    void on_inconsistent_topic(
            Topic* topic,
            InconsistentTopicStatus status) override
    {
        static_cast<void>(topic);
        static_cast<void>(status);
        std::cout << "Inconsistent topic received discovered" << std::endl;
    }

};

3.5.6 数据类型定义

3.5.6 数据类型定义

主题(Topic)中交换的数据类型定义分为两个类:TypeSupport(类型支持)和TopicDataType(主题数据类型)。

TopicDataType用于描述发布端与订阅端之间交换的数据类型,即对应某个主题(Topic)的数据。用户需为应用程序中使用的每种特定数据类型,创建一个TopicDataType的特化类。

在使用TopicDataType的特化类创建主题(Topic)对象前,必须先在域参与者(DomainParticipant)中注册该特化类。TypeSupport对象会封装一个TopicDataType实例,提供类型注册以及与发布、订阅交互所需的函数。

注册数据类型的步骤如下:

  1. TopicDataType实例创建一个新的TypeSupport对象;
  2. 调用TypeSupportregister_type()成员函数完成注册;
  3. 之后即可使用已注册的类型名称创建主题(Topic)。

注意

  • 不允许在同一个域参与者(DomainParticipant)中,将两种不同的数据类型注册为相同的名称,否则会报错。
  • 允许在同一个域参与者中,将同一种数据类型以相同或不同名称多次注册。
  • 若同一种数据类型在同一个域参与者中以相同名称重复注册,第二次注册不会产生任何效果,但也不会报错。

cpp

运行

// 在指定域中创建域参与者(DomainParticipant)
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 在域参与者中注册数据类型
// 若名称参数传nullptr,则使用数据类型自身返回的名称
TypeSupport custom_type_support(new CustomDataType());
custom_type_support.register_type(participant, nullptr);

// 上述指令与以下指令等效
// 即使将同一种数据类型以相同名称重复注册,也不会报错
custom_type_support.register_type(participant, custom_type_support.get_type_name());

// 使用已注册的类型创建主题(Topic)
Topic* topic =
        participant->create_topic("topic_name", custom_type_support.get_type_name(), TOPIC_QOS_DEFAULT);
if (nullptr == topic)
{
    // 错误处理
    return;
}

// 为同一种数据类型注册一个不同的别名
custom_type_support.register_type(participant, "data_type_name");

// 现在可使用该别名创建主题(若未指定名称,默认使用数据类型自身返回的名称)
Topic* another_topic =
        participant->create_topic("other_topic_name", "data_type_name", TOPIC_QOS_DEFAULT);
if (nullptr == another_topic)
{
    // 错误处理
    return;
}

3.5.6.1 动态数据类型

无需直接编写TopicDataType的特化类,可遵循 OMG(对象管理组织)的 “DDS 可扩展动态主题类型(Extensible and Dynamic Topic Types for DDS)” 接口动态定义数据类型。也可通过动态加载 XML 文件来描述数据类型。

cpp

运行

// 在指定域中创建域参与者(DomainParticipant)
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 加载包含类型描述的XML文件
DomainParticipantFactory::get_instance()->load_XML_profiles_file("example_type.xml");

// 获取所需数据类型的实例
DynamicTypeBuilder::_ref_type dyn_type_builder;
DomainParticipantFactory::get_instance()->get_dynamic_type_builder_from_xml_by_name("DynamicType",
        dyn_type_builder);

// 注册动态数据类型
TypeSupport dyn_type_support(new DynamicPubSubType(dyn_type_builder->build()));
dyn_type_support.register_type(participant, nullptr);

// 使用已注册的动态类型创建主题(Topic)
Topic* topic =
        participant->create_topic("topic_name", dyn_type_support.get_type_name(), TOPIC_QOS_DEFAULT);
if (nullptr == topic)
{
    // 错误处理
    return;
}

有关动态数据类型定义的完整说明,可参考 “XTypes” 章节。

3.5.6.2 带键的数据类型

若数据类型定义了一组字段以构成唯一键(key),则可在同一种数据类型中区分不同的数据集。

要定义带键的主题(keyed Topic),需完成以下操作:

  1. 重写TopicDataTypegetKey()成员函数,使其能根据数据字段返回对应的键值;
  2. m_isGetKeyDefined数据成员设为true,告知相关实体这是一个带键主题,且应使用getKey()函数获取键值。

未定义键的数据类型,其m_isGetKeyDefined成员会默认设为false

TopicDataType中实现键(key)有三种方式:

  1. 使用 Fast DDS-Gen 工具时,在 IDL 文件中为构成键的成员添加@Key注解;
  2. 使用 XTypes 时,为成员及其父级添加Key属性;
  3. 手动重写TopicDataTypegetKey()成员函数,并将m_isGetKeyDefined数据成员设为true

带键的数据类型用于在单个主题(Topic)中定义数据子流:

  • 同一主题中,具有相同键值的数据代表来自同一子流;
  • 同一主题中,具有不同键值的数据代表来自不同子流。

中间件会将这些子流分开管理,但所有子流都受限于该主题(Topic)的同一组 QoS 参数。若未定义键,则该主题关联的数据集仅局限于单个流。

3.5.7 创建主题(Topic)

3.5.7 创建主题(Topic)

主题(Topic)始终隶属于某个域参与者(DomainParticipant)。创建主题需调用域参与者实例的create_topic()成员函数,该函数相当于主题的 “工厂方法”。

必选参数

  1. 用于标识主题的字符串名称;
  2. 已注册的、将在主题中传输的数据类型名称;
  3. 描述主题行为的TopicQos(主题服务质量)。若传入值为TOPIC_QOS_DEFAULT,则使用 “默认主题服务质量(Default TopicQos)”。

可选参数

  1. 继承自TopicListener的监听器(Listener):实现主题发生事件或状态变化时触发的回调函数,默认使用空回调;
  2. StatusMask(状态掩码):激活或禁用TopicListener中单个回调的触发,默认启用所有事件。

若操作过程中出现错误(例如传入的 QoS 不兼容或不被支持),create_topic()将返回空指针。建议检查返回值是否为有效指针。

cpp

运行

// 在指定域中创建域参与者(DomainParticipant)
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 使用默认TopicQos且不设置监听器,创建主题
// 符号TOPIC_QOS_DEFAULT表示使用默认QoS
Topic* topic_with_default_qos =
        participant->create_topic("TopicName", "DataTypeName", TOPIC_QOS_DEFAULT);
if (nullptr == topic_with_default_qos)
{
    // 错误处理
    return;
}

// 可在创建方法中传入自定义的TopicQos
TopicQos custom_qos;

// 修改QoS属性
// (...)

Topic* topic_with_custom_qos =
        participant->create_topic("TopicName", "DataTypeName", custom_qos);
if (nullptr == topic_with_custom_qos)
{
    // 错误处理
    return;
}

// 使用默认QoS和自定义监听器创建主题
// CustomTopicListener继承自TopicListener
// 符号TOPIC_QOS_DEFAULT表示使用默认QoS
CustomTopicListener custom_listener;
Topic* topic_with_default_qos_and_custom_listener =
        participant->create_topic("TopicName", "DataTypeName", TOPIC_QOS_DEFAULT, &custom_listener);
if (nullptr == topic_with_default_qos_and_custom_listener)
{
    // 错误处理
    return;
}

3.5.7.1 基于配置文件创建主题

无需使用TopicQos,可通过 “配置文件名称” 调用域参与者实例的create_topic_with_profile()成员函数创建主题。

必选参数

  1. 用于标识主题的字符串名称;
  2. 已注册的、将在主题中传输的数据类型名称;
  3. 要应用到主题的配置文件名称。

可选参数

  1. 继承自TopicListener的监听器(Listener):实现主题发生事件或状态变化时触发的回调函数,默认使用空回调;
  2. StatusMask(状态掩码):激活或禁用TopicListener中单个回调的触发,默认启用所有事件。

若操作过程中出现错误(例如传入的 QoS 不兼容或不被支持),create_topic_with_profile()将返回空指针。建议检查返回值是否为有效指针。

注意:必须先加载 XML 配置文件。具体可参考 “从 XML 文件加载配置文件(Loading profiles from an XML file)” 章节。

cpp

运行

// 首先加载包含配置文件的XML
DomainParticipantFactory::get_instance()->load_XML_profiles_file("profiles.xml");

// 在指定域中创建域参与者(DomainParticipant)
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 使用配置文件且不设置监听器,创建主题
Topic* topic_with_profile =
        participant->create_topic_with_profile("TopicName", "DataTypeName", "topic_profile");
if (nullptr == topic_with_profile)
{
    // 错误处理
    return;
}

// 使用配置文件和自定义监听器创建主题
// CustomTopicListener继承自TopicListener
CustomTopicListener custom_listener;
Topic* topic_with_profile_and_custom_listener =
        participant->create_topic_with_profile("TopicName", "DataTypeName", "topic_profile", &custom_listener);
if (nullptr == topic_with_profile_and_custom_listener)
{
    // 错误处理
    return;
}

3.5.7.2 删除主题

删除主题需调用 “创建该主题的域参与者” 实例的delete_topic()成员函数。

cpp

运行

// 在指定域中创建域参与者(DomainParticipant)
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 创建主题
Topic* topic =
        participant->create_topic("TopicName", "DataTypeName", TOPIC_QOS_DEFAULT);
if (nullptr == topic)
{
    // 错误处理
    return;
}

// 使用主题进行通信
// (...)

// 删除主题
if (participant->delete_topic(topic) != RETCODE_OK)
{
    // 错误处理
    return;
}

3.5.8 主题数据过滤(Filtering data on a Topic)

3.5.8.1 创建内容过滤主题(ContentFilteredTopic)

内容过滤主题(ContentFilteredTopic)始终隶属于某个域参与者(DomainParticipant)。创建内容过滤主题需调用域参与者实例的create_contentfilteredtopic()成员函数,该函数相当于内容过滤主题的 “工厂方法”。

必选参数

  1. 用于标识内容过滤主题的字符串名称;
  2. 待过滤的关联主题(related Topic);
  3. 过滤表达式字符串:用于指定数据样本需满足的返回条件;
  4. 表达式参数字符串列表:为过滤表达式中的参数提供具体值。

注意:参数值的数量不得超过expression_parameters(表达式参数)QoS 配置所设定的最大值。根据 OMG DDS 标准,默认(且绝对)最大值为 100。

可选参数

用于创建过滤器的 “过滤器类名称” 字符串。该参数支持用户创建非标准 SQL 风格的过滤器(具体可参考 “使用自定义过滤器(Using custom filters)” 章节),默认值为FASTDDS_SQLFILTER_NAME(即DDSSQL)。

重要提示:若将过滤表达式设为空字符串,将禁用过滤功能。只需更新过滤表达式,即可在任意时刻启用 / 禁用数据读取器(DataReader)的过滤能力。

若操作过程中出现错误(例如关联主题隶属于其他域参与者、同名主题已存在、过滤表达式语法错误、参数值缺失等),create_contentfilteredtopic()将返回空指针。建议检查返回值是否为有效指针。

注意:不同的过滤器类可能对关联主题、表达式或参数有不同要求。特别是默认过滤器类,要求已为关联主题的数据类型注册 TypeObject(类型对象)。使用 fastddsgen 工具时,会默认生成 TypeObject 的注册代码。

cpp

运行

// 在指定域中创建域参与者(DomainParticipant)
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 创建主题(Topic)
/* IDL(接口定义语言)代码
 *
 * struct HelloWorld
 * {
 *     long index;  // 整数类型的索引
 *     string message;  // 字符串类型的消息
 * }
 *
 */
Topic* topic =
        participant->create_topic("HelloWorldTopic", "HelloWorld", TOPIC_QOS_DEFAULT);
if (nullptr == topic)
{
    // 错误处理
    return;
}

// 使用无参数表达式创建内容过滤主题
std::string expression = "message like 'Hello*'";  // 过滤条件:消息以“Hello”开头
std::vector<std::string> parameters;  // 空参数列表
ContentFilteredTopic* filter_topic =
        participant->create_contentfilteredtopic("HelloWorldFilteredTopic1", topic, expression, parameters);
if (nullptr == filter_topic)
{
    // 错误处理
    return;
}

// 使用带参数表达式创建内容过滤主题
expression = "message like %0 or index > %1";  // 过滤条件:消息匹配%0,或索引大于%1
parameters.push_back("'*world*'");  // 第一个参数:消息包含“world”(通配符匹配)
parameters.push_back("20");  // 第二个参数:索引阈值20
ContentFilteredTopic* filter_topic_with_parameters =
        participant->create_contentfilteredtopic("HelloWorldFilteredTopic2", topic, expression, parameters);
if (nullptr == filter_topic_with_parameters)
{
    // 错误处理
    return;
}

// 内容过滤主题实例可用于创建数据读取器(DataReader)对象
Subscriber* subscriber =
        participant->create_subscriber(SUBSCRIBER_QOS_DEFAULT);  // 创建订阅者
if (nullptr == subscriber)
{
    // 错误处理
    return;
}

// 基于无参数过滤主题创建数据读取器
DataReader* reader_on_filter = subscriber->create_datareader(filter_topic, DATAREADER_QOS_DEFAULT);
if (nullptr == reader_on_filter)
{
    // 错误处理
    return;
}

// 基于带参数过滤主题创建数据读取器
DataReader* reader_on_filter_with_parameters =
        subscriber->create_datareader(filter_topic_with_parameters, DATAREADER_QOS_DEFAULT);
if (nullptr == reader_on_filter_with_parameters)
{
    // 错误处理
    return;
}

3.5.8.2 更新过滤表达式与参数

内容过滤主题提供多个成员函数,用于管理过滤表达式和表达式参数:

  1. 调用get_filter_expression()成员函数,可获取当前过滤表达式;
  2. 调用get_expression_parameters()成员函数,可获取当前表达式参数;
  3. 调用set_expression_parameters()成员函数,可修改表达式参数,需遵循与创建内容过滤主题时相同的约束;
  4. 调用set_filter_expression()成员函数,可同时修改过滤表达式和表达式参数。

cpp

运行

// lambda表达式:打印内容过滤主题的所有信息
auto print_filter_info = [](
    const ContentFilteredTopic* filter_topic)
        {
            std::cout << "ContentFilteredTopic info for '" << filter_topic->get_name() << "':" << std::endl;
            std::cout << "  - Related Topic: " << filter_topic->get_related_topic()->get_name() << std::endl;  // 关联主题名称
            std::cout << "  - Expression:    " << filter_topic->get_filter_expression() << std::endl;  // 过滤表达式
            std::cout << "  - Parameters:" << std::endl;  // 参数列表

            std::vector<std::string> parameters;
            filter_topic->get_expression_parameters(parameters);  // 获取参数
            size_t i = 0;
            for (const std::string& parameter : parameters)
            {
                std::cout << "    " << i++ << ": " << parameter << std::endl;  // 打印每个参数的索引和值
            }
        };

// 在指定域中创建域参与者(DomainParticipant)
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 创建主题(Topic)
/* IDL(接口定义语言)代码
 *
 * struct HelloWorld
 * {
 *     long index;  // 整数类型的索引
 *     string message;  // 字符串类型的消息
 * }
 *
 */
Topic* topic =
        participant->create_topic("HelloWorldTopic", "HelloWorldTopic", TOPIC_QOS_DEFAULT);
if (nullptr == topic)
{
    // 错误处理
    return;
}

// 创建内容过滤主题
ContentFilteredTopic* filter_topic =
        participant->create_contentfilteredtopic("HelloWorldFilteredTopic", topic, "index > 10", {});  // 过滤条件:索引大于10,无参数
if (nullptr == filter_topic)
{
    // 错误处理
    return;
}

// 打印初始信息
print_filter_info(filter_topic);

// 在数据读取器(DataReader)对象上使用内容过滤主题
// (...)

// 更新过滤表达式与参数
if (RETCODE_OK !=
        filter_topic->set_filter_expression("message like %0 or index > %1", {"'Hello*'", "15"}))  // 新条件:消息以“Hello”开头,或索引大于15
{
    // 错误处理
    return;
}

// 打印更新后的信息
print_filter_info(filter_topic);

// 仅更新参数
if (RETCODE_OK !=
        filter_topic->set_expression_parameters({"'*world*'", "222"}))  // 新参数:消息包含“world”,索引阈值222
{
    // 错误处理
    return;
}

// 打印再次更新后的信息
print_filter_info(filter_topic);

3.5.8.3 删除内容过滤主题

删除内容过滤主题需调用 “创建该内容过滤主题的域参与者” 实例的delete_contentfilteredtopic()成员函数。

cpp

运行

// 在指定域中创建域参与者(DomainParticipant)
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 创建主题(Topic)
/* IDL(接口定义语言)代码
 *
 * struct HelloWorld
 * {
 *     long index;  // 整数类型的索引
 *     string message;  // 字符串类型的消息
 * }
 *
 */
Topic* topic =
        participant->create_topic("HelloWorldTopic", "HelloWorldTopic", TOPIC_QOS_DEFAULT);
if (nullptr == topic)
{
    // 错误处理
    return;
}

// 创建内容过滤主题
ContentFilteredTopic* filter_topic =
        participant->create_contentfilteredtopic("HelloWorldFilteredTopic", topic, "index > 10", {});  // 过滤条件:索引大于10,无参数
if (nullptr == filter_topic)
{
    // 错误处理
    return;
}

// 在数据读取器(DataReader)对象上使用内容过滤主题
// (...)

// 删除内容过滤主题
if (RETCODE_OK != participant->delete_contentfilteredtopic(filter_topic))
{
    // 错误处理
    return;
}

3.5.9 默认类 SQL 过滤器

ContentFilteredTopic API 所使用的过滤器表达式,可采用 SQL 语法的子集,并支持在 SQL 表达式中使用程序变量。本节将介绍这种默认类 SQL 语法及其使用方法。

内容概览:

  • 语法(Grammar)
  • 模糊匹配条件(Like condition)
  • 正则匹配条件(Match condition)
  • 类型比较(Type comparisons)
  • 示例(Example)

3.5.9.1 语法

允许使用的 SQL 表达式通过以下巴科斯范式(BNF)语法定义。

约定说明

  • “终结符”(Terminals)用英文引号标注。
  • <tokens> 以代码块格式呈现,字体颜色为黑色。

plaintext

Expression          ::=  FilterExpression  // 表达式定义为过滤器表达式
FilterExpression    ::=  Condition  // 过滤器表达式定义为条件
Condition           ::=  Predicate |  // 条件可分为断言、多条件组合、非条件、括号包裹的条件
                         Condition "AND" Condition |  // 条件1 与 条件2
                         Condition "OR" Condition |  // 条件1 或 条件2
                         "NOT" Condition |  // 非条件
                         "(" Condition ")"  // (条件)
Predicate           ::=  ComparisonPredicate |  // 断言可分为比较断言、区间断言
                         BetweenPredicate
ComparisonPredicate ::=  FIELDNAME RelOp Parameter |  // 字段名 关系运算符 参数
                         Parameter RelOp FIELDNAME |  // 参数 关系运算符 字段名
                         FIELDNAME RelOp FIELDNAME  // 字段名 关系运算符 字段名
BetweenPredicate    ::=  FIELDNAME "BETWEEN" Range |  // 字段名 在 区间内
                         FIELDNAME "NOT BETWEEN" Range  // 字段名 不在 区间内
RelOp               ::=  "=" | ">" | ">=" | "<" | "<=" |  // 关系运算符包括等于、大于、大于等于、小于、小于等于、
                         "<>" | "!=" | <like> | <match>  // 不等于、不等于、模糊匹配、正则匹配
Range               ::=  Parameter "AND" Parameter  // 区间定义为 参数1 到 参数2
Parameter           ::=  BOOLEANVALUE |  // 参数可分为布尔值、整数值、字符值、浮点值、字符串值、枚举值、参数引用
                         INTEGERVALUE |
                         CHARVALUE |
                         FLOATVALUE |
                         STRINGVALUE |
                         ENUMERATEDVALUE |
                         PARAMETER

大小写规则

“终结符”(Terminals)和 <tokens> 区分大小写,但同时支持大写和小写形式。

语法元素说明

1. 字段名(FIELDNAME)

用于引用数据结构中的字段,通过点号 . 访问嵌套结构,点号使用次数无限制,可引用数据结构中任意深度的字段。字段名需与对应结构的 IDL 定义中指定的名称一致。

plaintext

FIELDNAME     ::=  FieldNamePart ( "." FieldNamePart )*  // 字段名由字段部分和嵌套字段部分组成
FieldNamePart ::=  Identifier ( "[" Integer "]" )?  // 字段部分可包含标识符和数组索引(可选)

字段名示例:过滤器表达式:"points[0] = 0 AND color.red < 100"

对应的 IDL 结构定义:

idl

struct Color  // 颜色结构
{
    octet red;  // 红色通道(8位无符号整数)
    octet green;  // 绿色通道
    octet blue;  // 蓝色通道
};

struct Shape  // 形状结构
{
    long points[4];  // 包含4个长整型元素的数组(点坐标)
    Color color;  // 嵌套的颜色结构
};
2. 布尔值(BOOLEANVALUE)

取值为 true 或 false,区分大小写。

plaintext

BOOLEANVALUE ::=  ["TRUE", "true", "FALSE", "false"]  // 允许的布尔值形式
3. 整数值(INTEGERVALUE)

由一串数字组成,可选择性地在前面添加正号或负号,代表系统范围内的十进制整数值。十六进制数需以 0x 开头,且必须是有效的十六进制表达式。

plaintext

INTEGERVALUE ::=  (["+","-"])? Integer  // 整数可带正负号(可选)
Integer      ::=  (["0"-"9"])+ | ["0x","0X"](["0"-"9", "A"-"F", "a"-"f"])+  // 十进制整数或十六进制整数

整数值示例value = -10

4. 字符值(CHARVALUE)

由单引号包裹的单个字符。

plaintext

CHARVALUE ::=  "'" Character "'"  // 字符值格式为 '字符'
Character ::=  ~["\n"]  // 字符不能是换行符

字符值示例value = 'c'

5. 浮点值(FLOATVALUE)

由一串数字组成,可选择性地在前面添加正号或负号,还可选择性包含小数点 .。支持科学计数法,格式为 e:n(或 E:n),其中 n 为可带正负号的数字。

plaintext

FLOATVALUE ::=  (["+"], "-"])? (Integer Exponent | Integer Fractional | Integer Fractional Exponent)  // 浮点值格式
Fractional ::=  "." Integer  // 小数部分(. + 整数)
Exponent   ::=  ["e","E"] (["+"], "-"])? Integer  // 指数部分(e/E + 可选正负号 + 整数)

浮点值示例value = 10.1e-10

6. 字符串值(STRINGVALUE)

由单引号包裹的一串字符,不包含换行符或右单引号。字符串以左单引号或右单引号开头,以右单引号结尾。

plaintext

STRINGVALUE ::=  ["'"] ~["'", "\r", "\n"] ["'"]  // 字符串格式为 '字符序列',排除单引号、回车符、换行符

字符串值示例value = 'This is a string'

7. 枚举值(ENUMERATEDVALUE)

引用枚举中声明的值,格式为单引号包裹的枚举标签名称。枚举标签名称需与枚举的 IDL 定义中指定的标签名称一致。

plaintext

ENUMERATEDVALUE ::=  ["'"] ~["'", "\r", "\n"] ["'"]  // 枚举值格式为 '枚举标签'

枚举值示例:过滤器表达式:value = 'ENUM_VALUE_1'

对应的 IDL 定义:

idl

enum MyEnum  // 枚举类型
{
    ENUM_VALUE_1,  // 枚举标签1
    ENUM_VALUE_2,  // 枚举标签2
    ENUM_VALUE_3   // 枚举标签3
};

struct Enumerators  // 包含枚举的结构
{
    MyEnum value;  // 枚举类型字段
};
8. 参数引用(PARAMETER)

格式为 %n,其中 n 代表小于 100 的自然数(包含 0),引用对应上下文下的第 n+1 个参数。

plaintext

PARAMETER ::=  ["%"] ["0"-"9"] (["0"-"9"])?  // 参数引用格式为 %+1位或2位数字(0-99)

参数引用示例value = %1(引用第 2 个参数)


3.5.9.2 模糊匹配条件(Like condition)

like 运算符与 SQL 中定义的模糊匹配运算符功能类似,仅可用于字符串类型。使用时可搭配以下两种通配符:

  • 百分号 %(别名 *):代表 0 个、1 个或多个字符。
  • 下划线 _(别名 ?):代表单个字符。

所有通配符可组合使用。

like 运算符示例:过滤器表达式:"str like '%bird%'"

对应的 IDL 结构定义:

idl

struct Like  // 包含字符串的结构
{
    string str;  // 字符串字段
};

当字符串值为 There are birds flying 时,该过滤器表达式返回 true(因字符串中包含 "bird" 子串)。


3.5.9.3 正则匹配条件(Match condition)

match 运算符使用正则表达式执行全文搜索,仅可用于字符串类型,采用 POSIX 标准定义的基本正则表达式(BRE)。

match 运算符示例:过滤器表达式:"str match '^The'"

对应的 IDL 结构定义:

idl

struct Like  // 包含字符串的结构
{
    string str;  // 字符串字段
};

当字符串值为 There are birds flying 时,该过滤器表达式返回 true(因字符串以 "The" 开头)。


3.5.9.4 类型比较(Type comparisons)

下表展示了语法中支持的运算符对应的类型兼容性, 表示支持比较, 表示不支持比较。

运算符 1 \ 运算符 2布尔值(BOOLEAN)整数(INTEGER)浮点数(FLOAT)字符(CHAR)字符串(STRING)枚举(ENUM)
布尔值(BOOLEAN)
整数(INTEGER)
浮点数(FLOAT)
字符(CHAR)
字符串(STRING)
枚举(ENUM)✅ *

注:* 表示仅支持相同枚举类型之间的比较。


3.5.9.5 示例(Example)

假设 Topic Shape 的 IDL 定义如下:

idl

struct Shape  // 形状结构
{
    long x,  // x 坐标(长整型)
    long y,  // y 坐标
    long z,  // z 坐标
    long width,  // 宽度
    long height  // 高度
};

过滤器表达式示例

plaintext

"x < 23 AND y > 50 AND width BETWEEN %0 AND %1"

创建 ContentFilteredTopic

可使用上述过滤器表达式创建 ContentFilteredTopic,具体方法如 “创建 ContentFilteredTopic” 章节所述。代码示例如下:

cpp

运行

ContentFilteredTopic* sql_filter_topic =
        participant->create_contentfilteredtopic("Shape", topic,
                "x < 23 AND y > 50 AND width BETWEEN %0 AND %1",
                {"10", "20"});  // 参数列表:%0 对应 "10",%1 对应 "20"

在该示例中,参数被用于过滤器表达式。在内部,ContentFilteredTopic 创建时会先替换参数,最终使用的过滤器表达式如下:

plaintext

"x < 23 AND y > 50 AND width BETWEEN 10 AND 20"

3.5.10 使用自定义过滤器

Fast DDS API 支持创建用户自定义过滤器,并在后续注册该过滤器,以用于创建 ContentFilteredTopic。使用自定义过滤器需遵循以下步骤:

  • 创建自定义过滤器
  • 创建自定义过滤器的工厂
  • 注册工厂
  • 使用自定义过滤器创建 ContentFilteredTopic

3.5.10.1 创建自定义过滤器

自定义过滤器需通过一个继承自 IContentFilter 的类来实现。该类仅需实现一个函数,即重写 evaluate() 方法。每当 DataReader 接收到一个样本时,都会调用此函数,并传入以下参数:

  • payload:待过滤样本的序列化负载。
  • sample_info:样本附带的额外信息。
  • reader_guid:正在对其执行过滤操作的读取器(reader)的 GUID。

该函数返回一个布尔值,true 表示接受该样本,false 表示拒绝该样本。

以下代码片段展示了一个自定义过滤器的示例。该过滤器会从序列化样本中反序列化出 index 字段,并拒绝满足 index > low_mark_ 且 index < high_mark_ 条件的样本。

cpp

运行

class MyCustomFilter : public IContentFilter
{
public:

    MyCustomFilter(
            int low_mark,
            int high_mark)
        : low_mark_(low_mark)
        , high_mark_(high_mark)
    {
    }

    bool evaluate(
            const SerializedPayload& payload,
            const FilterSampleInfo& sample_info,
            const GUID_t& reader_guid) const override
    {
        // 从序列化样本中反序列化出 index 字段
        /* IDL 定义如下:
         *
         * struct HelloWorld
         * {
         *     long index;
         *     string message;
         * }
         */
        eprosima::fastcdr::FastBuffer fastbuffer(reinterpret_cast<char*>(payload.data), payload.length);
        eprosima::fastcdr::Cdr deser(fastbuffer);
        // 反序列化封装数据
        deser.read_encapsulation();
        int index = 0;

        // 反序列化 index 字段
        try
        {
            deser >> index;
        }
        catch (eprosima::fastcdr::exception::NotEnoughMemoryException& exception)
        {
            return false;
        }

        // 自定义过滤逻辑:拒绝 index 大于 low_mark_ 且小于 high_mark_ 的样本
        if (index > low_mark_ && index < high_mark_)
        {
            return false;
        }

        return true;
    }

private:

    int low_mark_ = 0;
    int high_mark_ = 0;

};

3.5.10.2 创建自定义过滤器的工厂

Fast DDS 通过工厂(Factory)创建过滤器。因此,需实现一个工厂类,用于实例化自定义过滤器。

自定义过滤器的工厂必须继承自 IContentFilterFactory 接口,该接口要求实现两个函数:

  1. 每当需要创建或更新自定义过滤器时,create_contentfilteredtopic() 会在内部调用 create_content_filter(),并传入以下参数:

    • filter_class_name:调用该工厂所针对的过滤器类名,支持同一工厂用于不同过滤器类。
    • type_name:待过滤主题(Topic)的类型名。
    • data_type:待过滤主题的类型支持对象。
    • filter_expression:自定义过滤器表达式。
    • filter_parameters:为过滤器参数设置的值(自定义过滤器表达式中需有可替换这些值的模式)。
    • filter_instance:创建过滤器时,输入时为 nullptr,输出时会指向创建好的过滤器实例;更新过滤器时,输入时为之前返回的实例指针。

    该函数需返回操作结果。

  2. 当需要删除自定义过滤器时,delete_contentfilteredtopic() 会在内部调用 delete_content_filter(),工厂需删除传入的自定义过滤器实例。

以下代码片段展示了一个自定义过滤器工厂的示例,该工厂用于管理上一节中实现的自定义过滤器实例。

cpp

运行

class MyCustomFilterFactory : public IContentFilterFactory
{
public:

    ReturnCode_t create_content_filter(
            const char* filter_class_name, // 自定义过滤器类名为 'MY_CUSTOM_FILTER'
            const char* type_name, // 该自定义过滤器仅支持一种类型:'HelloWorld'
            const TopicDataType* /*data_type*/, // 本实现中未使用该参数
            const char* filter_expression, // 该自定义过滤器未实现过滤器表达式
            const ParameterSeq& filter_parameters, // 需始终设置两个参数:low_mark 和 high_mark
            IContentFilter*& filter_instance) override
    {
        // 检查该 ContentFilteredTopic 是否应由当前工厂创建
        if (0 != strcmp(filter_class_name, "MY_CUSTOM_FILTER"))
        {
            return RETCODE_BAD_PARAMETER;
        }

        // 检查该 ContentFilteredTopic 是否为当前自定义过滤器支持的唯一类型而创建
        if (0 != strcmp(type_name, "HelloWorld"))
        {
            return RETCODE_BAD_PARAMETER;
        }

        // 检查是否已设置两个必需的过滤器参数
        if (2 != filter_parameters.length())
        {
            return RETCODE_BAD_PARAMETER;
        }

        // 若为更新操作,则删除之前的实例
        if (nullptr != filter_instance)
        {
            delete(dynamic_cast<MyCustomFilter*>(filter_instance));
        }

        // 实例化自定义过滤器
        filter_instance = new MyCustomFilter(std::stoi(filter_parameters[0]), std::stoi(filter_parameters[1]));

        return RETCODE_OK;
    }

    ReturnCode_t delete_content_filter(
            const char* filter_class_name,
            IContentFilter* filter_instance) override
    {
        // 检查该 ContentFilteredTopic 是否应由当前工厂创建
        if (0 != strcmp(filter_class_name, "MY_CUSTOM_FILTER"))
        {
            return RETCODE_BAD_PARAMETER;
        }

        // 删除自定义过滤器
        delete(dynamic_cast<MyCustomFilter*>(filter_instance));

        return RETCODE_OK;
    }

};

3.5.10.3 注册工厂

要在应用中使用自定义过滤器,需将自定义过滤器的工厂注册到 DomainParticipant 中。以下代码片段展示了如何通过 API 函数 register_content_filter_factory() 注册工厂。

cpp

运行

// 在指定域中创建一个 DomainParticipant
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 创建自定义过滤器工厂
MyCustomFilterFactory* factory = new MyCustomFilterFactory();

// 注册工厂
if (RETCODE_OK !=
        participant->register_content_filter_factory("MY_CUSTOM_FILTER", factory))
{
    // 错误处理
    return;
}

3.5.10.4 使用自定义过滤器创建 ContentFilteredTopic

“创建 ContentFilteredTopic” 章节介绍了创建 ContentFilteredTopic 的常规方法。若使用自定义过滤器,create_contentfilteredtopic() 有一个重载函数,可额外传入一个参数以选择自定义过滤器。

以下代码片段展示了如何使用自定义过滤器创建 ContentFilteredTopic

cpp

运行

// 在指定域中创建一个 DomainParticipant
DomainParticipant* participant =
        DomainParticipantFactory::get_instance()->create_participant(0, PARTICIPANT_QOS_DEFAULT);
if (nullptr == participant)
{
    // 错误处理
    return;
}

// 创建主题(Topic)
Topic* topic =
        participant->create_topic("HelloWorldTopic", "HelloWorld", TOPIC_QOS_DEFAULT);
if (nullptr == topic)
{
    // 错误处理
    return;
}

// 选择自定义过滤器创建 ContentFilteredTopic,不使用表达式但需传入两个参数
// 即使自定义过滤器不使用表达式,表达式也不能为空字符串,否则会像“创建 ContentFilteredTopic”章节中所述的那样,实际禁用过滤功能
std::string expression = " ";
std::vector<std::string> parameters;
parameters.push_back("10"); // low_mark 参数值
parameters.push_back("20"); // high_mark 参数值
ContentFilteredTopic* filter_topic =
        participant->create_contentfilteredtopic("HelloWorldFilteredTopic1", topic, expression, parameters,
                "MY_CUSTOM_FILTER");
if (nullptr == filter_topic)
{
    // 错误处理
    return;
}

// 之后可使用 ContentFilteredTopic 实例创建 DataReader 对象
Subscriber* subscriber =
        participant->create_subscriber(SUBSCRIBER_QOS_DEFAULT);
if (nullptr == subscriber)
{
    // 错误处理
    return;
}
DataReader* reader_on_filter = subscriber->create_datareader(filter_topic, DATAREADER_QOS_DEFAULT);
if (nullptr == reader_on_filter)
{
    // 错误处理
    return;
}

重要说明

尽管本示例中的自定义过滤逻辑未使用过滤器表达式,但需注意,表达式不能为空字符串 —— 否则会像 “创建 ContentFilteredTopic” 章节中所述的那样,禁用过滤功能。

注意事项

删除使用自定义过滤器的 ContentFilteredTopic 时,操作方式与 “删除 ContentFilteredTopic” 章节中介绍的常规方式完全一致。

3.5.11 过滤在何处应用:写入端与读取端

内容过滤器可在写入端(DataWriter)或读取端(DataReader)任意一端执行,因为写入端会在发现(discovery)过程中从读取端获取过滤器表达式。在写入端执行过滤能节省网络带宽,但代价是会增加写入端的 CPU 使用率。

3.5.11.1 写入端过滤的条件

只有当所有以下条件均满足时,写入端才会代替读取端执行过滤评估;否则,过滤将由读取端执行。

  1. 写入端(DataWriter)具有 “无限存活期”(infinite liveliness),具体可参考 “存活期 Qos 策略”(LivelinessQosPolicy)。
  2. 与读取端(DataReader)的通信既非进程内(intra-process)通信,也非数据共享(data-sharing)通信。
  3. 读取端(DataReader)未使用多播(multicast)。
  4. 写入端为读取端执行过滤的数量,未超过reader_filters_allocation所设置的最大值。

DataWriterQos中存在 “资源限制策略”(resource-limit policy),该策略用于控制写入端过滤资源的分配行为。将最大值设为 0 会禁用写入端的过滤评估;最大值设为 32(默认值)则表示写入端最多可为 32 个读取端执行过滤评估。

若写入端已为writer_resource_limits.reader_filters_allocation.maximum所指定数量的读取端执行过滤,此时若有新的带过滤功能的读取端创建,新读取端的过滤将由读取端自身执行。

3.5.11.2 发现过程中的竞争条件

在过滤器表达式和 / 或表达式参数会更新的应用中,可能会出现一种情况:在写入端通过发现过程接收到更新信息之前,它会一直使用旧版本的过滤器。这可能导致一种结果:若读取端更新过滤器后,写入端接收更新发现信息前的短时间内有数据发布,即使新过滤器本应允许该数据发送,该数据也可能无法发送到读取端;而在写入端接收更新发现信息后发布的数据,将使用更新后的过滤器进行判断。

若某些关键应用认为这种竞争条件问题无法接受,可将reader_filters_allocation的最大值设为 0,以此禁用写入端过滤。

翻译此文档保持内容不变

3.5.12 用于数据类型源代码生成的 Fast DDS-Gen

eProsima Fast DDS 附带一个内置源代码生成工具 Fast DDS-Gen,该工具可简化将数据类型的 IDL 规范转换为可运行实现的过程。因此,该工具会自动生成通过 IDL 定义的数据类型的源代码。下文将介绍该工具的基本用法。若需了解 Fast DDS 提供的所有功能,请参考 “Fast DDS-Gen” 章节。

3.5.12.1 基本用法

在 Linux 系统中,可通过调用 fastddsgen 命令执行 Fast DDS;在 Windows 系统中,则需调用 fastddsgen.bat 命令。包含数据类型定义的 IDL 文件需通过 <IDLfile> 参数传入。

  • Linux 系统fastddsgen [<options>] <IDLfile> [<IDLfile> ...]

  • Windows 系统fastddsgen.bat [<options>] <IDLfile> [<IDLfile> ...]

在 “用法”(Usage)中定义的可用参数里,Fast DDS-Gen 用于数据类型源代码生成的主要选项如下:

  • <replace>:若数据类型文件此前已生成,该选项会替换现有文件。
  • <help>:列出当前支持的平台和 Visual Studio 版本。
  • <no-typeobjectsupport>:禁用 TypeObject 表示注册代码的自动生成。
  • <example>:为指定的 <platform>(平台)生成一个基础的 DDS 应用示例及对应的构建文件。因此,Fast DDS-Gen 工具可使用提供的数据类型生成示例应用,同时生成在 Linux 发行版上用于编译的 Makefile,以及在 Windows 系统上使用的 Visual Studio 项目。若需查看相关示例,请参考教程 “构建发布 / 订阅应用”(Building a publish/subscribe application)。

3.5.12.2 输出文件

Fast DDS-Gen 会输出多个文件。假设 IDL 文件名为 “MyType”,且未定义上述任何选项,生成的文件如下:

  • MyType.hpp:数据类型定义文件。
  • MyTypePubSubType.cxx/MyTypePubSubType.h:数据类型的序列化与反序列化源代码。若主题(topic)实现了键(key),该文件还会定义 MyTypePubSubType 类的 getKey() 成员函数(具体可参考 “带键的数据类型”(Data types with a key))。
  • MyTypeCdrAux.hpp/MyTypeCdrAux.ipp:Fast CDR 进行类型编码与解码所需的辅助方法文件。
  • MyTypeTypeObjectSupport.cxx/MyTypeTypeObjectSupport.hpp:生成和注册 TypeObject 表示所需的辅助代码文件。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值