1. 概述
1.1 CDR定义
CDR (Common Data Representation) 是由OMG (Object Management Group) 制定的二进制数据序列化标准 ,定义了如何将IDL (Interface Definition Language) 描述的数据结构转换为平台无关的字节流,以及如何从字节流恢复原始数据结构。
与其他序列化格式不同,CDR从设计之初就专门针对分布式实时系统优化,具有以下核心优势:
- 跨平台:自动处理不同硬件架构的字节序和对齐差异(大小端)
- 跨语言互操作:支持C++、Java、Python、C#等所有主流编程语言
- 高性能:二进制编码,无冗余元数据开销,接近内存拷贝速度
- 强类型安全:基于IDL静态类型检查,避免运行时类型错误
- 标准性:唯一的国际标准二进制序列化格式,保证不同厂商产品的互操作性
1.2 标准演进
CDR标准经历了三次重大演进,每次都在性能和灵活性之间取得了更好的平衡:
| 版本 | 标准来源 | 发布时间 | 核心特性 | 主要应用 |
|---|---|---|---|---|
| 原始CDR | OMG formal/02-06-51 (CORBA 3.0) | 2002年 | 基础类型与复合类型支持,对齐规则定义 | CORBA,早期DDS |
| XCDR v1 | OMG DDS-XTypes v1.0 | 2010年 | 扩展类型支持,参数化CDR (PL_CDR),可选成员 | DDS v1.4,ROS2 Humble及更早版本 |
| XCDR v2 | OMG DDS-XTypes v1.3 | 2018年 | 64位类型4字节对齐,DELIMITED_CDR,PL_CDR2,wchar优化 | DDS v1.6,ROS2 Iron及以后版本 |
1.3 主要应用领域
CDR已经成为以下领域的标准序列化格式:
- DDS (Data Distribution Service):所有DDS实现的默认序列化格式
- ROS2 (Robot Operating System 2):机器人领域最流行的通信协议
- 工业自动化:OPC UA PubSub、工业以太网等实时控制系统
- 车载通信:AUTOSAR Adaptive平台的SOME/IP与DDS融合
- 航空航天:高可靠性实时数据交换系统
2. 三大设计原则
2.1 字节序自动处理机制
字节序是指多字节数据在内存中的存储顺序,不同硬件架构使用不同的字节序:
- 大端序 (Big-Endian):最高有效字节存储在最低地址(网络字节序)
- 小端序 (Little-Endian):最低有效字节存储在最低地址(x86/x86_64架构)
CDR字节序处理规则:
- 每个CDR封装流的开头包含一个字节序标志位:
0表示大端序,1表示小端序 - 发送方可以选择任意字节序进行序列化(通常选择本机字节序以提高性能)
- 接收方必须根据标志位自动进行字节序转换
- 在RTPS协议中,字节序标志位于DATA子消息的标志位中
示例 :32位整数0x12345678的字节表示
- 大端序:
0x12 0x34 0x56 0x78 - 小端序:
0x78 0x56 0x34 0x12
2.2 自然边界对齐规则
CDR要求所有数据类型必须按其自然边界对齐,这是为了匹配大多数CPU的内存访问模式。如果数据没有对齐,CPU可能需要进行两次内存访问才能读取完整的数据,严重影响性能。
- 对齐 = 数据的起始地址必须是某个数的整数倍
- 凑 = 不够就往前面塞空字节(填充),强行凑到要求的位置
1 字节对齐:随便放
2 字节对齐:只能放 2、4、6、8... 号位
4 字节对齐:只能放 4、8、12、16... 号位
8 字节对齐:只能放 8、16、24、32... 号位
原始CDR对齐规则
| 数据类型 | 大小(字节) | 对齐要求(字节) |
|---|---|---|
| char/octet/boolean | 1 | 1 |
| wchar | 4 | 4 |
| short/unsigned short | 2 | 2 |
| long/unsigned long/float | 4 | 4 |
| long long/unsigned long long/double | 8 | 8 |
| long double | 16 | 16 |
| string/sequence | 4(长度域) | 4 |
XCDR v2对齐优化
XCDR v2对对齐规则进行了重大修改,这是XCDR v2相比XCDR v1最显著的性能提升:
int64/uint64/float64/double从8字节对齐改为4字节对齐wchar从4字节(UTF-32)改为2字节(UTF-16)- 这一改变平均减少了15-30%的序列化后数据大小,特别是在包含多个64位类型的结构体中
对齐示例:
idl
struct Example {
short a; // 2字节
long b; // 4字节
char c; // 1字节
double d; // 8字节
};
- 原始CDR总大小:2 + 2(填充) + 4 + 1 + 7(填充) + 8 = 24字节
- XCDR v2总大小:2 + 2(填充) + 4 + 1 + 3(填充) + 8 = 20字节(注意:double现在4字节对齐,省去了4字节的填充)
2.3 无自描述性设计
CDR是一种无自描述性 的序列化格式,这意味着字节流中不包含任何类型元数据信息 。通信双方必须预先通过IDL达成类型共识,接收方必须使用与发送方完全相同的类型定义才能正确解析。
这种设计的优缺点非常明显:
- 优点:极致的性能和最小的数据大小,没有任何元数据开销
- 缺点:灵活性较差,类型变更需要双方同时升级
3. 数据类型系统
CDR支持所有OMG IDL定义的数据类型,这些类型可以分为基本类型、复合类型和特殊类型三大类。
3.1 基本数据类型
基本类型是所有复合类型的基础,它们的序列化格式是固定的:
"序列化格式" 列的大小,是数据本身的大小,不包括任何填充字节
| IDL类型 | 描述 | 序列化格式 |
|---|---|---|
boolean |
布尔值 | 1字节,0=false,1=true |
char |
8位字符 | 1字节,ISO 8859-1编码 |
wchar |
宽字符 | XCDR v1:4字节(UTF-32),XCDR v2:2字节(UTF-16) |
octet |
8位无符号整数 | 1字节 |
short |
16位有符号整数 | 2字节,按指定字节序 |
unsigned short |
16位无符号整数 | 2字节,按指定字节序 |
long |
32位有符号整数 | 4字节,按指定字节序 |
unsigned long |
32位无符号整数 | 4字节,按指定字节序 |
long long |
64位有符号整数 | 8字节,按指定字节序 |
unsigned long long |
64位无符号整数 | 8字节,按指定字节序 |
float |
32位浮点数 | 4字节,IEEE 754标准 |
double |
64位浮点数 | 8字节,IEEE 754标准 |
long double |
128位浮点数 | 16字节,IEEE 754标准 |
3.2 复合数据类型
复合类型由基本类型或其他复合类型组成,是实际应用中最常用的类型。
3.2.1 结构体 (Struct)
- 成员按照声明顺序依次序列化
- 每个成员都必须满足其自身的对齐要求
- 结构体整体对齐要求等于其最大成员的对齐要求
- 结构体末尾可能需要填充字节以满足整体对齐
3.2.2 联合 (Union)
- 首先序列化判别符 (discriminator)(整数、字符或枚举类型)
- 然后序列化与判别符对应的成员
- 对于可变联合,还需要序列化成员头信息
3.2.3 枚举 (Enum)
- 序列化为其底层整数类型
- 默认底层类型为
long(32位) - XCDR v2支持指定更小的底层类型以节省空间
3.2.4 数组 (Array)
- 固定长度数组直接序列化所有元素
- 元素按照数组顺序依次排列
- 每个元素都必须满足其自身的对齐要求
3.2.5 序列 (Sequence)
- 首先序列化一个
unsigned long类型的长度域(表示元素个数) - 然后序列化指定数量的元素
- 空序列的长度域为
0
3.2.6 字符串 (String)
- 首先序列化一个
unsigned long类型的长度域 - 然后序列化字符串内容,包括末尾的空字符
'\0' - 长度域包含空字符的长度,因此空字符串的长度域为
1
3.3 特殊数据类型
3.3.1 Any类型
- 可以存储任意IDL类型的值
- 序列化格式:类型码(TypeCode) + 实际值
- 是CDR中唯一包含元数据的类型
3.3.2 值类型 (Value Type)
- 支持继承和多态
- 序列化格式:类型标识 + 成员数据
- 类型标识用于在接收方确定具体的派生类型
4. 序列化格式演进
CDR标准定义了四种主要的序列化格式,分别适用于不同的可扩展性需求。
4.1 PLAIN_CDR
- 最基础的序列化格式
- 没有额外的头部信息
- 成员按照声明顺序连续排列
- 不支持类型扩展和可选成员
- 序列化效率最高
- 适用于
FINAL类型
4.2 PL_CDR (Parameterized CDR)
- XCDR v1引入,用于
MUTABLE类型 - 每个成员前面都有一个成员头
- 成员头包含成员ID和成员序列化长度
- 成员可以按照任意顺序排列
- 接收方可以忽略未知成员
- 支持可选成员
4.3 DELIMITED_CDR
- XCDR v2引入,用于
APPENDABLE类型 - 在PLAIN_CDR2编码前添加一个DHEADER (Delimiter Header)
- DHEADER包含字节序标志和后续数据的长度
- 接收方可以跳过整个对象而不需要解析其内容
- 支持向后兼容:旧版本可以读取新版本的消息(忽略新增成员)
4.4 PL_CDR2
- XCDR v2引入,用于
MUTABLE类型 - 在PL_CDR基础上增加了DHEADER
- 每个成员前面都有一个EMHEADER (Extended Member Header)
- EMHEADER包含成员ID、必须理解标志和成员长度
- 必须理解标志用于指示接收方是否必须理解该成员
- 支持向前和向后兼容
- 灵活性最高,但序列化效率最低
5. 封装格式详解
CDR字节流的封装格式根据应用场景略有不同,主要有以下三种:
5.1 标准CDR封装
用于CORBA和早期DDS实现:
+-----------------+-------------------------+
| 字节序标志(1字节) | 实际数据(按CDR规则编码) |
+-----------------+-------------------------+
- 实际数据从偏移1开始,所有对齐都相对于偏移1
5.2 DDS CDR封装
DDS使用的封装格式在标准CDR基础上增加了版本信息:
+-----------------+-----------------+-------------------------+
| 字节序标志(1位) | 版本(7位) | 选项(8位) |
+-----------------+-----------------+-------------------------+
| 实际数据(按CDR规则编码) |
+-------------------------------------------------------------+
- 版本字段:
0x00=XCDR v1,0x01=XCDR v2 - 实际数据从偏移2开始,所有对齐都相对于偏移2
5.3 DHEADER格式
用于XCDR v2中的DELIMITED_CDR和PL_CDR2:
+-----------------+-------------------------------------------+
| 字节序标志(1位) | 数据长度(31位) |
+-----------------+-------------------------------------------+
- 数据长度表示后续数据的字节数,不包括DHEADER本身
6. 可扩展机制
XCDR最强大的特性之一就是其完善的可扩展性机制,它允许类型在不破坏现有通信的情况下进行演进。
6.1 三种可扩展性类型
XCDR定义了三种可扩展性类型,通过IDL注解指定:
6.1.1 @final
- 类型不能被扩展
- 不能添加、删除或修改任何成员
- 使用PLAIN_CDR或PLAIN_CDR2编码
- 序列化效率最高
- 适用于稳定不变的基础类型
6.1.2 @appendable
- 只能在类型末尾添加新成员
- 不能修改或删除现有成员
- XCDR v1使用PLAIN_CDR编码
- XCDR v2使用DELIMITED_CDR编码
- 支持向后兼容:旧版本可以读取新版本的消息
- 适用于大多数业务类型
6.1.3 @mutable
- 可以添加、删除和重排序成员
- 可以修改成员的可选性
- 不能修改现有成员的类型
- 使用PL_CDR或PL_CDR2编码
- 支持向前和向后兼容
- 灵活性最高,但序列化效率最低
- 适用于频繁变更的类型
6.2 可选成员
XCDR支持可选成员,通过@optional注解指定:
- 可选成员可以存在也可以不存在
- 如果可选成员不存在,则不会被序列化
- 接收方可以通过检查成员头来确定成员是否存在
- 可选成员总是使用参数化CDR编码
6.3 成员ID
每个成员都有一个唯一的成员ID,通过@id注解指定:
- 成员ID用于在参数化CDR中标识成员
- 如果没有显式指定,成员ID会自动从0开始分配
- 建议显式指定成员ID以避免版本变更时的冲突
- 成员ID在类型的继承层次中必须唯一
版本演进示例:
idl
// v1版本
@appendable
struct Person {
@id(0) string name;
@id(1) long age;
};
// v2版本(向后兼容)
@appendable
struct Person {
@id(0) string name;
@id(1) long age;
@id(2) string email; // 在末尾添加新成员
};
7. 主流实现
目前最流行的CDR实现有以下三个,都符合OMG标准:
7.1 eProsima FastCDR
- 最流行的开源CDR实现,被Fast DDS和ROS2广泛使用
- 完全支持XCDR v1和XCDR v2
- 提供两种序列化模式:
- 标准模式:完全符合OMG标准,支持所有特性
- Fast模式:修改版CDR,不使用对齐,速度更快但不兼容标准
- 高性能,低内存占用
- 支持C++11及以上版本
- 提供动态序列化API,不需要代码生成
7.2 RTI Connext CDR
- 商业级CDR实现,性能和稳定性最佳
- 完全支持XCDR v1和XCDR v2
- 提供丰富的配置选项
- 支持可扩展性合规性掩码,可以调整序列化行为以兼容不同版本
- 提供FlatData语言绑定,进一步优化大型数据的序列化性能
- 提供完善的文档和技术支持
7.3 Eclipse Cyclone DDS CDR
- 轻量级,高性能的开源CDR实现
- 完全支持XCDR v1和XCDR v2
- 基于EPL许可证,商业友好
- 特别适合嵌入式和资源受限环境
- 代码简洁,易于理解和修改
8. 性能分析与对比
8.1 CDR内部格式性能对比
| 编码格式 | 序列化速度 | 反序列化速度 | 数据大小 | 灵活性 |
|---|---|---|---|---|
| PLAIN_CDR2 | ★★★★★ | ★★★★★ | ★★★★★ | ★☆☆☆☆ |
| DELIMITED_CDR | ★★★★☆ | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
| PL_CDR2 | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ | ★★★★★ |
| PL_CDR | ★★☆☆☆ | ★★☆☆☆ | ★★☆☆☆ | ★★★★☆ |
8.2 与其他序列化格式对比
| 特性 | CDR (XCDR2) | Protobuf | FlatBuffers | JSON |
|---|---|---|---|---|
| 序列化速度 | 快 | 快 | 最快 | 慢 |
| 反序列化速度 | 快 | 快 | 几乎瞬时 | 最慢 |
| 数据大小 | 小 | 最小 | 中等 | 最大 |
| 跨语言支持 | 好 | 极好 | 好 | 极好 |
| 可扩展性 | 好 | 极好 | 好 | 极好 |
| 自描述性 | 无 | 无 | 无 | 有 |
| 零拷贝支持 | 有限 | 无 | 完全 | 无 |
| 标准性 | 国际标准 | 公司标准 | 公司标准 | 国际标准 |
| 实时性 | 极好 | 好 | 极好 | 差 |
| 多态支持 | 是 | 否 | 否 | 是 |
对于分布式实时系统,CDR是最佳选择
9. CDR在ROS2中的应用
ROS2完全基于DDS构建,因此CDR是ROS2消息的默认序列化格式。
9.1 ROS2消息与IDL的关系
ROS2的.msg文件本质上是IDL的简化语法,在编译时会自动转换为标准IDL,然后生成CDR序列化/反序列化代码。
例如,以下ROS2消息:
msg
string name
int32 age
float64 height
会被转换为以下IDL:
idl
struct Person {
string name;
long age;
double height;
};
9.2 ROS2中的CDR版本
- ROS2 Humble及更早版本:默认使用XCDR v1
- ROS2 Iron及以后版本:默认使用XCDR v2
- 不同版本之间可以通过配置实现互操作
9.3 ROS2中的CDR性能优化
- 使用
--ros-args --param use_sim_time:=true可以减少时间戳的序列化开销 - 对于大型数据(如点云、图像),使用零拷贝传输
- 合理设计消息结构,避免嵌套过深和过多的可选成员
10. 实际应用
10.1 类型设计
- 优先使用@final类型 :除非确实需要扩展,否则使用@final类型以获得最佳性能
- 合理安排成员顺序:将相同大小的成员放在一起,减少填充字节
- 显式指定成员ID:为所有成员显式指定@id注解,避免版本变更时的冲突
- 避免使用可选成员:可选成员会增加序列化开销,尽量使用默认值代替
- 使用固定长度数组:对于长度固定的数据,使用数组而不是序列
- 限制字符串长度:为字符串指定最大长度,避免内存溢出
10.2 性能优化
- 升级到XCDR v2:XCDR v2比XCDR v1平均快20%,数据大小减少15-30%
- 预分配缓冲区:提前计算序列化大小并预分配缓冲区,避免动态内存分配
- 使用零拷贝技术:在支持的平台上使用零拷贝API,减少数据复制
- 避免嵌套过深:减少数据结构的嵌套层次,提高序列化效率
- 批量处理数据:将多个小消息合并为一个大消息,减少序列化开销
10.3 兼容性
- 遵循可扩展性规则:严格遵守@final、@appendable和@mutable类型的扩展规则
- 永远不要修改现有成员:不要修改现有成员的类型、ID或顺序
- 使用必须理解标志:对于关键成员,设置必须理解标志以确保接收方正确处理
- 测试兼容性:在发布新版本之前,测试与旧版本的双向兼容性
- 使用标准模式:除非你完全控制所有通信节点,否则不要使用FastCDR的Fast模式
11. 常见问题
11.1 字节序问题
问题 :不同架构的设备之间通信时出现数据乱码
解决方案:
- 确保CDR实现正确处理字节序标志
- 在RTPS协议中,检查DATA子消息的字节序标志位
- 避免手动操作字节流,使用CDR库提供的API
11.2 对齐问题
问题 :序列化后的数据大小与预期不符
解决方案:
- 了解不同CDR版本的对齐规则差异
- 使用CDR库提供的大小计算API预先计算序列化大小
- 在XCDR v2中,64位类型是4字节对齐,不是8字节对齐
11.3 兼容性问题
问题 :不同版本的应用之间无法通信
解决方案:
- 检查可扩展性类型是否正确
- 确保成员ID没有冲突
- 检查是否使用了相同的CDR版本
- 在RTI Connext中,调整可扩展性合规性掩码
11.4 性能问题
问题 :序列化/反序列化速度太慢
解决方案:
- 切换到XCDR v2
- 使用@final类型代替@mutable类型
- 减少可选成员的使用
- 预分配缓冲区
- 使用FastCDR的标准模式(比其他实现更快)