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)
使用实例(而非新建DataWriter、DataReader和Topic)的优势在于:相关实体已完成创建和发现流程,因此可减少内存占用,且无需执行新的发现操作(避免发现过程中产生的元数据流量,详情见 “发现机制(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_state与view_state状态流转逻辑:

- 接收 “从未见过” 的实例样本 →
view_state设为NEW,代计数初始化为 0; - 接收实例样本 → 若此前实例状态为
NOT_ALIVE(非活跃),且检测到 “活跃” 写入器,no_writers_generation_count递增;若实例因销毁后重新活跃,disposed_generation_count递增; - 读取 / 提取样本 →
view_state转为NOT_NEW(非新视图); - 无 “活跃” 写入器 → 实例状态转为
NOT_ALIVE_NO_WRITERS(无写入器); - 写入器销毁实例 → 实例状态转为
NOT_ALIVE_DISPOSED(已销毁); - 数据读取器中无样本,且实例状态为
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) |
|---|---|
| 1 | Position1 |
| 2 | Position2 |
| 3 | Position3 |
| 4 | Position4 |
| 5 | Position5 |
在此场景中,每当接收新样本,数据库中对应实例的条目会更新为最新已知位置;销毁实例可对应删除数据库中的相关数据。注册和注销实例的操作不会直接反映在数据库中,但如果同时持久化instance_state和view_state,也可跟踪实例的生命周期。
DataWriter通知 “将发布某实例数据” 这一行为,对数据库无实际意义 —— 只有当接收新数据时,数据库才会为新发现的实例执行插入操作。
关系型数据库也可存储历史数据,但根据具体场景,可能需要使用时序数据库以提升效率。在本航班跟踪场景中,除实例句柄外,可将样本时间戳(source timestamp)也设为主键,从而能查询特定航班的历史跟踪数据。示例表如下:
| 实例句柄 [主键](Instance handle [PK]) | 源时间戳 [主键](Source Timestamp [PK]) | 数据(Data) |
|---|---|---|
| 1 | 1 | Position1 |
| 2 | 1 | Position2 |
| 1 | 2 | Position3 |
| 1 | 3 | Position4 |
| 2 | 2 | Position5 |
基于此表,查询特定实例句柄可获取该航班的跟踪记录:
| 实例句柄 [固定](Instance handle [Fixed]) | 源时间戳(Source Timestamp) | 数据(Data) |
|---|---|---|
| 1 | 1 | Position1 |
| 1 | 2 | Position3 |
| 1 | 3 | Position4 |
查询特定时间戳可获取该时刻所有航班的位置快照:
| 实例句柄(Instance handle) | 源时间戳 [固定](Source Timestamp [Fixed]) | 数据(Data) |
|---|---|---|
| 1 | 2 | Position3 |
| 2 | 2 | Position5 |
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实例,提供类型注册以及与发布、订阅交互所需的函数。
注册数据类型的步骤如下:
- 用
TopicDataType实例创建一个新的TypeSupport对象; - 调用
TypeSupport的register_type()成员函数完成注册; - 之后即可使用已注册的类型名称创建主题(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),需完成以下操作:
- 重写
TopicDataType的getKey()成员函数,使其能根据数据字段返回对应的键值; - 将
m_isGetKeyDefined数据成员设为true,告知相关实体这是一个带键主题,且应使用getKey()函数获取键值。
未定义键的数据类型,其m_isGetKeyDefined成员会默认设为false。
在TopicDataType中实现键(key)有三种方式:
- 使用 Fast DDS-Gen 工具时,在 IDL 文件中为构成键的成员添加
@Key注解; - 使用 XTypes 时,为成员及其父级添加
Key属性; - 手动重写
TopicDataType的getKey()成员函数,并将m_isGetKeyDefined数据成员设为true。
带键的数据类型用于在单个主题(Topic)中定义数据子流:
- 同一主题中,具有相同键值的数据代表来自同一子流;
- 同一主题中,具有不同键值的数据代表来自不同子流。
中间件会将这些子流分开管理,但所有子流都受限于该主题(Topic)的同一组 QoS 参数。若未定义键,则该主题关联的数据集仅局限于单个流。
3.5.7 创建主题(Topic)
3.5.7 创建主题(Topic)
主题(Topic)始终隶属于某个域参与者(DomainParticipant)。创建主题需调用域参与者实例的create_topic()成员函数,该函数相当于主题的 “工厂方法”。
必选参数
- 用于标识主题的字符串名称;
- 已注册的、将在主题中传输的数据类型名称;
- 描述主题行为的
TopicQos(主题服务质量)。若传入值为TOPIC_QOS_DEFAULT,则使用 “默认主题服务质量(Default TopicQos)”。
可选参数
- 继承自
TopicListener的监听器(Listener):实现主题发生事件或状态变化时触发的回调函数,默认使用空回调; 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()成员函数创建主题。
必选参数
- 用于标识主题的字符串名称;
- 已注册的、将在主题中传输的数据类型名称;
- 要应用到主题的配置文件名称。
可选参数
- 继承自
TopicListener的监听器(Listener):实现主题发生事件或状态变化时触发的回调函数,默认使用空回调; 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()成员函数,该函数相当于内容过滤主题的 “工厂方法”。
必选参数
- 用于标识内容过滤主题的字符串名称;
- 待过滤的关联主题(related Topic);
- 过滤表达式字符串:用于指定数据样本需满足的返回条件;
- 表达式参数字符串列表:为过滤表达式中的参数提供具体值。
注意:参数值的数量不得超过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 更新过滤表达式与参数
内容过滤主题提供多个成员函数,用于管理过滤表达式和表达式参数:
- 调用
get_filter_expression()成员函数,可获取当前过滤表达式; - 调用
get_expression_parameters()成员函数,可获取当前表达式参数; - 调用
set_expression_parameters()成员函数,可修改表达式参数,需遵循与创建内容过滤主题时相同的约束; - 调用
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 接口,该接口要求实现两个函数:
-
每当需要创建或更新自定义过滤器时,
create_contentfilteredtopic()会在内部调用create_content_filter(),并传入以下参数:filter_class_name:调用该工厂所针对的过滤器类名,支持同一工厂用于不同过滤器类。type_name:待过滤主题(Topic)的类型名。data_type:待过滤主题的类型支持对象。filter_expression:自定义过滤器表达式。filter_parameters:为过滤器参数设置的值(自定义过滤器表达式中需有可替换这些值的模式)。filter_instance:创建过滤器时,输入时为nullptr,输出时会指向创建好的过滤器实例;更新过滤器时,输入时为之前返回的实例指针。
该函数需返回操作结果。
-
当需要删除自定义过滤器时,
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 写入端过滤的条件
只有当所有以下条件均满足时,写入端才会代替读取端执行过滤评估;否则,过滤将由读取端执行。
- 写入端(DataWriter)具有 “无限存活期”(infinite liveliness),具体可参考 “存活期 Qos 策略”(LivelinessQosPolicy)。
- 与读取端(DataReader)的通信既非进程内(intra-process)通信,也非数据共享(data-sharing)通信。
- 读取端(DataReader)未使用多播(multicast)。
- 写入端为读取端执行过滤的数量,未超过
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 表示所需的辅助代码文件。

4511

被折叠的 条评论
为什么被折叠?



