4、数据项编码实现:如何在C语言中实现各数据类型的序列化与反序列化

SECS-II 消息的核心是数据项(Data Item),每个数据项由 数据项头(Item Header) 和 数据体(Data Body) 组成。在 C 语言中实现序列化(将结构体转换为字节流)与反序列化(将字节流还原为结构体),是协议栈移植中最基础且最容易出错的环节。本节将深入剖析 SECS-II 六种基本数据类型的编码规则,并给出可移植的 C 语言实现。

核心原则:SECS-II 采用大端字节序(Big-Endian)传输多字节数据。所有序列化/反序列化操作必须严格遵循大端序,否则会导致跨平台通信失败。

4.1 SECS-II 数据项格式总览

每个 SECS-II 数据项由三部分构成:长度字节(Length Bytes)、数据格式字节(Format Byte) 和 数据体(Data Body)。格式字节的高 4 位表示数据类型,低 4 位表示长度字节数(通常为 1 或 3)。

SECS-II 数据项字节布局 格式字节 (1 Byte) 长度字节 (1 或 3 Bytes) 数据体 (Data Body) (N Bytes, 由长度字节指定) 格式字节结构: Bit[7:4] = 数据类型码 (0x00~0x0A) | Bit[3:0] = 长度字节数 (0x01 或 0x03)

4.2 六种基本数据类型编码规则

SECS-II 定义了六种核心数据类型,每种类型在 C 语言中对应不同的序列化/反序列化策略。下表总结了各类型的格式码、C 语言映射及字节序要求:

类型名称 格式码 (Hex) C 语言类型 字节序 长度字节数 典型用途
List 0x01 嵌套结构体 — 1 或 3 消息容器
Binary 0x00 uint8_t[] — 1 或 3 原始字节
Boolean 0x0A uint8_t — 1 标志位
ASCII 0x02 char[] — 1 或 3 字符串
I1 / I2 / I4 / I8 0x04~0x07 int8_t / int16_t / int32_t / int64_t 大端 1 有符号整数
U1 / U2 / U4 / U8 0x08~0x0B uint8_t / uint16_t / uint32_t / uint64_t 大端 1 无符号整数
F4 / F8 0x0C~0x0D float / double 大端 1 浮点数
关键点:List 类型不包含数据体,其“长度”表示子项个数而非字节数。Boolean 类型固定使用 1 字节长度,值 0x00 为假,非 0x00 为真。

4.3 序列化核心实现

序列化的本质是将 C 语言变量按大端序写入字节缓冲区。以下给出整数和浮点数的核心序列化函数:

/* 序列化 int32 (I4) 到缓冲区 */
void serialize_i4(uint8_t *buf, int32_t value) {
    buf[0] = (uint8_t)((value >> 24) & 0xFF);
    buf[1] = (uint8_t)((value >> 16) & 0xFF);
    buf[2] = (uint8_t)((value >> 8) & 0xFF);
    buf[3] = (uint8_t)(value & 0xFF);
}

/* 序列化 float (F4) 到缓冲区 */
void serialize_f4(uint8_t *buf, float value) {
    uint32_t raw;
    memcpy(&raw, &value, sizeof(raw));
    buf[0] = (uint8_t)((raw >> 24) & 0xFF);
    buf[1] = (uint8_t)((raw >> 16) & 0xFF);
    buf[2] = (uint8_t)((raw >> 8) & 0xFF);
    buf[3] = (uint8_t)(raw & 0xFF);
}

/* 序列化 ASCII 字符串到缓冲区 */
void serialize_ascii(uint8_t *buf, const char *str, uint32_t len) {
    memcpy(buf, str, len);
}
注意:浮点数序列化必须通过 memcpy 将 float 转换为 uint32_t 后再进行大端转换,直接强制类型转换会触发未定义行为。ASCII 字符串不需要字节序转换,直接逐字节拷贝即可。

4.4 反序列化核心实现

反序列化是从字节流中按大端序还原出 C 语言变量。以下是对应的反序列化函数:

/* 反序列化 int32 (I4) 从缓冲区 */
int32_t deserialize_i4(const uint8_t *buf) {
    return ((int32_t)buf[0] << 24) |
           ((int32_t)buf[1] << 16) |
           ((int32_t)buf[2] << 8)  |
           (int32_t)buf[3];
}

/* 反序列化 float (F4) 从缓冲区 */
float deserialize_f4(const uint8_t *buf) {
    uint32_t raw = ((uint32_t)buf[0] << 24) |
                   ((uint32_t)buf[1] << 16) |
                   ((uint32_t)buf[2] << 8)  |
                   (uint32_t)buf[3];
    float value;
    memcpy(&value, &raw, sizeof(value));
    return value;
}

/* 反序列化 ASCII 字符串从缓冲区 */
void deserialize_ascii(const uint8_t *buf, char *out, uint32_t len) {
    memcpy(out, buf, len);
    out[len] = '\0'; /* 确保字符串终止 */
}

4.5 完整数据项编码与解码流程

实际应用中,序列化/反序列化需要组合格式字节、长度字节和数据体。以下流程图展示了编码一个 I4 数据项的完整步骤:

编码 I4 数据项完整流程 步骤1: 写入格式字节 0x41 (I4 + 1长度字节) 步骤2: 写入长度字节 0x04 (4字节数据) 步骤3: 写入数据体 大端序 4 字节 示例:编码值 123456 (0x0001E240) 缓冲区: [0x41] [0x04] [0x00] [0x01] [0xE2] [0x40] 格式字节 长度字节 数据体(大端序) 解码流程(反向操作): 1. 读取格式字节,解析数据类型和长度字节数 2. 读取长度字节,确定数据体字节数 3. 读取数据体,按大端序还原为 C 变量

4.6 跨平台移植注意事项

在嵌入式系统或不同架构间移植时,以下问题必须处理:

  • 字节序检测:使用预定义宏(如 __BYTE_ORDER__)或运行时检测,确保只在需要时进行字节序转换。
  • 对齐问题:某些 ARM/MIPS 平台不允许非对齐访问,序列化/反序列化时应使用逐字节操作而非结构体强制转换。
  • 浮点格式:确保目标平台使用 IEEE 754 标准,否则 F4/F8 序列化结果不兼容。
  • 长度字段处理:当数据体长度超过 255 字节时,必须使用 3 字节长度字段(格式字节低 4 位为 0x03)。
推荐实践:在协议栈中统一使用 uint8_t* 作为序列化/反序列化的中间表示,避免直接操作结构体指针。这样可以最大程度保证代码的可移植性和安全性。

4.7 性能优化建议

对于高频消息处理场景,以下优化策略可显著提升吞吐量:

优化策略 适用场景 预期收益 实现复杂度
批量序列化 List 中包含多个同类型数据项 减少函数调用开销 低
预分配缓冲区 已知最大消息长度 避免动态内存分配 低
查表法格式解析 频繁解析格式字节 减少分支预测失败 中
SIMD 加速拷贝 大型 ASCII/Binary 数据项 提升大块数据拷贝速度 高

通过以上实现,C 语言可以高效、准确地完成 SECS-II 数据项的序列化与反序列化,为上层消息事务处理奠定坚实基础。