1. 序列化基础:什么是序列化、为什么需要序列化、C语言中序列化的应用场景

大家好,我是老刘。做嵌入式开发十几年了,今天咱们来聊聊序列化这个话题。

说实话,我刚入行那会儿,对序列化这词儿挺懵的。觉得不就是把数据存起来吗?有啥好讲的?后来踩了不少坑,才明白这东西有多重要。

1.1 什么是序列化?

序列化,说白了就是把内存里的数据结构,变成一串连续的字节流。反序列化就是反过来,把这串字节流再还原成原来的数据结构。

你想想看,你在代码里定义了一个结构体:

typedef struct {
    uint8_t  id;
    uint16_t temperature;
    uint32_t timestamp;
    char     name[16];
} SensorData;

这个结构体在内存里是分散存放的。有对齐填充,有字节顺序问题。你想把它存到文件里,或者通过网络发出去,怎么办?

嗯,这时候就需要序列化了。把它变成一串连续的、没有歧义的字节:

// 序列化后的字节流(大端模式)
[0x01]           // id = 1
[0x01][0x90]     // temperature = 400
[0x00][0x00][0x00][0x64]  // timestamp = 100
[0x48][0x65][0x6C][0x6C][0x6F]...  // "Hello"

核心要点:序列化解决的是「内存表示」与「存储/传输表示」之间的转换问题。内存里的结构体,不能直接写到文件里或发到网络上。

1.2 为什么需要序列化?

我在项目中遇到过这么一件事:有个同事直接把结构体用 fwrite 写进了文件。在自己电脑上跑得好好的,换了一台机器就全乱了。为什么?

原因有三:

  1. 内存对齐不一样:不同编译器、不同平台,结构体的 padding 可能不同
  2. 字节序不同:大端小端,这是个老生常谈的问题
  3. 数据类型大小不同:int 在 8 位机上是 2 字节,在 32 位机上是 4 字节

说白了,序列化就是为了解决跨平台、跨语言、跨进程的数据交换问题。

场景 不序列化会怎样?
文件存储 换个编译器就读不出来了
网络传输 不同字节序的机器之间数据全乱
进程间通信 共享内存里的结构体,两边定义不一样就崩
固件升级 配置参数存到 Flash,版本升级后结构变了

1.3 C语言中序列化的应用场景

我这些年做过的项目里,序列化几乎无处不在。给你列几个典型的:

场景一:嵌入式配置参数存储

做物联网设备的时候,经常要把配置参数存到 Flash 或 EEPROM 里。比如 WiFi 密码、服务器地址这些。

typedef struct {
    char     ssid[32];
    char     password[64];
    uint16_t port;
    uint8_t  retry_count;
} WifiConfig;

// 序列化:把结构体变成字节数组
void serialize_wifi_config(const WifiConfig *cfg, uint8_t *buf) {
    uint16_t offset = 0;
    memcpy(buf + offset, cfg->ssid, 32);        offset += 32;
    memcpy(buf + offset, cfg->password, 64);    offset += 64;
    buf[offset++] = (cfg->port >> 8) & 0xFF;    // 大端
    buf[offset++] = cfg->port & 0xFF;
    buf[offset++] = cfg->retry_count;
}

我的习惯:做嵌入式序列化时,我一般会加一个版本号字段。这样以后结构体变了,还能兼容旧数据。这个坑我踩过,血的教训。

场景二:网络通信协议

两个设备通过串口或网络通信,数据必须按约定好的格式打包。我做过一个传感器网络的项目,每个节点上报数据时都要序列化:

// 通信协议帧格式
// | 帧头(2B) | 长度(1B) | 类型(1B) | 数据(NB) | 校验(1B) |
// | 0xAA 0x55 |   len    |   type   |  payload |  checksum|

uint8_t frame[256];
frame[0] = 0xAA;
frame[1] = 0x55;
frame[2] = payload_len + 3;  // 类型+数据+校验
frame[3] = 0x01;             // 温度数据
memcpy(&frame[4], payload, payload_len);
frame[4 + payload_len] = calc_checksum(frame, 4 + payload_len);

场景三:日志与调试数据导出

调试的时候,经常要把内部状态导出来分析。序列化成 JSON 或二进制格式,方便上位机解析。

// 我曾经写过一个调试工具
// 把系统运行状态序列化成 CSV 格式
void log_system_status(const SystemStatus *status) {
    printf("%lu,%d,%d,%d,%u\n",
           status->uptime,
           status->cpu_load,
           status->memory_used,
           status->sensor_count,
           status->error_code);
}

注意:千万不要直接用 memcpy 把结构体写到文件或发到网络。不同平台的对齐方式、字节序、基本类型大小都可能不一样。我见过有人这么干,结果产品量产了才发现问题,那叫一个惨。

1.4 序列化的核心挑战

做序列化,其实就是在解决这几个问题:

  • 字节序:大端还是小端?通信双方得约定好
  • 数据对齐:结构体里的 padding 怎么处理?
  • 可变长度数据:字符串、数组这些,长度怎么表示?
  • 版本兼容:以后数据结构变了,老数据还能不能读?
  • 效率:嵌入式系统资源有限,序列化不能太慢,也不能占太多内存

嗯,这些内容后面几章会一个一个展开讲。今天先把基础概念理清楚。

序列化核心知识体系 序列化 / 反序列化 什么是序列化? • 内存结构 → 字节流 • 解决内存表示与存储/传输 表示之间的转换 为什么需要? • 跨平台兼容 • 跨语言数据交换 • 持久化存储 • 网络传输 应用场景 • 嵌入式配置存储 • 网络通信协议 • 日志与调试导出 • 固件升级数据 核心挑战:字节序 · 对齐 · 可变长度 · 版本兼容 · 效率

这张图把今天讲的内容串起来了。序列化不是什么高深的技术,但用好了,能让你的代码更健壮、更可移植。

好了,第一章就到这里。记住一句话:别偷懒,别直接用 memcpy 怼结构体。老老实实写序列化函数,以后会感谢自己的。


公众号:蓝海资料掘金营,微信deep3321