DDS-XTypes v1.3 解读-中 7. Extensible and Dynamic Topic Types for DDS7.2 Type System (类型系统)7.2.1 Introduction (引言)【翻译】本节定义了本规范所提议的类型系统的具体模型。该模型完全独立于任何特定的程序设计语言或序列化格式数据表示。本类型系统在设计上支持以下关键特征强类型安全Strong Type Safety支持静态的编译期类型匹配。动态类型内省Dynamic Type Introspection支持在没有编译期知识的情况下分析类型结构。类型演进与向前/向后兼容性Type Evolution允许类型在不中断现有通信的前提下进行扩展。【白话解释】XTypes 强调它的类型系统只是一种纯粹抽象的“数学模型”。不管你是写 C、Python 还是 Rust也不管你在网络上是用 CDR 二进制还是 XML 文本传输在 XTypes 的世界里它们都必须被映射到这一套统一的抽象类型模型中。7.2.2 Type System Model (类型系统模型)这里是 XTypes 规定系统里到底能有哪些数据类型的核心定义。在 XTypes 模型中所有类型被划分为两大类基本类型 (Primitive Types)和构建类型 (Constructed Types)。1. 基础/原始类型 (Primitive Types)【翻译】原始类型是不可再细分的基本构建块。它们是构成所有复杂数据类型的基石。本类型系统支持以下原始类型类型名称 (Type Name)描述 (Description)标准对齐边界 (标准 CDR 对齐字节)boolean布尔值 (true / false)1byte/octet8 位无符号整数 (通常表示原始字节)1char8/char8 位字符 (ISO Latin-1)1char16/wchar16 位字符 (UTF-16)2int16/short16 位有符号整数2uint16/unsigned short16 位无符号整数2int32/long32 位有符号整数4uint32/unsigned long32 位无符号整数4int64/long long64 位有符号整数8uint64/unsigned long long64 位无符号整数8float32/float32 位单精度浮点数 (IEEE-754)4float64/double64 位双精度浮点数 (IEEE-754)8float128/long double128 位高精度浮点数8 或 16【技术贴士】这里定义的对齐边界就是我们在最开始讨论 Padding 时起决定性作用的基准。一旦在复杂结构体中遇到这些基础类型序列化引擎就会自动按照表格里的字节数进行内存对齐。2. 构建类型 (Constructed Types)【翻译】构建类型是通过组合原始类型或其他构建类型而形成的复杂类型。本规范定义了以下几种构建类型A. 集合类型 (Collection Types)集合类型用于容纳一组相同类型的元素称为元素类型Element Type。数组 (Array):【翻译】拥有固定维度和大小的多维集合。数组中的元素个数在编译期就已确定。【白话】如long my_arr[10][20]。它是固定长度Fixed-Length的。在内存和网络中连续排布对齐规则与单个元素相同性能极高。序列 (Sequence):【翻译】动态大小的一维集合。序列可以被指定一个最大容量限制Bounded Sequence也可以是不限长度的Unbounded Sequence。【白话】类似于 C 中的std::vector。它是非固定长度Variable-Length的。在传输时它的头部必然会带有一个 4 字节的“长度头Length”在计算 Padding 时这个长度头必须首先对齐 4 字节。映射/字典 (Map):【翻译】由关联的键值对Key-Value Pairs组成的集合。在 1.2/1.3 规范中也得到了标准化支持。B. 聚合类型 (Aggregation Types)聚合类型允许将不同类型的多个字段组合成一个单一实体。结构体 (Structure / Struct):【翻译】包含一组具名成员Members的类型每个成员都可以是不同的类型。【XTypes 特性】XTypes 极大地增强了 Struct。可扩展Extensible可以通过在末尾追加字段来升级结构体。可选字段Optional结构体成员可以通过optional修饰如果该成员为 null则在序列化时不占用任何内容空间后面我们会学到它如何通过 Bitmask 标记省略。共用体 (Union):【翻译】包含一个判别器Discriminator和一组可选成员的类型。在任一时刻共用体中只有一个成员是有效激活的其具体激活哪个成员由判别器的值来决定。C. 别名与枚举 (Enumeration Alias)枚举 (Enumeration):一组具名的整型常量的集合如enum Color { RED, GREEN, BLUE }。别名 (Alias / Typedef):为已有的类型起一个新名字如typedef long MyUserId;。 这一节的技术承接关系请注意这些类型划分是如何完美对应上我们之前探讨的对齐Padding和拷贝方式的原始类型 (Primitive) 数组 (Array) 仅含定长成员的结构体在 DDS 中归为Fixed-Length Types。➡️ 协议栈在反序列化时直接触发最快速的直接内存拷贝 (memcpy)机制。序列 (Sequence) 字符串 (String) 带有optional的结构体在 DDS 中归为Variable-Length Types。➡️ 协议栈必须调用反序列化引擎Parser老老实实通过“读长度、分内存、跳过 Padding 字节”的动态解析模式来进行数据还原。如果这一段关于类型模型的抽象定义已经理顺我们接下来可以开始深入研读 XTypes 最精彩的兼容性设计核心7.2.3 节 Type Extensibility and Mutability (类型的可扩展性与可变性规则定义 FINAL / APPENDABLE / MUTABLE 的区别)。7.2.3 Type Extensibility and Mutability (类型的可扩展性与可变性)7.2.3.2 深度技术拆解这三种修饰符通常在 IDL 中以final、appendable、mutable标注在底层彻底决定了网络报文怎么打包对齐与 Padding以及版本兼容性1. FINAL —— 绝对静止零扩展性定义这个类型一旦定义任何人都不能再改动。如果非要改动即使只改动一个字节也会被 DDS 视为完全不同的两种类型双方直接断开连接。对齐与序列化机制* 采用PLAIN_CDR格式序列化。它是我们前面提到的极致性能通道。所有的字段在网络字节流里都是“人挨着人”紧密排列的。协议栈可以直接根据偏移量在内存中进行Direct Copy (memcpy)完全没有任何协议头Header开销。应用场景适用于追求极致吞吐量和低延迟且绝对不会发生变化的物理量。例如三维空间坐标struct Point3D { float x; float y; float z; };。2. APPENDABLE —— 只许追加向前兼容定义允许在数据结构的末尾追加新的字段。但是原有的字段不能删除、不能换顺序、不能改类型。对齐与序列化机制在 XTypes 1.2/1.3 中APPENDABLE 类型默认采用DELIMITED_CDR格式进行序列化。底层的 Padding 规则会发生微妙变化* 为了实现兼容DELIMITED_CDR 会在结构体序列化时在头部或者内部嵌入一个4 字节的“长度指示器Length”。接收方收到数据后如果发现发送方的版本比自己新数据包更长它可以根据这个“长度指示器”安全地跳过末尾那些它看不懂的新增字段只解析自己懂的前半部分。这就意味着反序列化时它不能再无脑进行一整块memcpy而是需要先读取长度并在遇到未知追加字段时将指针移开。应用场景大多数汽车或工业控制的数据帧如传感器数据报文。今天只有 5 个参数明天可能需要加第 6 个。3. MUTABLE —— 彻底自由完全动态定义这是最灵活、但也最复杂的模式。你可以随意删除字段、给中间插入新字段、调整顺序或者把某个字段设为可有可无optional。对齐与序列化机制采用PL_CDR (Parameter List CDR)格式进行序列化。在网络流中原有的 Padding 和紧凑排布规则不复存在数据不再按照“一个接一个”的物理顺序排布。每个字段在发送时都会被包裹成一个TLV (Tag-Length-Value)结构Tag (2 字节):该字段的 Member ID。Length (2 字节):该字段实际占用多少字节。Value:实际数据。每一个这样的小包裹Member Header都需要进行 4 字节的对齐填充。反序列化行为接收方的协议栈完全变成了像解析 JSON 一样的“字典查询”过程。它拿着每一个 Member ID 去匹配对得上的就解析并存入内存对不上的比如别人删掉的或者自己没有的直接忽略。应用场景大型的、需要高频迭代演进的业务配置报文或者是跨部门联合开发、字段经常变更的复杂业务数据。 总结你可以用下面这张表格来记忆它们在反序列化性能与兼容性之间的终极权衡修饰符网络封装格式兼容性能力反序列化手段性能开销finalPLAIN_CDR无改动即失联直接内存拷贝 (memcpy)极低零开销appendableDELIMITED_CDR允许末尾追加新字段局部解析 长度跳转中等mutablePL_CDR允许任意增删改乱序逐个解析 TLV字典查找7.3.2 深度技术拆解什么是 TypeObject它在网络中扮演什么角色在前几章中我们多次提到接收方在收到变长或MUTABLE数据时需要“对照本地的类型蓝图TypeSupport”。如果发布方和订阅方是同一个人写的两边都有相同的*.idl文件这当然没问题。但如果是动态系统或者两边的结构体版本不一样比如发布方追加了字段接收方在运行期去哪里找这个“全新的类型蓝图”呢这就是TypeObject的高光时刻。1. TypeObject 的本质数据结构的“二进制终极压缩包”TypeObject 是把你的 IDL 结构体包括它的名字、有哪些字段、每个字段的类型、每个字段的Member ID、以及它是 final 还是 mutable整体进行了一次二进制序列化。也就是说它把“结构体定义本身”变成了网络上的一串byte数组。2. TypeIdentifier —— 它的“孪生数字指纹”因为完整的 TypeObject 包含了结构体的所有细节如果结构体很大这个二进制包也会很大。如果在网络中疯狂广播它会把带宽吃满。 所以规范给它配了一个孪生兄弟TypeIdentifier类型标识符。协议栈会对 TypeObject 进一次 Hash 计算比如 SHA-256生成一个固定长度的、极小的数字指纹通常只有几十个字节。这个指纹在全局互联网中是唯一的。只要结构体改了一个符号指纹就会彻底改变。7.3.3 ⚔️ 连环招式在网络发现Discovery阶段它们是怎么配合的还记得我们在 2.2 节讲网络互操作性时提到的Minimal Profile和Basic ProfileTypeLookup 服务吗它们在这里完美落地了场景一个新节点发布者带着新结构体加入了网络第一步打招呼发布者向局域网广播自己的存在。在它的 RTPS 发现报文里它只带上了体积很小的TypeIdentifier指纹。第二步接收方对暗号接收方收到报文看了一眼这个指纹查了一下自己的本地数据库情况 A见过“哦这个指纹我认识我本地有对应的 IDL 蓝图。” 接收方直接走向我们 7.2.4 节学的类型兼容性比对算法。情况 B没见过“等等这个指纹是个什么鬼我本地查不到它的结构体蓝图”第三步触发 TypeLookup 服务 —— Basic Profile 的大招接收方立刻通过网络向发布者发送一个点对点的请求“兄弟我本地没有TypeIdentifier 0xABC...的蓝图请把你的完整TypeObject发给我”发布者把藏在内存里的TypeObject 二进制压缩包发给接收方。第四步运行时解包接收方收到 TypeObject 后在内存中将其解开。此时即使接收方在编译期从来没见过这个结构体它在运行期也瞬间秒懂了“哦原来这个新类型包含 3 个字段ID 0 是 longID 1 是 string...”第五步建立连接有了完整的蓝图接收方就可以用DynamicDataAPI 去动态解析发布者随后发过来的真实数据流了。 总结7.3 节的价值在于它打破了传统通信中“数据和结构体必须在编译期死死绑定”的魔咒。IDL是留给程序员看的和写代码的。TypeObject是留给网络协议栈在运行时Runtime互相交流、动态认亲用的。有了 TypeObjectDDS 才真正变成了一个“活的”、具备自我进化能力的分布式文件/数据网络。这就是 7.3 节关于类型表示法的核心逻辑。如果这里理顺了下面我们就要进入全篇最硬核、涉及最多 Padding 算术题和虚拟机指针挪动的一章 ——7.4 Data Representation数据表示法即 Extended CDR v1/v2 的物理排布规则。现在我们终于来到了整篇规范中技术最硬核、跟你在代码里调试字节流Byte Stream最直接相关的章节 ——7.4 Data Representation (数据表示法)。这里就是规定真实的用户数据Payload在网络上有线传输Wire Format时字节到底怎么排、Padding 怎么补、封装头怎么填的物理世界。以下是7.4 节的规范原文严格翻译与底层深度拆解7.4 Data Representation (数据表示法)7.4.2 深度技术拆解三种物理排布的大对撞这一节的干货全部集中在这些二进制流在网线上跑的时候指针究竟是怎么挪动的。我们用最底层的“物理视角”来剖析它们1. PLAIN_CDR —— 裸数据狂奔性能的终极追求如果你的结构体被标记为了final它在网络上的排布干净得让人发指。没有名字没有 ID没有长度头。比如一个struct { int32 a; int16 b; }网络流里只有[a的4字节] [b的2字节]。对齐规则严格遵循基础 CDR。解析它只需要知道本地结构体的偏移量指针一路memcpy过去速度是纳秒级别的。硬伤只要发送方少发了或多发了 1 个字节接收方的指针就会错位后面所有的数据全部变成乱码也就是我们常说的“反序列化崩溃”。2. DELIMITED_CDR —— 带安全边界的追加Encoding V2 的精髓当类型变成appendable时为了允许末尾加字段数据流就不能再这么“无脑裸奔”了。它引入了解包安全网。在Extended CDR (Encoding version 2)中它的物理排布长这样结构体头部会塞入一个4 字节的整型Int32里面记录了“这个结构体包括追加的所有字段实际占用了多少字节的长度”。反序列化虚拟机的行为接收方老版本首先读取这 4 字节的长度指示器。接着按照它自己知道的字段去老老实实解析。解析完自己认识的所有字段后老版本接收方发现指针才走了 20 字节但头部的长度指示器说“这个包总长 28 字节”。关键一步接收方不需要报错它直接把指针向后强行平移 8 个字节$28 - 20 8$直接跳过它不认识的那些追加字段。对齐代价那个 4 字节的长度指示器本身需要按 4 字节对齐。并且结构体内部的所有对齐都是相对于这整个网络流的绝对起始位置来计算的。3. PL_CDR (Parameter List CDR) —— TLV 碎皮包动态演进的极致当类型是mutable时对齐规则彻底放飞自我变成了互联网中最常见的TLV (Tag-Length-Value)字典。在网络流中数据不再连贯而是碎成了一个个的“参数块Parameter”┌──────────────────┬──────────────────┬─────────────────────────┐ │ Member ID (2字节) │ Length (2字节) │ Value (实际数据值) │ └──────────────────┴──────────────────┴─────────────────────────┘Padding 规则发生了大变动在以前的PLAIN_CDR里如果一个int162字节后面跟着一个int324字节中间必须补 2 个字节的 Padding让int32走到 4 的倍数地址。但在PL_CDR里因为每个字段前面都挡了一个4 字节的固定头部2字节 ID 2字节 Length这个头部本身已经把地址强行对齐到了 4 的倍数。PID_SENTINEL (结束哨兵)当所有字段发完后流的末尾必须追加一个特殊的 Member ID通常是0x0001称为 Sentinel用来告诉接收方“字典已经翻完了到此结束。”7.4.3 Encapsulation Header (封装头) —— 网络层的“红绿灯”接收方在拿到这一大串字节流时它是怎么知道该用上面哪种规则去拆包的XTypes 规范规定所有用户数据 Payload 的最前面 4 个字节必须是封装头Encapsulation Header。这 4 个字节的分工极其明确前 2 字节 (Options/Identifier):声明使用的编码格式。0x00 00/0x00 01: 告诉接收方这是PLAIN_CDR大端/小端。0x00 02/0x00 03: 告诉接收方这是PL_CDR大端/小端。0x00 06/0x00 07: 告诉接收方这是 Encoding V2 的DELIMITED_CDR。后 2 字节 (Options/Padding):记录了一些特殊的标志位或者用来做这 4 字节头本身的 Padding 填充确保后面的真实数据依然从 4 字节对齐的边界开始。 总结7.4 节是把 XTypes 前面所有的理论可扩展性、类型系统真正翻译成机器语言的一章。当你在网络抓包如用 Wireshark时你看到的 Data Submessage 里的前 4 个字节比如0x00 06 00 00就是 7.4 节规定的封装头。只有读懂了这 4 个字节你和你的协议栈才能决定接下来是应该直接memcpy针对PLAIN_CDR还是先读 4 字节看长度针对DELIMITED_CDR亦或是老老实实当成字典去拆解 TLV针对PL_CDR。到这里数据在网络上怎么传、怎么排就彻底闭环了。接下来我们将进入规范的最后一包 ——7.5 Language Binding (语言绑定看看 Plain 静态绑定和 Dynamic 动态反射绑定在代码层面是如何提供给程序员使用的)。准备好去看看最后的应用层代码长什么样了吗