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 写进了文件。在自己电脑上跑得好好的,换了一台机器就全乱了。为什么?
原因有三:
- 内存对齐不一样:不同编译器、不同平台,结构体的 padding 可能不同
- 字节序不同:大端小端,这是个老生常谈的问题
- 数据类型大小不同: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 怼结构体。老老实实写序列化函数,以后会感谢自己的。