"Reactive Stream Processing in Industrial IoT using DDS and Rx " 是指在工业物联网(IIoT )场景中,结合 DDS(Data Distribution Service) 和 Rx(Reactive Extensions) 技术,实现 响应式流式处理 的架构。
核心术语解释:
名称 | 含义 |
---|---|
Industrial IoT (IIoT) | 工业物联网,指连接机器、传感器、控制系统的数据网络 |
DDS (Data Distribution Service) | 实时数据分发中间件,用于可靠、低延迟、分布式通信 |
Rx (Reactive Extensions) | 响应式编程框架,处理异步数据流 和事件驱动系统 |
Reactive Stream Processing | 响应式流处理,表示基于事件流的数据处理方式 |
为何在 IIoT 中使用 Reactive + DDS?
IIoT 需求特征:
- 大量传感器数据(高速、持续流动)
- 实时响应控制系统(毫秒级响应)
- 容错 & 稳定性(制造、能源、交通等关键应用)
- 可扩展性(成百上千设备)
DDS 的优势:
- 发布-订阅模型,天然支持分布式数据通信
- 支持 QoS(延迟、丢包率、带宽控制)
- 零拷贝传输,适合实时系统
- 支持多种数据发现、路由机制
Rx 的优势:
- 用声明式方式描述复杂异步数据流
- 运算符丰富:
filter
,map
,merge
,zip
,combineLatest
... - 支持 背压(Backpressure)、异步流处理、容错恢复
两者结合:Rx + DDS
典型架构流程:
[Sensor] --(Data Stream)--> [DDS Publisher]
↓
[DDS Subscriber Layer]
↓
[Rx Observable Stream]
↓
[Rx Operators: filter/map/retry]
↓
[Alert System / Dashboard / DB]
工作流程举例(实时温度监控):
-
传感器节点 通过 DDS 发布温度数据(Publisher)
-
边缘计算节点 作为 DDS Subscriber 接收数据
-
用 Rx 把数据转为 Observable:
cppauto stream = from_dds(topic) .filter([](TempReading r) { return r.value > 80; }) .map([](TempReading r) { return r.timestamp; });
-
将异常温度传给报警系统
技术整合示例(C++)
cpp
dds::sub::DataReader<TempReading> reader = ...;
auto observable = rxcpp::observable<>::create<TempReading>(
[&reader](rxcpp::subscriber<TempReading> s) {
while (true) {
TempReading sample = reader.take();
s.on_next(sample);
}
});
observable
.filter([](auto sample){ return sample.temp > 80; })
.subscribe([](auto hotSample) {
std::cout << " Overheat: " << hotSample.temp << "\n";
});
概念对比:传统 vs Reactive
功能 | 传统轮询方式 | Reactive DDS-Rx |
---|---|---|
数据获取 | 定时查询 | 事件驱动 |
扩展性 | 低 | 高 |
响应延迟 | 高 | 低 |
编码复杂度 | 高 | 中(可读性更好) |
容错机制 | 自己处理 | 操作符内置 |
应用场景
- 生产设备实时监控
- 流水线数据分析
- 预测性维护(预测设备故障)
- 远程诊断与报警
- 能耗优化(实时负载监测)
面临挑战
- Rx 的学习曲线较高(尤其 C++ 实现)
- DDS QoS 设置复杂(需细致调优)
- 系统调试和事件跟踪较困难(涉及多线程和异步)
- 跨平台部署 DDS + Rx 需谨慎选择版本和绑定
总结
Reactive Stream Processing in IIoT using DDS and Rx 是现代工业系统中非常先进的架构模式,结合 DDS 的实时性与 Rx 的响应式能力,能够打造高度可扩展、可维护、实时性强的 IIoT 系统。
**工业物联网系统(Industrial IoT Systems)**的核心特性的描述:
1. Industrial IoT Systems
工业物联网系统 是将**物联网(IoT)**技术应用到工业场景(如制造、能源、交通、农业)的系统。这些系统由传感器、控制器、执行器、边缘/云计算平台等组成,用来实时采集、分析并响应现场数据。
不能中断信息处理。
含义:
- 工业场景中的系统通常是实时运行 的,例如:
- 工厂流水线控制系统
- 电力网监控系统
- 自动化仓储机器人导航系统
- 一旦数据流入系统(例如温度、速度、电压等传感器信息),系统必须持续处理 ,不可延迟、不能丢失。
- 中断处理意味着可能会:
- 导致安全事故(如温度失控)
- 造成产线停摆或设备损坏
- 导致数据不一致或失真(如实时监控图像延迟)
技术挑战:
- 高可靠性(系统不能崩溃)
- 高可用性(必须7x24运行)
- 容错机制(节点挂了,不能影响整体)
必须遵守由现实世界 施加的时间限制。
含义: - IIoT 系统必须在现实物理世界的时序约束下 运行。例如:
- 控制机械臂的命令必须在 10 毫秒内发出
- 火警报警必须在 1 秒内响应
- 电网频率异常必须在 50 毫秒内反馈
- 也就是说,系统的时序不是由开发者自由决定的 ,而是由物理环境和工业过程决定。
- 属于硬实时系统 :任务必须在规定时间内完成 ,否则会造成不可接受的后果。
技术挑战: - 实时调度
- 精确时间同步(如使用 NTP/PTP)
- 实时操作系统支持(RTOS)
- 网络延迟控制(如使用 DDS 进行零拷贝传输)
举例说明:
假设你有一个智能工厂中的机器人系统:
- 它每 10 毫秒接收一次传感器数据(如距离、速度、电机温度);
- 然后在 5 毫秒内做出决策(是否继续运行、是否规避障碍);
- 并在接下来的 2 毫秒内将命令发送到电机控制器。
这就是典型的"不能停顿数据处理 " + "必须符合现实世界时间限制"的例子。
总结
特性 | 描述 |
---|---|
不能停止处理 | 实时数据不断流入,系统必须持续响应 |
受现实时间限制 | 行为要在固定时间窗口内完成,否则失效 |
持续运行 | 7x24 运作,不能重启或掉线 |
高后果风险 | 超时或失效可能导致安全/经济事故 |
**响应式系统(Reactive Systems)的特性总结,尤其适用于工业物联网(Industrial IoT)**或其他高负载、高并发、实时要求强的系统架构。我们来详细逐条解释这些概念:
一览表:响应式系统的四大核心特性
特性 | 描述 | 关键词 |
---|---|---|
Responsive | 系统能对事件及时做出反应 | 快速响应 |
Resilient | 出现故障时依然能继续运行 | 容错机制 |
Elastic | 系统能根据负载自动扩缩容 | 横向扩展 |
Event-Driven | 系统通过事件和消息驱动组成 | 解耦、异步 |
这些特性正是 Reactive Manifesto(响应式宣言) 的基础。 |
1. Responsive(响应式)
系统能够以环境所需的速度对外部或内部事件做出反应。
在工业场景中的例子:
- 检测到温度过高 → 必须立即停止设备。
- 摄像头识别到异常 → 实时报警。
- 客户端操作 → 后台立即返回响应。
技术实践:
- 使用 异步处理(如 Rx 或协程)
- 事件优先级机制
- 使用非阻塞 I/O,避免延迟
2. Resilient(弹性/容错)
系统具备容错能力 ,即使某个组件或服务失败,系统整体也不崩溃。
典型手段:
- 微服务架构中的熔断(circuit breaker)
- 节点失败 → 自动切换到备用节点
- 异常处理流如:
retry
,timeout
,fallback
技术实践:
- Supervisor模式(如 Akka)
- 隔离与重启策略
- 日志驱动回溯
3. Elastic / Scalable(弹性 / 可扩展)
系统可以根据工作负载的变化自动伸缩资源,包括:
- 自动增加/减少线程、CPU 核心
- 部署更多服务副本
工业场景:
- 数据突增 → 自动加节点处理传感器数据
- 夜间负载低 → 自动缩容节省能源
技术实践:
- 容器 + 自动调度器(如 Kubernetes)
- 分布式消息队列 + 并发工作线程池
- 按需调度的函数服务(如 serverless)
4. Event-Driven(事件驱动)
"Asynchronous, loosely-coupled, modular, pipelined"
系统内部通过异步事件和消息 进行通信,各模块之间是松耦合的 ,系统呈现出模块化+管道式结构。
优势:
- 每个模块职责单一、清晰
- 解耦便于测试和维护
- 异步处理更高性能
技术实现:
- 使用消息中间件(如 Kafka, MQTT, DDS)
- 使用 Rx(ReactiveX)进行数据流编排
- 事件总线架构(event bus / event sourcing)
Messaging Middleware(消息中间件)
消息中间件是事件驱动架构的支撑核心,它用于:
- 不同系统组件之间传递消息(解耦)
- 保证消息可靠送达、顺序、有状态处理
- 支持异步 / 发布-订阅通信
常见中间件包括:
名称 | 特性 |
---|---|
MQTT | 轻量、适合低功耗设备 |
Kafka | 高吞吐、分布式、日志式处理 |
DDS | 实时、QoS 控制、适合工业级系统 |
ZeroMQ/RabbitMQ | 灵活、支持多种传输模型 |
总结示意图:
┌──────────────────────────┐
│ Industrial IoT │
└────────┬─────────────────┘
│
▼
┌───────────────┐ [Event-Driven]
│ Sensor/Device │─────► Publish data
└───────────────┘
│
▼ [Resilient]
┌────────────┐ ┌──────────┐
│ Messaging │───►│ Processor│ (modular, retryable)
└────────────┘ └──────────┘
│ ▲
▼ │
┌────────────┐ │ [Responsive]
│ Dashboard │ <───────┘ Display alert in ms
└────────────┘
小结对照表:
特性 | 含义 | 技术工具 |
---|---|---|
Responsive | 实时响应事件 | Rx, Reactor, async/await |
Resilient | 容错、恢复机制 | 重试、熔断、Actor模型 |
Elastic | 负载变化时自动伸缩 | k8s, autoscaling, cloud API |
Event-Driven | 事件驱动异步解耦 | DDS, MQTT, Kafka, Rx |
Messaging Middleware | 解耦通信桥梁 | Kafka, DDS, RabbitMQ |
DDS(Data Distribution Service) 在工业物联网(Industrial IoT )中的角色概述。下面我们用更详细直观的方式来逐一理解 DDS 所连接的各个组成部分,以及它在工业级系统中扮演的关键角色。
一句话理解 DDS:
DDS 是专为实时、分布式、大规模系统设计的数据通信标准,广泛用于工业物联网中,实现设备、传感器、系统之间的高效可靠的数据流动。
DDS 与 Industrial IoT 的数据流关系图
[Sensors] ──┐
│
[Events] ───┤
▼
┌──────────┐
│ DDS │ ← 数据中枢/通信骨干
└──────────┘
▲ ▲
┌─────────┘ └─────────┐
▼ ▼
[Real-Time Apps] [Enterprise Apps]
│ │
▼ ▼
[Actuators] [Storage/BI/Cloud]
每个关键词的解释:
1. Streaming Data(流式数据)
- 表示传感器或设备持续不断地生成数据流。
- DDS 天生支持 发布-订阅 模式,非常适合处理持续流式的数据。
2. Sensors(传感器)
- 工业 IoT 的数据来源,如温度计、压力计、运动检测器等。
- 这些传感器不断将数据通过 DDS 发送给需要的系统或模块。
3. Events(事件)
- 某些数据满足特定条件就会触发事件(例如:温度超标)。
- DDS 可以对这些事件进行实时传输和多方订阅,从而快速触发自动化响应。
4. Real-Time Applications(实时应用)
- 如:生产线控制系统、自动驾驶机器人、智能报警系统。
- 它们对延迟要求极高(毫秒级),而 DDS 提供的 QoS(Quality of Service)参数使其能够严格控制延迟、可靠性、带宽使用等。
5. Enterprise Applications(企业级应用)
- 如:云存储、大数据平台、ERP 系统、BI 分析系统。
- DDS 可以与这些系统集成,使得现场数据能汇总到高层应用,实现全局优化、数据可视化等。
6. Actuators(执行器)
- 如电机、机器人手臂、阀门、LED 指示灯等。
- 实时应用接收到 DDS 消息后,控制这些执行器进行响应动作(例如关闭机器、触发警报)。
DDS 的技术特点和优势
特性 | 描述 |
---|---|
发布-订阅模型 | 生产者发布数据,消费者订阅,无需相互知道 |
支持 QoS 策略 | 控制传输可靠性、延迟、持久性、带宽等 |
去中心化架构 | 无需中央服务器,提高系统容错性和实时性 |
零配置发现机制 | 自动发现网络中的发布者和订阅者 |
多种传输方式 | 支持 UDP、TCP、共享内存等传输方式 |
跨平台、跨语言 | 可在嵌入式设备到服务器上运行 |
DDS 在工业 IoT 中解决的痛点:
问题 | DDS 的解决方式 |
---|---|
实时性要求高 | 支持低延迟传输 + 实时 QoS 设置 |
设备种类多,协议不同 | DDS 是开放标准,有统一接口和模型 |
数据量大,消息太频繁 | 支持高吞吐数据流处理 + 过滤策略 |
通信网络复杂,拓扑变化频繁 | 自动发现机制,无需手动维护连接 |
部分设备不稳定,易断网 | 内建容错机制 + 缓存/重发策略 |
总结一句话:
DDS 是工业物联网中的"数据高速公路",连接传感器、事件源、实时控制系统、企业平台和执行器,确保数据能够快速、安全、可靠地流动与处理。
DDS(Data Distribution Service)通信模型 的核心架构,特别是由 RTI(Real-Time Innovations)DDS 所实现的"Global Data Space"概念。下面我们详细讲解每个关键术语和架构组成,并配图和类比帮你建立清晰理解。
一句话理解 DDS 通信模型:
DDS 提供一个"全局数据空间"(Global Data Space),让所有发布者和订阅者可以自动发现彼此,通过话题(Topic)进行解耦的数据交换,靠 QoS 协议建立通信契约。
核心概念解释
1. Global Data Space(全局数据空间)
- 就像一个"公共消息黑板 "或"共享数据库 ",但不是存储数据,而是传输实时消息。
- 所有 参与者(Participant) 都可以向这里发布数据 (Publisher)或订阅数据(Subscriber)。
- 不需要彼此知道对方是谁,实现完全解耦。
2. Domain(域)
- 类似于一个独立的通信"频道"。
- 不同 Domain 之间数据不互通,用于逻辑隔离(例如不同车间、不同功能子系统)。
- 每个 Participant 加入一个 Domain。
3. Topic(主题)
- 表示一种特定类型的数据结构(如"Track"、"Alarm")。
- 类似于 MQTT 的 Topic,但有结构化数据类型定义(通过 IDL 定义)。
- 一个 Topic 对应一个数据模型(带字段的结构体)。
4. Key(键)
- Topic 数据可通过 Key 区分"实例",例如每辆车的追踪数据都是 Track Topic,但用
car_id
区分不同对象。
5. Publisher / Subscriber
- Publisher 负责将某个 Topic 的数据发送到 Global Data Space。
- Subscriber 负责监听并接收某个 Topic 的数据。
- 一个 Participant 可同时拥有多个 Pub 和 Sub。
6. QoS(Quality of Service)
- DDS 提供丰富的 QoS 策略,例如:
- Reliability(可靠性):是否保证不丢包?
- Durability(持久性):是否需要存储一段时间?
- Deadline(时限):多久必须收到一次数据?
- Latency Budget(延迟预算):最大允许延迟是多少?
- Pub 和 Sub 之间只有 QoS 匹配,才会真正建立"通信契约",否则不交换数据。
7. Automatic Discovery(自动发现机制)
- Pub/Sub 不用事先知道对方是谁。
- Participant 会自动发现同一 Domain 中所有 Topic 和 Participant,实现"零配置连接"。
架构图理解(文字版)
txt
┌────────────┐
│ Global │
│ Data Space │
└────┬───────┘
│
┌────────────┐ ┌────────┴────────┐ ┌────────────┐
│ Participant│ │ Participant │ │ Participant│
│ (Pub:Track1) ───▶ Filter: Track1 │◀─── │ (Sub:Track1)
└────────────┘ └────────────────┘ └────────────┘
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Participant│ │Participant │ │Participant │
│ (Pub:Alarm)│ │(Pub:Track2)│ │ (Sub:Alarm)│
└────────────┘ └────────────┘ └────────────┘
总结:DDS 通信模型的优势
特性 | 描述 |
---|---|
解耦设计 | Pub/Sub 无需直接通信,基于 Topic 通讯 |
自动发现 | 系统自动找出谁在发送谁在接收 |
灵活 QoS | 每个通信通道都可定制延迟、可靠性、频率等要求 |
结构化 Topic 数据 | 更适合复杂系统比 MQTT 更强 |
无 Broker / 中心节点 | 真正去中心化通信,支持嵌入式 |
类比帮助你理解:
术语 | 类比说明 |
---|---|
Global Data Space | 公司公告板,所有人都能看到上面发布的信息 |
Topic | 公告的类型,比如"警报"、"追踪信息" |
Domain | 部门或分公司,各自有自己的公告板 |
Key | 每个公告上写的"编号"或"对象标识" |
Publisher | 写公告的人 |
Subscriber | 看公告并做事的人 |
QoS | 公告发布和查看的规则,比如"必须看"、"多久更新一次" |
如果你想了解: |
- DDS 的实时应用案例(如自动驾驶、风电监控)
- DDS 与 MQTT、Kafka 对比
- 如何使用 RTI Connext DDS 实现自动发现与调试
DDS(Data Distribution Service)标准家族(DDS Standard Family) 中的主要组成部分和扩展模块。它清晰展示了从底层通信协议,到中间的核心数据分发机制,再到顶层语言绑定和安全扩展的一整套技术体系。以下是详细解释:
一张图解释 DDS 标准家族结构(逻辑层级)
┌────────────────────────────┐
│ 应用层 (Application Layer) │ ← 开发者写的业务代码
├────────────────────────────┤
│ DDS 编程语言绑定 (API层) │ ← DDS-JAVA、DDS-C++ 等语言支持
├────────────────────────────┤
│ 数据建模与封送层 (IDL/XTypes) │ ← 定义数据结构、类型演化
├────────────────────────────┤
│ 安全扩展 (Security Layer) │ ← 加密、认证、访问控制
├────────────────────────────┤
│ 数据分发核心协议 (DDS Core) │ ← 主题发布订阅机制、QoS
├────────────────────────────┤
│ 传输层 (RTPS / UDP / TCP / SHM) │ ← 网络传输与发现机制
└────────────────────────────┘
各部分详细解释
顶层:应用与开发接口
模块名 | 说明 |
---|---|
DDS-WEB | 允许 Web 客户端通过 HTTP(S) 接口访问 DDS 数据(Web 友好 API)。 |
DDS-RPC* | 提供远程过程调用(Remote Procedure Call)机制,在 Pub/Sub 基础上支持请求/响应交互。 |
DDS-C++ / DDS-JAVA / DDS-IDL-C# / DDS-IDL-C | 支持多种语言与 DDS 通信(C++、Java、C#、C)。 开发者可用这些语言与 DDS 数据空间交互。 |
中层:数据建模与序列化
模块名 | 说明 |
---|---|
IDL 4.0 | Interface Definition Language,用来定义结构化数据类型。 |
DDS-XTYPES | 提供支持**数据类型演化(Type Evolution)**的能力。例如旧设备发布旧格式数据,新设备仍能理解。 |
安全层(可选)
模块名 | 说明 |
---|---|
DDS-SECURITY | 提供认证、加密、访问控制等安全机制,确保工业系统的数据传输是可信和私密的。 使用 TLS、DTLS、访问策略进行保护。 |
核心层(核心DDS标准)
模块名 | 说明 |
---|---|
DDS v1.4 | 最新版本的核心 DDS 标准,定义发布/订阅机制、QoS 策略、数据生命周期等基础能力。 |
RTPS v2.2 | Real-Time Publish-Subscribe 协议:DDS 使用的底层传输协议,支持自动发现、无 Broker 通信。 |
底层传输协议(Transport)
协议名称 | 功能说明 |
---|---|
UDP | 默认使用的高效无连接协议。 |
TCP | 用于需要可靠连接的场景(如金融数据交换)。 |
HTTP(S) | 支持 Web 客户端访问(通过 DDS-WEB)。 |
TLS / DTLS | 传输层安全协议,保障 DDS 通信加密。 |
SHARED MEMORY(共享内存) | 用于同一主机内进程间 DDS 通信,提高性能。 |
总结:DDS 标准家族的核心特性
层级 | 功能 |
---|---|
应用层 | 多语言开发接口支持 |
数据层 | 可扩展数据模型(XTypes) |
安全层 | 加密、安全认证、授权控制 |
核心协议 | 实时通信 + QoS + 自动发现 |
网络层 | 支持 UDP、TCP、共享内存等 |
理解提示:
- DDS 并不是单一协议,而是一个完整的通信生态,可以用在嵌入式、边缘、服务器、云端。
- 它特别适合实时性要求高、分布式、异构系统,如工业控制、自动驾驶、机器人系统。
- 所有这些模块是 模块化标准,可以根据需求启用/裁剪。
"Stream Processing" 的理解可以总结如下:
Stream Processing(流处理)的定义
流处理是一种架构风格,专门针对连续不断产生的数据序列进行实时处理。
详细解释:
- 数据是连续的:不是一次性批量获取,而是数据像流水一样不断产生,比如传感器数据、日志事件、金融交易流等。
- 实时处理:系统必须快速响应、处理和分析这些数据,不能等待全部数据收集完毕后再做计算。
- 无界数据:数据流理论上是无限长的,不断流入系统。
- 常见操作 :过滤、转换、聚合、分组、连接等,通常形成一个处理流水线(pipeline)。
举个简单类比:
就像自来水管里一直流着水,流处理系统不停地"接水",处理水中的杂质、调节水质,然后送到用水端,而不是等水管装满了再一次性处理。
应用场景示例:
- 工业IoT:设备传感器数据实时监控预警。
- 金融行业:实时风险监控、欺诈检测。
- 互联网:日志分析、实时推荐系统。
- 通信:网络流量监控。
Unix 命令行的管道(pipes)和过滤器(filters) 的思想,结合数据流处理中的 数据管道(Data Pipelines) 概念。以下是详细理解:
Unix Command Line (Pipes and Filters) 基本思想
- Pipes(管道):允许将一个命令的输出直接传递给另一个命令作为输入,形成"数据流"。
- Filters(过滤器) :是处理数据流的独立命令,负责对数据进行转换、过滤、聚合等操作。
比如:
bash
cat file.txt | grep "error" | sort | uniq -c
这就是一个数据管道,数据从左向右流动,经过多个处理步骤。
Data Pipelines 在流处理中的对应
你提到的单词:
- Where:类似过滤器(filter),根据条件筛选数据。
- Once:可能指"一次性操作"或单次事件处理。
- CombineLatest:常见于响应式编程(Reactive Programming),将多个数据流合并,取最新的值。
- Select:对应映射(map),对数据进行转换。
- Scan:类似累积或折叠操作(reduce/accumulate),对流中数据进行累积计算。
- Merge:将多个数据流合并成一个。
- Raw Data:原始数据流。
结合理解:
这些操作像 Unix命令管道中的命令,分别完成不同的功能:
- 数据从 Raw Data 流出,
- 经过 Where 过滤,
- 通过 Select 转换,
- 用 Scan 进行累计计算,
- 多个流用 Merge 或 CombineLatest 合并,
- 最终形成输出数据流(o/p)。
总结
- Unix 命令行管道和过滤器提供了一个经典的数据流处理模型。
- 流处理框架(比如 Reactive Extensions, Rx,或者工业IoT中的DDS)借鉴了这一理念,将复杂数据处理拆分成多个独立模块,连接成管道。
- 这样做的好处是模块化、异步、易扩展。
用 RxCpp(Reactive Extensions for C++)结合演示一个简单的数据流处理示例,帮助理解这些管道操作:
任务
- 从一组数字(Raw Data)开始,
- 用
Where
过滤偶数, - 用
Select
把数字乘以10(转换), - 用
Scan
累积求和, - 用
Merge
合并两个流, - 用
CombineLatest
组合两个流的最新值。
代码示例(RxCpp)
cpp
#include <rxcpp/rx.hpp>
#include <iostream>
namespace rx=rxcpp;
namespace rxsub=rxcpp::subjects;
namespace rxu=rxcpp::util;
int main() {
// 原始数据流
auto raw_data1 = rx::observable<>::iterate({1, 2, 3, 4, 5});
auto raw_data2 = rx::observable<>::iterate({10, 20, 30});
// 过滤 (Where):过滤偶数
auto filtered = raw_data1
.filter([](int v){ return v % 2 == 0; });
// 转换 (Select):乘以10
auto transformed = filtered
.map([](int v){ return v * 10; });
// 扫描 (Scan):累积求和
auto scanned = transformed
.scan(0, [](int acc, int v){ return acc + v; });
// 合并 (Merge):合并两个流
auto merged = rx::observable<>::merge(scanned, raw_data2);
// 组合最新 (CombineLatest)
auto combined = scanned.combine_latest(
raw_data2,
[](int acc, int v){ return std::make_pair(acc, v); }
);
std::cout << "Merged output:" << std::endl;
merged.subscribe([](int v){ std::cout << v << " "; });
std::cout << std::endl;
std::cout << "CombineLatest output:" << std::endl;
combined.subscribe([](std::pair<int,int> p){
std::cout << "(" << p.first << ", " << p.second << ") ";
});
std::cout << std::endl;
return 0;
}
讲解
filter
对应你说的 Where,筛选符合条件的元素。map
就是 Select,元素转换操作。scan
实现 Scan,累计聚合结果。merge
合并两个数据流。combine_latest
组合两个流中最新的值,生成元组。
输出示例(说明流程)
Merged output:
10 30 60 10 20 30
CombineLatest output:
(10, 10) (30, 10) (30, 20) (60, 20) (60, 30)
Reactive Extensions (Rx) 的核心概念和背景,以下是详细理解:
Reactive Extensions (Rx) 简要理解
- 发明背景
由微软的 Erik Meijer 和团队发明,目的是为了解决异步和事件驱动编程的复杂性。 - 核心是什么?
Rx 是一套 API(编程接口) ,用于创建、组合和操作 异步事件流(observable streams)。 - Rx 的三个核心部分 :
- Observables(可观察对象)
- 把异步数据流抽象成一等公民,像集合一样处理数据流。
- Composition(组合)
- 提供丰富的操作符(filter过滤、select映射、aggregate聚合等),支持组合多个流和时间操作。
- Schedulers(调度器)
- 管理并发执行,支持线程池、定时器等,方便控制异步执行上下文。
- Observables(可观察对象)
- 函数式编程风格
- Rx大量使用函数式编程和单子(Monad)概念,使流处理更简洁、可组合。
- 多语言支持
- Rx有丰富的跨平台实现,比如 Rx.NET(C#)、RxJava(Java)、RxJS(JavaScript)、RxCpp(C++)、RxPython(Python)等。
总结
Rx 把异步和事件编程变成了处理"数据流"的操作,就像处理普通集合一样灵活,极大简化了复杂的异步逻辑。它通过 Observables、组合操作符、调度器 三部分,让程序员写出更简洁、响应式的代码。
Push 和 Pull 两种接口模型,以 C# 为例,解释它们的对偶关系。下面详细讲解:
Pull 接口(拉模式)
- 代表接口:
IEnumerator<T>
和IEnumerable<T>
- 工作方式:消费者主动去请求数据(拉取)
- 关键方法和属性 :
IEnumerator<T>
:T Current { get; }
------ 当前元素bool MoveNext()
------ 移动到下一个元素,返回是否成功(还有元素)
IEnumerable<T>
:IEnumerator<T> GetEnumerator()
------ 获取枚举器,用于遍历数据
理解 :你(消费者)手动控制数据读取流程,像用for循环遍历集合一样,每次调用MoveNext()
拉取下一个元素。
Push 接口(推模式)
- 代表接口:
IObserver<T>
和IObservable<T>
- 工作方式:生产者主动把数据推送给消费者
- 关键方法 :
IObserver<T>
(观察者接口):void OnNext(T value)
------ 推送一个新值void OnCompleted()
------ 数据发送完毕void OnError(Exception ex)
------ 出现错误
IObservable<T>
(可观察对象接口):IDisposable Subscribe(IObserver<T>)
------ 订阅数据流
理解:生产者决定什么时候把数据发给你(消费者),你不需要主动拉,而是被动"接收推送"。
总结
特点 | Pull (拉) | Push (推) |
---|---|---|
主动方 | 消费者主动请求数据 | 生产者主动推送数据 |
接口 | IEnumerable<T> 和 IEnumerator<T> |
IObservable<T> 和 IObserver<T> |
控制权 | 消费者控制数据读取流程 | 生产者控制数据发送流程 |
适用场景 | 同步、有限数据集合 | 异步、无限数据流(事件流) |
关系
- Rx(Reactive Extensions)就是基于 Push 模式的观察者设计模式扩展。
- Pull 是传统遍历集合的方式,Push 更适合异步事件流和响应式编程。
比较不同编程模型中同步/异步 和单个/多个数据项的接口设计,结合一些技术和库来说明它们的关系。下面详细解释:
维度一:同步 (Sync) vs 异步 (Async)
-
同步 (Sync)
调用者发起操作后,需要等待结果返回,整个调用是阻塞的。
例如:cppT get_data(); // 直接返回数据,调用线程阻塞直到结果准备好
-
异步 (Async)
调用者发起操作后,立即返回一个"未来"的对象(future),结果稍后可用,调用线程不阻塞。
例如:cppstd::future<T> get_data_async(); get_data_async().then([](T result){ /*处理结果*/ });
这里
.then()
是注册回调,表示异步结果准备好时要执行的动作。
维度二:单个 (Single) vs 多个 (Many)
- 单个 (Single)
返回单一数据项,比如一个元素或值。 - 多个 (Many)
返回多个数据项,典型的是一个序列或集合,比如std::vector<T>::iterator
代表一段连续数据序列的迭代器。
结合示例
情况 | 典型接口 | 说明 |
---|---|---|
单个同步 | T get_data() |
直接返回单个数据,调用阻塞 |
单个异步 | std::future<T> get_data_async() + .then() |
返回 future,结果异步传递,带回调 |
多个同步 | std::vector<T>::iterator |
直接遍历数据序列,阻塞 |
多个异步 | rxcpp::observable<T> |
RxCpp中可观察序列,异步推送多个数据项 |
解释
get_data()
是最传统的同步单个数据访问方式。std::future<T>
是 C++ 标准库支持的异步单个数据访问方式,可以注册回调继续链式操作。std::vector<T>::iterator
是同步访问多个数据的传统方式。rxcpp::observable<T>
是基于观察者模式的异步多数据项流,支持响应式编程。
总结
单个 (Single) | 多个 (Many) | |
---|---|---|
同步 (Sync) | T | std::vector::iterator |
异步 (Async) | std::future + then | rxcpp::observable |
这张表和内容帮助你理解了不同模型下的数据访问接口如何设计,以及这些接口背后的同步异步、单个多个的区分。 |
这段代码是一个 RxCpp 的简单示例,展示了如何创建一个可观察序列(observable),订阅它,并响应事件。
代码解释:
cpp
rxcpp::observable<int> values = rxcpp::observable<>::range(1, 5);
- 这里用
rxcpp::observable<>::range(1, 5)
创建了一个 可观察序列,它会依次产生从 1 到 5 的整数(包括1和5)。 values
就是这个可观察对象。
cpp
auto subscription = values.subscribe(
[](int v){ printf("OnNext: %d\n", v); },
[](){ printf("OnCompleted\n"); }
);
- 通过
subscribe
方法订阅这个序列。 - 传入两个 lambda 函数:
- 第一个:当有新值产生时(OnNext),打印这个值。
- 第二个:序列完成时(OnCompleted),打印"OnCompleted"。
运行效果
程序会打印:
OnNext: 1
OnNext: 2
OnNext: 3
OnNext: 4
OnNext: 5
OnCompleted
进一步说明
- observable 是数据流源,能产生数据项。
- subscribe 表示开始监听这个数据流,消费数据项和完成事件。
- RxCpp 模仿了 Rx 的核心概念,实现了响应式编程。
演示了如何在数据流中添加**变换(map)和过滤(filter)**操作。
代码解析:
cpp
auto subscription =
rxcpp::observable<>::range(1, 5) // 产生1到5的整数序列
.map([](int i) { return 10 * i; }) // 对每个元素乘以10,变成10, 20, 30, 40, 50
.filter([](int v) { return (v % 15) != 0; }) // 过滤掉能被15整除的元素(30, 45...)
.subscribe(
[](int v){ printf("OnNext: %d\n", v); }, // 每个满足条件的元素打印
[](){ printf("OnCompleted\n"); } // 序列结束时打印
);
具体流程
range(1,5)
生成数字序列:1, 2, 3, 4, 5map
操作将每个数字乘以 10,序列变成:10, 20, 30, 40, 50filter
操作去掉能被 15 整除的数字,30 被过滤掉,剩下:10, 20, 40, 50subscribe
打印每个剩余元素,最后打印"OnCompleted"
运行结果:
OnNext: 10
OnNext: 20
OnNext: 40
OnNext: 50
OnCompleted
小结
map
是对流中数据元素做转换(这里是乘10)。filter
是对流中数据元素做筛选,只保留满足条件的。- RxCpp 支持链式操作,代码简洁易读。
"Map Marble Diagram" 是用来直观展示响应式编程中map
操作符行为的图示,常见于 Rx(Reactive Extensions)文档和教学中。
什么是 Marble Diagram?
- **Marble Diagram(弹珠图)**是一种图形化表示数据流的方式,形象地显示事件随时间推移在流中的变化。
- 每个"弹珠"代表流中的一个数据元素,横轴表示时间轴。
Map 操作符的 Marble Diagram 具体含义
- 输入流:显示原始数据事件(比如数字或对象)沿时间顺序发出。
- Map 变换:对每个输入事件应用一个函数(如加倍、转换、映射到另一个值)。
- 输出流 :显示经过
map
变换后,新的数据事件流。
举例
假设输入流事件是:
--1---2---3---4---|
每个数字是时间顺序出现的事件。
map
操作将数字乘以 10:
--10--20--30--40--|
- 横线(--)表示时间流逝
- 数字代表事件值
- 竖线(|)表示序列完成
图示的价值
- 清楚地展示了
map
是如何一一对应地转换每个事件。 - 表明
map
不改变事件的数量和顺序,只改变事件的内容。 - 帮助理解响应式流的处理过程和时序。
展示了如何使用定时生成事件 、map转换 和combine_latest组合多个流 ,以及如何限制事件数量(take)并订阅消费。
cpp
auto o1 = rx::observable<>::interval(std::chrono::seconds(2));
auto o2 = rx::observable<>::interval(std::chrono::seconds(3));
auto values = o1.map([](int i) { return char('A' + i); }).combine_latest(o2);
values.take(10).subscribe(
[](std::tuple<char, int> v) { printf("OnNext: %c, %d\n", std::get<0>(v), std::get<1>(v)); },
[]() { printf("OnCompleted\n"); });
代码详细解析:
cpp
auto o1 = rx::observable<>::interval(std::chrono::seconds(2));
auto o2 = rx::observable<>::interval(std::chrono::seconds(3));
o1
:每隔2秒产生一个递增的整数事件(0,1,2,3,...)o2
:每隔3秒产生一个递增的整数事件(0,1,2,3,...)
cpp
auto values = o1
.map([](int i) { return char('A' + i); }) // 把o1的整数映射成字符:0->'A', 1->'B', 2->'C'...
.combine_latest(o2);
o1.map(...)
:把 o1 的数字事件转换成字符流('A', 'B', 'C', ...)。combine_latest(o2)
:将 o1(字符流)和 o2(整数流)两个流"组合",只要其中任一流发出新事件,输出一个包含最新两个流值的元组。
cpp
values
.take(10) // 只取前10个事件,避免无限流
.subscribe(
[](std::tuple<char, int> v) {
printf("OnNext: %c, %d\n", std::get<0>(v), std::get<1>(v));
},
[]() { printf("OnCompleted\n"); }
);
- 订阅组合流
values
,打印每次组合事件的两个值(字符和整数)。 - 流完成时打印 "OnCompleted"。
事件发生的时间和组合解释:
- o1每2秒产生字符 'A', 'B', 'C', ...
- o2每3秒产生整数 0, 1, 2, ...
combine_latest
会在任一流更新时发出最新的组合值。
举例:
| 时间 (秒) | o1 事件 | o2 事件 | 输出(combine_latest) |
| ------ | ----- | ----- | --------------------------------------- |
| 2 | 'A' | --- | ('A', latest o2 = no event yet, 默认是初始值) |
| 3 | --- | 0 | ('A', 0) |
| 4 | 'B' | --- | ('B', 0) |
| 6 | 'C' | 1 | ('C', 1) |
| 8 | 'D' | --- | ('D', 1) |
| 9 | --- | 2 | ('D', 2) |
| ... | ... | ... | ... |
总结
- interval 用于周期产生事件,适合定时任务模拟。
- map 转换事件内容。
- combine_latest 将多个流合并,输出最新的组合值。
- take 控制流的长度,避免无限。
- subscribe 监听事件,执行消费动作。
介绍了 Rx4DDS 的核心概念:
Rx4DDS = DDS + Rx
- DDS (Data Distribution Service) 是工业物联网中的实时数据通信标准。
- Rx (Reactive Extensions) 提供了基于观察者模式的响应式流编程模型。
Rx4DDS 是什么?
- Rx4DDS 把 DDS 产生的数据通过 Rx 的 Observable 模型封装起来。
- 它提供了针对 DDS 的响应式扩展绑定,支持多种语言:
- C++11
- C#
- JavaScript
核心思想
- 任何产生数据的 DDS 组件都可以视为一个 Observable(可观察对象) ,包括:
- Topics(数据主题)
- Discovery(参与者和主题发现事件)
- Statuses (QoS状态变化、连接状态等)
这样,你就可以用 Rx 的强大组合、过滤、变换等操作对 DDS 流进行响应式处理。
好处
- 将 DDS 的实时数据发布-订阅模型与响应式编程结合,简化异步数据流处理。
- 统一处理多源数据,提升工业物联网应用的灵活性和响应速度。
DDS for Distribution, Rx for Processing
- DDS(Data Distribution Service)负责数据分发,即把数据从发送者(DataWriter)传递到接收者(DataReader)。
- Rx(Reactive Extensions)负责数据处理,即对接收到的数据流做响应式操作,比如过滤、转换、组合。
关键角色对应关系
DDS 组件 | Rx 概念 |
---|---|
DataWriter | 生产数据的源头 |
DataReader | 订阅并读取数据 |
Observable | 将 DataReader 封装成可观察数据流 |
Observer | 订阅并处理 Observable 发出的数据 |
图示理解
- DataWriter 发布数据到 DDS 网络。
- 多个 DataReader 订阅这些数据。
- 每个 DataReader 被 Rx 封装成一个 Observable,这样就可以用 Rx 的各种操作来处理数据流。
- Observer 订阅 Observable,响应数据变化。
简言之,DDS 负责可靠分发数据,Rx 负责灵活处理数据流,两者结合实现工业物联网中的高效、响应式数据通信和处理。
对比了 DDS 和 Rx 两者的设计理念与特性,展示了为什么它们是完美搭配。
DDS 与 Rx:绝佳组合
DDS 特点
- 设计方法论:基于数据中心(Data-Centric)架构
- 松耦合 ,匿名发布-订阅模型:
- 发布者不知道订阅者是谁,订阅者不知道发布者是谁
- 数据流:Topic 表示类型为 T 的数据样本流
- 流的生命周期 :
- 实例(数据的键)有生命周期(新建 -> 活跃 -> 释放)
- 发布-订阅模型:发布者可以无视是否有订阅者,订阅者可以重复读取数据
Rx 特点
- 响应式、组合式编程:支持将多个异步事件流组合、转换、过滤等
- 内存中的 Observable/Observer 模式
- Observable 不知道谁订阅它,与 DDS 的松耦合理念相似
- Observable 表示类型为 T 的对象流
- Observable 的生命周期 :
- 有事件流(OnNext*),结束信号(OnCompleted)或错误信号(OnError)
- 热(Hot)与冷(Cold) Observable :
- 热 Observable 会主动产生数据,无论有没有订阅者
- 冷 Observable 只有订阅者订阅时才开始产生数据
两者相辅相成的理由
方面 | DDS | Rx |
---|---|---|
架构 | 数据中心,分布式发布-订阅 | 响应式,内存流处理 |
角色 | 发布者/订阅者,分布式匿名 | Observable/Observer,松耦合 |
流的生命周期 | 实例生命周期(新/活跃/销毁) | 事件流生命周期(OnNext/OnCompleted) |
数据流类型 | 持续数据样本流 | 按需产生的事件流 |
发布者与订阅者关系 | 发布者不知道订阅者 | Observable 不知道 Observer |
数据访问 | 订阅者可以多次读取相同数据 | 事件一旦产生推送给观察者 |
总结
- DDS 专注于跨网络可靠地分发数据,管理数据的生命周期和分布式状态。
- Rx 提供强大灵活的内存流数据组合与处理能力。
- 结合起来,Rx4DDS 让开发者能用响应式编程模型优雅地处理分布式实时数据流。
一个用于流处理演示的例子,结合了 DDS 的数据主题(Topics)和形状数据模型:
Stream Processing Demos with Shapes
- 三个 DDS 主题(Topics) :
"Square"
(正方形)"Triangle"
(三角形)"Circle"
(圆形)
- 数据类型定义:ShapeType 类
cpp
class ShapeType {
string color; // 作为键(@key),用于标识实例唯一性
int shapesize; // 形状大小
int x; // 形状位置的 x 坐标
int y; // 形状位置的 y 坐标
};
理解
- 每个主题对应一种形状类型的数据流,发布这类数据的发布者(DataWriter)会持续发送带有这些字段的实例数据。
color
字段作为 Key,用于唯一标识每个形状实例,比如"红色圆形"、"蓝色正方形"等。- 通过 DDS 的发布-订阅机制,订阅者(DataReader)能收到这些形状数据的实时更新,进行后续的流处理。
- 结合 Rx,用户可以用响应式操作处理这些形状数据流,比如筛选某种颜色、统计特定大小的形状,或者合并多种形状流。
这段代码用 Rx4DDS 和 RxCpp 实现了从 DDS 订阅一个名为 "Square"
的 ShapeType
主题,并将接收到的数据转换成响应式流进行处理。下面是详细分析:
代码整体
cpp
rx4dds::TopicSubscription<ShapeType> topic_sub(participant, "Square", waitset, worker);
rx::observable<LoanedSample<ShapeType>> source = topic_sub.create_observable();
rx::observable<ShapeType> square_track =
source
>> rx4dds::complete_on_dispose()
>> rx4dds::error_on_no_alive_writers()
>> filter([](LoanedSample<ShapeType> s) {
return s.info().valid();
}) // skip invalid samples
>> map([](LoanedSample<ShapeType> valid) {
return valid.data();
}); // map samples to data
逐行分析
1. 订阅 DDS 主题
cpp
rx4dds::TopicSubscription<ShapeType> topic_sub(participant, "Square", waitset, worker);
- 创建一个
TopicSubscription
对象,用来订阅 DDS 中名为"Square"
的ShapeType
主题。 - 参数:
participant
:DDS参与者(domain participant),表示通信的上下文。"Square"
:主题名称,订阅对应形状数据。waitset
:等待集,用于等待DDS事件通知。worker
:线程池或执行上下文,用于调度订阅事件的处理。
2. 创建可观察数据流
cpp
rx::observable<LoanedSample<ShapeType>> source = topic_sub.create_observable();
- 从订阅对象创建一个 RxCpp 的
observable
(可观察对象),其元素类型是LoanedSample<ShapeType>
。 LoanedSample<ShapeType>
包含DDS数据样本以及额外的元信息(如样本是否有效,时间戳等)。- 这一步把DDS的事件驱动数据源包装成了 Rx 的流。
3. 数据流转换与过滤
cpp
rx::observable<ShapeType> square_track =
source
>> rx4dds::complete_on_dispose()
>> rx4dds::error_on_no_alive_writers()
>> filter([](LoanedSample<ShapeType> s) {
return s.info().valid();
})
>> map([](LoanedSample<ShapeType> valid) {
return valid.data();
});
source
使用了管道式操作符(>>
)进行多个操作的组合:complete_on_dispose()
- 作用:当订阅者取消订阅(dispose)时,自动发出
OnCompleted
事件,完成流。
- 作用:当订阅者取消订阅(dispose)时,自动发出
error_on_no_alive_writers()
- 作用:如果没有任何活跃的发布者(DataWriter)在发送数据,流会发出错误(
OnError
)。
- 作用:如果没有任何活跃的发布者(DataWriter)在发送数据,流会发出错误(
filter(...)
- 作用:筛选流中的有效样本。DDS中样本可能失效(例如数据注销),此处只保留
s.info().valid() == true
的样本。
- 作用:筛选流中的有效样本。DDS中样本可能失效(例如数据注销),此处只保留
map(...)
- 作用:将
LoanedSample<ShapeType>
映射到ShapeType
,即只提取实际的业务数据,去掉元信息。
- 作用:将
总结
- 这段代码完成了从 DDS 主题数据接收、异常处理、有效样本筛选,到最终得到纯净的业务数据流的完整流程。
- 利用 RxCpp 的响应式组合,代码简洁且易扩展。
- 典型用途是工业物联网中实时流数据的响应式处理,适合复杂事件处理、监控和控制。
这段代码基于之前已经定义好的 square_track
(来自 DDS 的 Square 数据流),通过 map
和 tap
操作,将其转换为"Circle"的轨迹流 circle_track
,同时写回到 DDS。下面是逐行解释和理解:
全部代码回顾
cpp
int circle_degree = 0;
rx::observable<ShapeType> circle_track =
square_track
.map([circle_degree](ShapeType & square) mutable {
circle_degree = (circle_degree + 3) % 360;
return shape_location(square, circle_degree);
})
.tap([circle_writer](ShapeType & circle) mutable {
circle_writer.write(circle);
}); // tap replaced as publish_over_dds later
逐步分析
int circle_degree = 0;
- 初始化一个角度变量,用于控制"Circle"的旋转角度轨迹。
- 每次对 Square 事件做映射转换时,它会递增 3 度(模拟沿圆周移动)。
.map(...)
cpp
.map([circle_degree](ShapeType & square) mutable {
circle_degree = (circle_degree + 3) % 360;
return shape_location(square, circle_degree);
})
- 输入类型 :从上游的
square_track
得到一个ShapeType
实例(Square)。 - 功能 :
- 将 Square 数据转换为一个模拟圆形轨迹上的坐标。
shape_location(square, circle_degree)
:一个函数(假设是你写的)根据square
的颜色、大小,结合当前角度circle_degree
,生成一个新ShapeType
。circle_degree
持续增加,用于计算圆周位置。
mutable
使得捕获的circle_degree
可以在lambda
内部被修改。
.tap(...)
cpp
.tap([circle_writer](ShapeType & circle) mutable {
circle_writer.write(circle);
})
- 作用:在流的副作用中写入一个 Circle 到 DDS。
tap
(等价于 Rx 中的doOnNext
)是一个不改变流内容、但可用于副作用(side-effect)的操作。- 这里把转换后的 Circle 数据通过
circle_writer.write()
写出到 DDS 的"Circle"
主题。 - 实际项目中会被替换为
publish_over_dds
这种正式 API。
总结
操作 | 功能 |
---|---|
.map() |
把 Square 数据映射到一个沿着圆周轨迹移动的 Circle 数据 |
.tap() |
将这些 Circle 数据写回 DDS(副作用) |
circle_degree |
控制轨迹角度,实现流动效果 |
应用场景
- 工业可视化:模拟"机器或对象沿轨道运动"的数据。
- 流式数据变换与发布:Square → Circle 是典型的"派生主题"(Derived Topic)处理方式。
- 数据闭环处理:传感器数据经加工再写回,用于反馈或控制。
这段代码实现了一个将 "Circle" 轨迹转换为 "Triangle" 轨迹的 RxCpp + DDS 流处理流程 ,是对前面 square → circle
流的继续扩展,形成完整的数据管道(square → circle → triangle)。下面是详细解释和分析:
原始代码回顾
cpp
int tri_degree = 0;
rx::observable<ShapeType> triangle_track =
circle_track
.map([tri_degree](ShapeType & circle) mutable {
tri_degree = (tri_degree + 9) % 360;
return shape_location(circle, tri_degree);
})
>> rx4dds::publish_over_dds(triangle_writer);
triangle_track.subscribe();
逐行理解
int tri_degree = 0;
- 初始化一个角度变量
tri_degree
,控制 Triangle 的旋转角度轨迹。 - 每次触发时,会在之前的角度上增加 9 度,模拟三角形沿圆形路径移动。
.map(...)
cpp
.map([tri_degree](ShapeType & circle) mutable {
tri_degree = (tri_degree + 9) % 360;
return shape_location(circle, tri_degree);
})
- 作用 :将上游
circle_track
流中的ShapeType
(代表圆形)转换为新坐标的Triangle
。 shape_location()
是假设提供的一个函数,用于根据角度和原始形状生成一个新位置的形状。- 角度每次增加 9 度,使每个 Triangle 相对于上一个有新的位置。
这里和
circle_track
的+3
不同,+9
的步长使 Triangle 的轨迹更快地旋转一圈,表现出不同轨迹密度。
>> rx4dds::publish_over_dds(triangle_writer);
- 使用
rx4dds
提供的publish_over_dds()
运算符,将流中产生的 Triangle 数据发布到 DDS。 triangle_writer
是一个预先定义的DataWriter<ShapeType>
,对应 DDS 中的 "Triangle" 主题。- 这个操作是副作用性的,自动将每个流项写出。
.subscribe();
- 激活整个数据流(没有订阅就不会开始处理)。
- 因为
publish_over_dds
本身有副作用输出,不需要额外处理每个元素,空订阅即可触发处理链。
数据流总结
你现在的流式数据处理是这样:
Square (DDS)
↓
map(+3°) ➝ Circle
↓
map(+9°) ➝ Triangle
↓
publish_over_dds
每一步都通过 map()
改变形状在空间中的位置(角度),最终输出新的 DDS 主题数据,实现了典型的 派生数据管道处理模式。
优点总结
特性 | 描述 |
---|---|
函数式变换 | 使用 map 来计算新的数据位置,干净可读 |
副作用抽离 | publish_over_dds() 解耦了数据计算和数据发布 |
角度控制 | 实现周期性、轨道化的空间运动仿真 |
可组合性强 | 上游来源可以是任何 Observable,适合嵌入更复杂流程 |
可扩展建议
- 若你想加入状态过滤(如颜色匹配、大小限制)可在
.map()
前加.filter(...)
。 - 可以加入
.window()
或.buffer()
来处理形状批量更新。 - 可结合
rxcpp::observe_on()
或subscribe_on()
控制线程调度,提升性能。
skip_invalid_samples()
是一个 命名变换(Named Transformation) 的例子,它用 C++14 的泛型 lambda 表达了 RxCpp 中常见的函数式处理逻辑。我们来详细拆解这段代码的含义和用途。
原始代码分析
cpp
auto skip_invalid_samples()
{
return [](auto src_observable) {
return src_observable
.filter([](auto & sample) {
return sample.info().valid();
});
};
}
它做了什么?
这个函数定义了一个 Rx observable 的中间处理阶段 ,将所有不合法的数据样本(sample.info().valid() == false
)从流中过滤掉(skip)。
分解步骤如下:
- 返回值是一个 Lambda
skip_invalid_samples()
返回一个函数,它接受一个 observable(src_observable
)并对其应用一个.filter(...)
操作。 - 内部的 filter 条件
- 使用
sample.info().valid()
检查数据是否有效。 - 如果样本有效,就保留它;否则就丢弃。
- 使用
- 支持泛型类型
- 使用
auto
和泛型 lambda(C++14 功能),兼容所有含有info().valid()
方法的数据类型(如LoanedSample<T>
)。
- 使用
应用方式(链式组合)
你可以像这样将它插入你的 RxCpp 流:
cpp
auto square_track =
source
>> skip_invalid_samples()
>> map([](auto sample) { return sample.data(); });
这相当于:
cpp
auto square_track =
source
.filter([](auto &sample) { return sample.info().valid(); })
.map([](auto sample) { return sample.data(); });
好处是 把 filter 的逻辑封装成了可复用组件。
命名变换的优势
优点 | 说明 |
---|---|
可读性高 | 通过函数名清晰表达意图:跳过无效数据 |
模块化 | 将逻辑封装为可组合模块,利于维护 |
可重用 | 多个地方可以共享这个变换函数 |
链式兼容 | 与 RxCpp 的 >> 运算符完美集成 |
扩展建议
你可以定义多个这样的命名变换:
cpp
auto complete_on_dispose()
{
return [](auto o) { return o >> rx4dds::complete_on_dispose(); };
}
auto map_to_data()
{
return [](auto o) {
return o.map([](auto s) { return s.data(); });
};
}
然后串起来使用:
cpp
auto track = source
>> complete_on_dispose()
>> skip_invalid_samples()
>> map_to_data();
这段代码展示了在 C++11 中实现命名变换(Naming Transformation)的方式,特别是在不支持泛型 lambda(C++14 引入)的情况下,如何使用结构体 + operator()
来模拟函数式组合。
我们逐步解析:
目标
这段代码的目的是实现一个 RxCpp 变换操作器(transformation operator) ,用于把 LoanedSample<T>
类型的 observable 映射为 T
类型的 observable。
原始代码分析
cpp
struct MapSampleToDataOp
{
template <class Observable>
rx::observable<typename Observable::value_type::DataType>
operator ()(Observable src) const
{
typedef typename Observable::value_type LoanedSample;
return src.map([](LoanedSample & sample) {
return sample.data();
});
}
};
inline MapSampleToDataOp map_samples_to_data()
{
return MapSampleToDataOp();
}
逐步解释
结构体 MapSampleToDataOp
- 用于封装一个"变换器"。
- 提供了
operator()
,让它像函数一样调用。
operator ()(Observable src)
- 接受一个 RxCpp 的
observable<LoanedSample<T>>
作为输入。 - 使用
.map(...)
将每个LoanedSample<T>
转换为T
类型。
模板 template <class Observable>
- 支持任意类型的 observable,只要它的
value_type
有data()
方法。
类型推导
cpp
typedef typename Observable::value_type LoanedSample;
- 获取 observable 中元素的类型。
- 通常是
LoanedSample<ShapeType>
。
然后:
cpp
sample.data();
- 提取出里面的有效数据(即
ShapeType
)。
返回类型
cpp
rx::observable<typename Observable::value_type::DataType>
- 假设
LoanedSample<T>
中定义了DataType = T
,所以返回的是observable<T>
。
使用示例
使用时就像函数一样用它:
cpp
auto square_track = source
>> map_samples_to_data();
等价于:
cpp
auto square_track = source.map([](LoanedSample<ShapeType>& sample) {
return sample.data();
});
优点(在 C++11 下)
特点 | 描述 |
---|---|
命名变换 | 让业务逻辑的语义更清晰:map_samples_to_data() |
可组合 | 可以和其他 RxCpp 运算符一起使用(例如 >> 运算符) |
C++11 兼容 | 没用泛型 lambda,适用于老编译器 |
推荐组合
你可以定义更多这样的操作器,形成"DSL 风格"的流处理:
cpp
auto skip_invalid_samples = SkipInvalidSamplesOp();
auto complete_on_dispose = CompleteOnDisposeOp();
auto square_track = source
>> complete_on_dispose
>> skip_invalid_samples
>> map_samples_to_data();
提供的代码使用了 rx4dds
和 RxCpp
的 group_by_instance
操作符,将一个 DDS Topic
的流按颜色 (shape.color()
) 分组。这是一个典型的 分组流处理模式,适用于按实例(例如颜色、设备ID等)区分的实时数据流。
我们逐步解析你的代码和它的类型含义。
原始代码解读
cpp
rx4dds::TopicSubscription<ShapeType> topic_sub(participant, "Square", waitset, worker);
auto grouped_stream =
topic_sub.create_observable()
>> group_by_instance([](ShapeType & shape) {
return shape.color();
});
步骤解释:
- 创建订阅
topic_sub.create_observable()
返回一个rx::observable<LoanedSample<ShapeType>>
,表示实时接收到的样本流。 - group_by_instance(...)
这个操作是基于LoanedSample<ShapeType>
中的shape.color()
进行逻辑分组。每个颜色分一组。 - 结果:
grouped_stream
类型为:
cpp
rx::observable<rx::grouped_observable<
std::string, // 分组键:颜色
LoanedSample<ShapeType> // 分组内的元素类型
>>
decltype(grouped_stream)
等价解析
你给出的:
cpp
decltype(grouped_stream) ===
rx::observable<
rx::grouped_observable<
string,
LoanedSample<ShapeType>
>
>
是完全正确的。
这表示:
grouped_stream
是一个 observable,它每次发出的元素是一个grouped_observable
。- 每个
grouped_observable
对应一个颜色(如"RED"
)。 - 每个
grouped_observable
又是一个 observable,它发出某个颜色对应的所有LoanedSample<ShapeType>
。
group_by_instance 的作用场景
举个例子:
如果你订阅的是这样的 DDS 数据:
json
{ "color": "RED", "x": 100 }
{ "color": "BLUE", "x": 50 }
{ "color": "RED", "x": 120 }
{ "color": "BLUE", "x": 60 }
group_by_instance
会分出两个 grouped_observable
:
- 一个是 key = "RED",值流为:
{RED, 100} -> {RED, 120}
- 一个是 key = "BLUE",值流为:
{BLUE, 50} -> {BLUE, 60}
如何使用 grouped_observable
你可以对每组分别处理:
cpp
grouped_stream.subscribe([](auto group) {
std::string key = group.get_key();
group.subscribe([key](const auto& sample) {
std::cout << "Group " << key << ": " << sample.data().x << "\n";
});
});
总结
项目 | 含义 |
---|---|
group_by_instance |
按照 color() 对 ShapeType 进行逻辑分组 |
grouped_stream 类型 |
observable<grouped_observable<string, LoanedSample<ShapeType>>> |
每个分组的流 | 是 observable<LoanedSample<ShapeType>> |
用法 | 可以针对每个颜色分组设置独立的处理逻辑 |
这段代码展示了如何对 按颜色分组的 DDS 数据流 进行 Rx 风格的并行流处理 ,并通过 circle_writer
和 triangle_writer
将转换结果重新发布回 DDS 系统。
让我们逐步解析这段代码的逻辑、处理流程和每一步的作用。
背景回顾
grouped_stream
是一个rx::observable<grouped_observable<std::string, LoanedSample<ShapeType>>>
。- 每个
grouped_observable
代表一个颜色 (go.key()
是颜色名),它是一个独立的数据流(由同一个颜色的ShapeType
数据组成)。 - 本例中,针对每个颜色分组的流,执行了完整的数据清洗、变换、发布管道。
核心处理链 flat_map
cpp
grouped_stream
.flat_map([circle_writer, triangle_writer](GroupedShapeObservable go) {
...
return inner_transformed;
}).subscribe();
flat_map
的作用是:
- 对每个分组
go
执行一个转换操作,返回一个observable<ShapeType>
- 所有这些子 observable 被展开(flatten)成一个总 observable。
- 最终
.subscribe()
表示开始执行所有流的订阅。
inner_transformed 分析
cpp
rx::observable<ShapeType> inner_transformed =
go
>> to_unkeyed()
>> complete_on_dispose()
>> error_on_no_alive_writers()
>> skip_invalid_samples()
>> map_samples_to_data()
>> map_to_circle_track()
>> publish_over_dds(circle_writer, ShapeType(go.key()))
>> map_to_triangle_track()
>> publish_over_dds(triangle_writer, ShapeType(go.key()));
步骤解释:
操作符 | 含义 |
---|---|
to_unkeyed() |
把 grouped_observable 转为普通 observable,去除分组键包装 |
complete_on_dispose() |
当该分组被删除时,结束这个 observable |
error_on_no_alive_writers() |
如果 upstream 没有活跃 writer,抛出错误 |
skip_invalid_samples() |
过滤掉无效的 DDS 数据(比如 dispose 通知) |
map_samples_to_data() |
从 LoanedSample<ShapeType> 中提取 ShapeType 实际数据 |
map_to_circle_track() |
把 square 数据变换为 circle 移动轨迹(自定义逻辑) |
publish_over_dds(...) |
使用指定 writer 将数据重新发布到 DDS |
map_to_triangle_track() |
基于 circle 数据继续生成 triangle 轨迹 |
publish_over_dds(...) |
发布 triangle 数据到 DDS |
注意:ShapeType(go.key()) 是根据颜色初始化一个形状对象,作为发布模板。 |
整体数据流逻辑
(Square 数据, 按颜色分组)
↓
每组 (filter → map → 变换 → 发送到 DDS 的 Circle topic)
↓
(进一步变换 → 发送到 DDS 的 Triangle topic)
↓
所有结果合并成一个 observable
↓
subscribe() 开始处理
总结
组件 | 作用 |
---|---|
grouped_stream |
按颜色分组的 DDS 数据流 |
flat_map() |
并行处理每个颜色的数据子流 |
map_samples_to_data() |
从 DDS 样本中提取实际数据 |
map_to_circle_track() |
根据 square 位置生成 circle 的轨迹 |
map_to_triangle_track() |
再从 circle 生成 triangle 轨迹 |
publish_over_dds(...) |
将结果重新发回 DDS 网络 |
这段代码展示了 如何对多个按颜色分组的数据流(grouped_observable
)进行处理、合并、平均计算,并通过 DDS 发布结果。
我们分步骤详细解析每一层变换操作的含义及其背后的数据流处理思想。
整体目标
监听 Square 数据的不同颜色组,对每组:
- 清洗 → 解包 → 提取 → 合并 → 平均位置计算 → 发布到 Triangle Topic(颜色为 ORANGE)
分解代码结构与解释
cpp
grouped_stream
.map([](GroupedShapeObservable go) {
return go
>> rx4dds::to_unkeyed()
>> rx4dds::complete_on_dispose()
>> rx4dds::error_on_no_alive_writers()
>> rx4dds::skip_invalid_samples()
>> rx4dds::map_samples_to_data();
})
第一步:处理每个颜色子流
- 每个
GroupedShapeObservable
被处理为一个observable<ShapeType>
map()
返回一个observable<ShapeType>
的集合 ------ 所以你得到了observable<vector<observable<ShapeType>>>
cpp
>> rx4dds::coalesce_alive()
第二步:只保留"活跃"数据流
coalesce_alive()
会把这些observable<ShapeType>
过滤,保留有数据源活动的流。- 输出仍然是:
observable<vector<observable<ShapeType>>>
cpp
>> map([](const vector<rxcpp::observable<ShapeType>> & srcs) {
return rx4dds::combine_latest(srcs);
})
第三步:合并所有活跃颜色流为一个组合流
- 使用
combine_latest()
把多个 observable 合并为一个observable<vector<ShapeType>>
- 每次任何一个输入 observable 推送新值时,组合出的向量包含所有最新值
cpp
>> rxcpp::switch_on_next()
第四步:订阅合并后的 observable
switch_on_next()
订阅最新的observable<vector<ShapeType>>
,替换旧的- 输出变为:
observable<vector<ShapeType>>
cpp
>> rxcpp::map([](const std::vector<ShapeType> & shapes) {
return calculate_average(shapes);
})
第五步:平均位置计算
- 针对每次收到的
vector<ShapeType>
(所有颜色中最新的 sample),计算位置平均值或其他聚合结果 - 输出:
observable<ShapeType>
,每次代表平均形状位置
cpp
>> rx4dds::publish_over_dds(triangle_writer, ShapeType("ORANGE"));
最后一步:结果发布到 Triangle Topic,颜色固定为 "ORANGE"
- 每个计算得到的
ShapeType
被设置为"ORANGE"
,并使用triangle_writer
发布到 DDS - 最终输出:
observable<ShapeType>
(发布操作可能是 side-effect)
总体流图
Grouped Observables (Square by color)
│
map / filter / transform
↓
vector<observable<ShapeType>>
↓
combine_latest (multi-stream -> vector<samples>)
↓
switch_on_next
↓
observable<vector<ShapeType>>
↓
map → calculate_average
↓
publish_over_dds (color = "ORANGE")
总结表格
步骤 | 操作 | 输出类型 | 说明 |
---|---|---|---|
1 | .map() + rx4dds filters |
observable<vector<observable<ShapeType>>> |
清洗每个颜色组 |
2 | coalesce_alive() |
同上 | 保留活跃源 |
3 | combine_latest() |
observable<observable<vector<ShapeType>>> |
合并为一个组合流 |
4 | switch_on_next() |
observable<vector<ShapeType>> |
只订阅最新流 |
5 | map(calculate_average) |
observable<ShapeType> |
每次推送聚合数据 |
6 | publish_over_dds(...) |
observable<ShapeType> |
发布到 DDS,颜色为 "ORANGE" |
额外建议
如果你打算扩展这个架构:
- 可在不同颜色分组上应用不同算法
- 可将聚合操作变为滑动窗口处理(例如
.buffer_with_time()
) - 结合
rxcpp::window()
做批处理、速率限制等控制
对 在 C++11 中使用函数式编程(Functional Programming, FP)和 RxCpp 的一些现实反馈和建议,总结非常实用。下面是你提供内容的详细解释和分析:
你的观点:"My ¢2 on FP in C++11"
(即:我的一点关于 C++11 中函数式编程的看法)
1. Noisy!! --- 很"吵"
➤ 问题:
- C++11 的 lambda 写法相比其他语言非常冗长。
[]
,mutable
,->
, 类型推导等语法经常导致代码臃肿。
建议:
- 写法上尽量简洁,除非必要不加
mutable
- 若 lambda 太复杂,考虑提取为命名函数对象或
std::function
2. Lambdas don't extend local object lifetimes when captured
➤ 问题:
- 捕获变量不会自动延长其生命周期
- 导致常常需要用
std::shared_ptr
管理生命周期,防止 dangling references
建议:
- 避免引用捕获 (
[&]
) 除非完全必要 - 使用
std::shared_ptr
或std::unique_ptr
捕获资源(通过[sp = my_ptr]
) - 避免捕获局部变量用于异步场景(特别是 Rx 中)
3. Death by auto
➤ 问题:
- RxCpp 中 observable 的类型太复杂,用
auto
虽然简化代码,但让类型信息变得不可见 - 当出错时,编译器报错位置可能与出错点相距甚远,难以定位
建议:
-
如果你已经知道类型 ,就写出来,别用
auto
-
例如:
cpprxcpp::observable<ShapeType> square_track = ...;
比:
cppauto square_track = ...;
更具可读性和可调试性
4. Types are the best documentation
➤ 理念:
- C++ 的静态类型是最好的文档
- 明确写出类型可以让后来的开发者(甚至你自己)更快理解代码
5. RxCpp 的类型特别长
➤ 现实:
-
RxCpp 的模板类型组合太复杂,一层套一层:
cpprxcpp::observable< rxcpp::grouped_observable<std::string, LoanedSample<ShapeType>> >
-
你会经常看到这样的东西。
建议:
-
用
auto
只在局部逻辑明确时使用 -
或者封装常用组合为类型别名或函数封装
-
例如:
cppusing GroupedShapeObservable = rxcpp::grouped_observable<std::string, LoanedSample<ShapeType>>;
6. 使用 .as_dynamic()
可以隐藏类型,但注意性能
➤ 功能:
.as_dynamic()
会把 observable 类型转成运行时多态(类似虚函数调用)- 这能简化类型推导、让链式调用变轻松
代价:
.as_dynamic()
引入了额外间接调用,性能会受影响- 高性能实时场景(如 IIoT)中应避免滥用
总结建议清单
问题 | 建议 |
---|---|
lambda 太冗长 | 简化写法或提取为函数对象 |
lambda 捕获生命周期问题 | 使用 shared_ptr 捕获、避免 [&] |
auto 滥用 |
写出明确类型 |
类型太复杂 | 类型别名、分步骤写 |
.as_dynamic() 滥用 |
仅在开发调试或非性能关键路径使用 |
对 "Hot" 和 "Cold" Observable (热 / 冷 可观察对象)在 Rx / Reactive Programming 中的概念做了对比,以下是对这段内容的详细理解和解释:
Hot Observable(热流)
概念:
- 数据源会一直发射事件 ,不管有没有订阅者。
- 你加入得晚,你就会错过之前发射的事件。
- 就像直播:你打开晚了,只能看到未来的内容,过去的内容不会补给你。
特性:
- 主动推送(push)
- 和订阅者解耦:订阅者不能控制事件何时开始
- 多个订阅者共享同一个事件流(每个订阅者看到不同的"片段")
举例:
示例 | 描述 |
---|---|
Mouse movements | 鼠标在动,不管你看不看,它都在动 |
User input | 键盘敲击不停地发事件 |
Stock prices | 股票行情一直在变动 |
DDS Topic | 发布者持续发布 Topic 数据,不管订阅者是否存在 |
语义问题:
- 订阅者 必须跟上速度,否则会掉数据(丢帧)
- 常需要使用:sampling (抽样)、throttle 、buffer 来应对过载
Cold Observable(冷流)
概念:
- 事件源只有在订阅后才开始运行
- 每个订阅者都能从头开始看到完整事件序列
- 就像你点开一部电影,每个人都从第一秒看起
特性:
- 延迟执行,惰性(lazy)
- 每次订阅都是一个全新的实例
- 支持 背压(backpressure),即下游可以影响上游速率
举例:
示例 | 描述 |
---|---|
Reading a file | 每个订阅者都从头读取整个文件 |
Database query | 每次查询都是独立结果 |
observable.interval |
每个订阅者开始后才进入定时触发,时间线从0开始 |
对比总结
属性 | Hot Observable | Cold Observable |
---|---|---|
启动时机 | 程序启动就开始发射 | 每次订阅才开始发射 |
多订阅者 | 接收不同的片段 | 每个都从头接收完整数据 |
数据丢失风险 | 有,必须跟上或采样 | 没有,每次都有完整数据 |
常见场景 | 实时流、事件驱动 | 延迟数据处理、重放历史 |
应用建议(特别是 Rx + DDS)
- DDS Topics 是典型的 Hot Observable:不管有没有订阅者,数据都在发布。
- 如果你希望将 DDS 的数据流"缓存"或"重放",你需要把它转化为 Cold 流 ,或使用 ReplaySubject、Buffer 等技巧。
- 用 RxCpp 构建系统时,一定要意识到你的数据流是 Hot 还是 Cold,这会决定你是否需要做节流、缓冲或抽样处理。
将数据的"生成(生产)"与"消费(使用)分离" ,通过 Rx(Reactive Extensions)提供的 Observable(可观察流)抽象,你可以让代码更具灵活性与解耦性,特别适用于工业物联网(IIoT)场景中使用 DDS 的数据通信。
下面是详细解释:
核心概念:生产者-消费者分离
传统代码往往会把 "怎么获取数据" 和 "怎么处理数据" 写在一起(紧耦合),例如你用阻塞 read()
来获取数据后马上处理,这样做的问题是:
- 换一种获取方式(如异步监听)时,要大改代码结构;
- 不利于代码重用和测试;
- 无法灵活切换运行时策略(例如从 polling 切成 event-driven)。
在 DDS 中有两种数据访问(生产)方式:
方式 | 描述 | 类比 |
---|---|---|
1⃣ WaitSet-based | 程序主动"阻塞"等待某个条件触发(像 select() ) |
Polling 模式 |
2⃣ Listener-based | DDS 在数据到达时调用回调函数(事件驱动) | Event-driven 模式 |
各有利弊:
- WaitSet 更可控,但结构冗长;
- Listener 更实时,但容易乱成"回调地狱";
- 两者使用的 API 完全不同,代码可读性和维护性差异大。
解决方案:使用 Rx Observable
Rx 的好处是:你不关心数据是怎么来的(阻塞还是回调),你只专注于怎么"处理"数据。
例如:
cpp
rx::observable<ShapeType> shapes = topic_sub.create_observable();
shapes
.filter(...)
.map(...)
.subscribe(...);
不管 create_observable()
里实现用了 WaitSet 还是 Listener,只要返回一个 Observable,后面的消费链式处理逻辑都 不需要改变!
优势总结:
特性 | 好处 |
---|---|
生产-消费解耦 | 更灵活的代码架构 |
抽象统一 | 不同获取方式统一成 Observable |
更强复用性 | 同样的消费代码适用于不同数据源 |
更易维护和扩展 | 改变获取方式不用重写业务逻辑 |
声明式风格 | filter、map、tap 等链式操作一目了然 |
举例类比:
假设你要处理"Square"这个 Topic 的数据,不管你底层是:
- 监听数据到来(Listener);
- 用 WaitSet 轮询;
- 或者是模拟数据产生;
你只要写好这个数据消费逻辑:
cpp
shapes
.filter(is_valid)
.map(to_circle)
.subscribe(write_to_dds);
底层实现方式 完全不用关心 ,这就是 Rx 带来的 抽象与灵活性优势。
DDS(Data Distribution Service)和 Rx(Reactive Extensions)结合的契合点,具体阐释了两者在概念、类型及操作上的对应关系,方便理解如何用 Rx 的抽象来操作 DDS 的数据。
核心理解:DDS 和 Rx 如何匹配
DDS 概念 | Rx 对应概念 / 类型 / 操作 | 说明 |
---|---|---|
Topic of type T | IObservable | Observable 封装内部创建了对应的 DataReader 来读取 Topic 数据流。 |
Communication status / Discovery event streams | IObservable、IObservable | 这些 DDS 状态和发现事件,都可以映射为对应的 Observable 流。 |
Topic of type T with key type=Key | IObservable<IGroupedObservable<Key, T>> | 通过 Key(实例标识)对 Topic 流进行分组,分组后会产生多个分组 Observable。 |
Detect new instance / Dispose instance / Instance updates | 通过调用 IObserver<IGroupedObservable<Key,T>> 的 OnNext()、OnCompleted() 通知观察者 | 代表实例生命周期的事件通知(新增、销毁、更新)以 GroupedObservable 形式发布。 |
通知观察者新值 | IObserver.OnNext() | 新数据的发布通知,标准 Rx 的数据推送。 |
读取带历史 N 的数据 | IObservable.Replay(N) | Replay 操作符可缓存并重放最近 N 条数据,实现 DDS 中的"读历史"。 |
详细说明
- DDS 中的一个 Topic 是某种类型数据的流,Rx 用
IObservable<T>
来抽象这种数据流,方便用链式操作处理。 - DDS 的 数据状态和系统事件(如样本丢失、订阅者发现)也被封装为可观察的事件流,方便响应式处理。
- 针对带有 Key(实例标识)的 Topic,Rx 支持用
group_by
产生多个分组的 Observable,能分别处理每个实例的生命周期和数据更新。 - Rx 的通知机制(OnNext, OnCompleted) 与 DDS 的数据发布和生命周期通知高度契合,实现了自然的映射。
- Replay(N) 允许订阅者获取最新的 N 条历史数据,类似 DDS 支持的历史缓存。
总结
Rx 的可观察流(Observable)完美映射 DDS 的 Topic 和相关事件流。通过 Rx 的组合操作,可以优雅处理 DDS 数据的发布、实例管理及状态事件,极大简化工业物联网中实时数据流处理的代码复杂度。
比较 DDS 中的查询条件和多Topic操作 与 Rx 中对应的概念和操作,说明如何用 Rx 的操作符来实现 DDS 的数据查询和多数据流的联合处理。
核心理解:DDS 查询语法和操作对应到 Rx 的表达
DDS 概念(查询条件) | Rx 对应概念/类型/操作 | 说明 |
---|---|---|
Query Conditions(查询条件) | Rx 中的 Where(...) 或 GroupBy(...) |
DDS 查询中的过滤(WHERE)和分组条件可用 Rx 的过滤和分组操作符实现。 |
SELECT in CFT expression | IObservable<T>.Select(...) |
类似 SQL 的选择字段,用 Rx 的 Select 进行数据映射或变换。 |
FROM in CFT expression | DDSObservable.FromTopic("Topic1") |
从 DDS Topic 生成 Observable。 |
FROM keyed Topic | DDSObservable.FromKeyedTopic("Topic2") |
从带 Key 的 Topic 创建 Observable,支持实例分组。 |
WHERE in CFT expression | IObservable<T>.Where(...) |
过滤条件实现。 |
ORDER BY in CFT expression | IObservable<T>.OrderBy(...) |
排序操作(Rx 中通常不常用,但可以通过操作实现类似效果)。 |
MultiTopic (INNER JOIN) | IObservable<T>.Join(...).Where(...).Select(...) |
多个 Topic 之间的"内连接"通过 Rx 的 Join 操作符实现。 |
Join between DDS and non-DDS data | Join , CombineLatest , Zip |
支持 DDS 数据与非 DDS 数据流的组合与同步处理。 |
详细说明
- 查询条件对应关系 :
DDS 中的 SQL 类查询语法(SELECT、FROM、WHERE、ORDER BY)对应到 Rx 的函数式链式调用:Where
对应过滤条件(WHERE)Select
对应字段选择与数据映射(SELECT)FromTopic
创建数据流(FROM)GroupBy
用于分组处理带 Key 的 TopicOrderBy
用于排序(尽管 Rx 中排序不常见,需要额外实现)
- 多 Topic 联合处理 :
- DDS 允许多个 Topic 的数据做 INNER JOIN 操作,Rx 通过
Join
,CombineLatest
,Zip
等操作符实现数据流之间的联合、同步和组合,支持跨源(DDS 和非 DDS)的数据流融合。
- DDS 允许多个 Topic 的数据做 INNER JOIN 操作,Rx 通过
总结
DDS 的查询和多数据源联合操作可以借助 Rx 的丰富操作符优雅实现,提升数据流处理的灵活性和表达力。使用 Rx,我们可以用声明式函数链式写法轻松替代传统的复杂查询语法,实现实时、高效、组合的数据处理。