0x9a.com

保持简单,保持好奇

二进制文件格式设计:从 PNG 学习

2026-03-05

二进制文件格式设计的核心是识别、边界、扩展和校验。本文以 PNG 为主线,重点拆解签名与 Chunk(IHDR/IDAT/IEND),最后简要迁移到网络帧与 Protobuf。

1. 文件格式先要解决的四个问题

无论是图片、日志归档,还是自定义二进制文档,核心都绕不开这四件事:

  1. 识别:怎么快速确认“这是我的格式”?
  2. 边界:怎么知道每段数据从哪开始、到哪结束?
  3. 扩展:未来加字段时,旧解析器会不会直接崩?
  4. 校验:数据损坏时,能不能尽早发现并定位?

对应到工程手段,就是:

2. 先定全局语义,再谈字段

在设计具体字段之前,先把这些“地基规则”写死:

这些规则看起来不起眼,但它们决定了解析器是否稳定、可审计、可维护。

3. PNG:文件格式设计的经典样板

PNG 非常适合用来学习“可演进的二进制文件结构”。

如果只抓主干,最关键就是三个 Chunk:IHDR(头)、IDAT(数据)、IEND(结束)。

3.1 文件签名(Magic Number)

PNG 固定 8 字节签名:

89 50 4E 47 0D 0A 1A 0A

解析器可以在第一时间做“快速失败”:

3.2 Chunk 通用结构

PNG 后续内容由多个 Chunk 组成,每个 Chunk 都是同一结构:

Length(4) | Type(4) | Data(N) | CRC(4)

关键点:

这种结构的工程价值很高:按长度可跳过未知块,按类型可增量扩展语义

3.3 IHDR:必须第一个

IHDR 是第一个 Chunk,Data 固定 13 字节:

  1. Width(4)
  2. Height(4)
  3. Bit depth(1)
  4. Color type(1)
  5. Compression method(1)
  6. Filter method(1)
  7. Interlace method(1)

它相当于“全文件解码前提”。IHDR 不合法,后面基本都没有继续解析的意义。

3.4 IDAT:图像数据主体(可多个)

IDAT 承载压缩后的图像数据,注意两点:

也就是说,解码路径通常是:

  1. 拼接 IDAT 数据;
  2. zlib/deflate 解压;
  3. 按行执行 unfilter(None/Sub/Up/Average/Paeth);
  4. 得到最终像素数据。

3.5 IEND:必须最后一个

IEND 表示文件结束:

它让解析器可以明确“到此为止”,避免靠 EOF 猜测结构完整性。

3.6 为什么这种结构特别耐用

Chunk 机制的核心收益:

这就是“格式可演进”的基本盘。

4. 一个很实用的小技巧:TL 融合(Tag + Length 合并)

标准 TLV 是三段:Tag | Length | Value(Tag 在不少文档里也会写作 Type)。

但在短数据场景里,可以把 Tag 和 Length 融到 1 个字节里。例如:

这样短字符串(0~31 字节)只用 1 字节头,省空间且解析快。

uint8_t b = read_u8();
if ((b & 0xE0) == 0x20) {   // 001xxxxx
    size_t len = b & 0x1F;  // 0..31
    read_bytes(len);
}

实践建议:

这种“短头内联”的思路,在 MessagePack 的 fixstr 里也能看到。

5. 文件视角下的校验与版本

5.1 校验分层

不要用一种手段试图覆盖所有风险:

常见组合:

5.2 版本策略

配合 Chunk/TLV,版本演进会稳定很多。

6. 迁移到网络协议:Framing + Protobuf

上面这套思想可以迁移到网络,但网络侧重点更偏“流式切包”。

6.1 先做帧边界,再谈 Payload 编码

TCP 是字节流,不是消息流,所以通常要先定义帧头:

Magic | Version | MsgType | Flags | Length | Payload | CRC

有了 Length 才能稳定切出完整消息;否则即使用了 Protobuf,也会卡在“包边界不明确”。

6.2 Protobuf wire format(最小必要细节)

Protobuf 每个字段是 key + value

其中字符串、bytes、子消息都走 length-delimited

[key(varint)][len(varint)][payload]

举个最小例子:

message User {
  uint32 id = 1;
  string name = 2;
}

id=150, name="A" 时,字节可写成:

08 96 01 12 01 41

这和文件里的 TLV/Chunk 在思想上是同构的:

结语

把 PNG 读透之后,再看 TLV、网络帧和 Protobuf,会发现它们共享同一组底层原则:

掌握这组原则,才能从“会写字段”走到“会做长期可演进的格式设计”。

← 返回首页