26、序列化与压缩:zlib压缩序列化数据、压缩比与速度权衡
说实话,做嵌入式这么多年,我踩过最大的坑之一,就是忽略了数据体积。
你想想看,一个结构体序列化出来可能才几百字节,但要是存到Flash里,或者通过无线模块发出去,日积月累下来,那数据量就吓人了。我有个项目,设备每天上报一次状态,一年下来日志文件撑爆了存储芯片。后来加了zlib压缩,体积直接缩到原来的三分之一。嗯,今天我们就聊聊这个。
为什么序列化之后还要压缩?
序列化是把内存里的结构体变成字节流。但字节流里往往有很多冗余。比如你存一个int32的数值0,它占了4个字节,但实际信息量几乎为零。再比如字符串里重复的字段名、填充字节,这些都是可以压缩的。
说白了,压缩就是去掉这些冗余,让数据更紧凑。
我个人的习惯是:只要数据要落盘或者走网络,就考虑加一层压缩。尤其是嵌入式设备,Flash和带宽都是稀缺资源,能省一点是一点。
zlib 是什么?
zlib 是一个开源的压缩库,C语言写的,几乎无处不在。Linux、Windows、各种RTOS都能用。它实现了Deflate压缩算法,压缩比和速度可以调节。
你不需要自己写压缩算法,zlib 已经帮你封装好了。我们只需要调用几个函数就行。
zlib 的核心函数
zlib 的API其实不多,常用的就这几个:
| 函数 | 作用 |
|---|---|
compress() |
压缩数据,简单粗暴,一次调用搞定 |
uncompress() |
解压缩,对应上面的压缩 |
deflateInit() / deflate() / deflateEnd() |
流式压缩,适合大块数据或不确定长度的数据 |
inflateInit() / inflate() / inflateEnd() |
流式解压缩 |
对于序列化后的数据,我一般用 compress() 就够了。数据量不大,一次压缩搞定,代码也干净。
实战:序列化 + zlib 压缩
我们拿一个传感器数据结构来举例。
#include <stdio.h>
#include <string.h>
#include <zlib.h>
typedef struct {
int id;
float temperature;
float humidity;
char location[32];
} SensorData;
int serialize_and_compress(SensorData *data, unsigned char *out, size_t *out_len) {
unsigned char buf[256];
int serialized_len = sizeof(SensorData);
// 1. 序列化:直接把结构体拷贝到缓冲区
memcpy(buf, data, serialized_len);
// 2. 压缩
uLongf compressed_len = *out_len;
int ret = compress(out, &compressed_len, buf, serialized_len);
if (ret != Z_OK) {
printf("压缩失败: %d\n", ret);
return -1;
}
*out_len = compressed_len;
return 0;
}
int main() {
SensorData sensor = {
.id = 1001,
.temperature = 25.6f,
.humidity = 68.3f,
.location = "机房A区"
};
unsigned char compressed[256];
size_t compressed_len = sizeof(compressed);
if (serialize_and_compress(&sensor, compressed, &compressed_len) == 0) {
printf("原始大小: %zu 字节\n", sizeof(SensorData));
printf("压缩后大小: %zu 字节\n", compressed_len);
printf("压缩比: %.2f%%\n", (float)compressed_len / sizeof(SensorData) * 100);
}
return 0;
}
你看,代码其实很简单。序列化就是memcpy,压缩就是compress。但这里有个坑——压缩后的数据不能直接memcpy回结构体,必须先解压缩。
压缩比与速度的权衡
zlib 的 compress() 默认使用压缩级别6(0-9)。级别越高,压缩比越好,但速度越慢。级别0是不压缩,级别9是最大压缩。
我在项目中做过测试,给大家一个参考:
| 压缩级别 | 压缩比(越小越好) | 压缩耗时(相对) | 适用场景 |
|---|---|---|---|
| 0 | 100% | 极快 | 只是打包,不压缩 |
| 1 | 约40% | 快 | 实时性要求高的场景 |
| 6 | 约30% | 中等 | 默认,平衡之选 |
| 9 | 约25% | 慢 | 存储场景,不关心速度 |
为什么会这样?因为压缩算法本质上是找重复模式。级别越高,它找得越仔细,但计算量也越大。
我个人习惯:如果数据是实时发送的,用级别1或3。如果是存到Flash里,用级别6或9。别小看这点差别,我曾经在一个项目里用级别9压缩日志,结果每次写入都要卡几百毫秒,后来改成级别3,速度上去了,压缩比也没差太多。
流式压缩:处理大块数据
如果你要压缩的数据很大,比如几兆字节的日志文件,用 compress() 一次性压缩可能会撑爆内存。这时候就要用流式压缩。
流式压缩的核心思想是:边读边压,分块处理。
int stream_compress(FILE *in, FILE *out, int level) {
z_stream strm;
unsigned char in_buf[4096];
unsigned char out_buf[4096];
int ret;
int flush;
memset(&strm, 0, sizeof(strm));
deflateInit(&strm, level);
do {
strm.avail_in = fread(in_buf, 1, sizeof(in_buf), in);
if (ferror(in)) {
deflateEnd(&strm);
return Z_ERRNO;
}
flush = feof(in) ? Z_FINISH : Z_NO_FLUSH;
strm.next_in = in_buf;
do {
strm.avail_out = sizeof(out_buf);
strm.next_out = out_buf;
ret = deflate(&strm, flush);
if (ret == Z_STREAM_ERROR) {
deflateEnd(&strm);
return ret;
}
fwrite(out_buf, 1, sizeof(out_buf) - strm.avail_out, out);
} while (strm.avail_out == 0);
} while (flush != Z_FINISH);
deflateEnd(&strm);
return Z_OK;
}
这段代码看起来长,但逻辑很清晰:每次读4KB数据,压缩后写入输出文件。直到文件读完,调用 Z_FINISH 收尾。
避坑指南:我曾经踩过的坑
我曾经在一个项目里,把压缩后的数据直接通过串口发送。接收端解压缩时,发现数据不对。查了半天,原来是压缩后的数据里可能包含0x00字节,而串口驱动把0x00当成了字符串结束符,直接截断了。
所以,如果你用字符串方式传输压缩数据,一定要用二进制传输,或者做base64编码。别问我怎么知道的……
另一个坑是:压缩后的数据长度可能比原始数据还大。尤其是数据本身已经很小,或者随机性很强的时候。zlib的文档里也说了,压缩后的输出缓冲区应该比输入大0.1%再加12字节。我一般直接给输入长度的1.1倍,省心。
什么时候不该用压缩?
压缩不是万能的。以下几种情况,我建议你别加压缩:
- 数据本身已经压缩过,比如JPEG图片、MP3音频。再压也压不下去,反而浪费时间。
- 数据量极小,比如几十个字节。压缩后的开销可能比数据本身还大。
- 实时性要求极高,比如控制指令。压缩和解压缩的延迟可能影响系统响应。
嗯,说白了,压缩是个工具,用不用、怎么用,得看场景。
知识体系图
下面这张图总结了序列化与压缩的核心逻辑:
从图里可以看得很清楚:序列化是第一步,压缩是第二步。压缩后的数据可以存、可以传、可以记日志。用的时候,先解压缩,再反序列化,还原成结构体。
总结
zlib 压缩序列化数据,说白了就是用CPU时间换存储空间和带宽。嵌入式设备上,Flash和带宽往往比CPU更贵,所以这个交换通常是划算的。
我个人建议:默认加上压缩,级别选6。如果发现性能瓶颈,再调低级别或者去掉压缩。别一开始就追求极致压缩比,那点空间省下来,可能不够你调试一次崩溃的时间成本。
嗯,今天就聊到这里。记住一句话:压缩是手段,不是目的。你的目的是让数据更小、更快、更可靠。
公众号:蓝海资料掘金营,微信deep3321