二进制 JSON:MessagePack、CBOR 与 BSON 的取舍
结论:MessagePack、CBOR、BSON 都把 JSON 的数据模型压成字节,但体积、类型系统与生态完全不同。本文用同一份数据对比字节开销,说明 int64 与二进制字段的处理,并给出「什么时候值得换」的判断线。

结论:对外交换和需要人查看的数据保持文本 JSON,只有当二进制内容、超过 2^53 的整数或精确小数让压缩后的体积与类型表达真正吃亏时,才在内部链路换 MessagePack 或 CBOR;BSON 只属于 MongoDB 生态。
文本 JSON 省掉的是空格,二进制格式省掉的是标点与数字的字符表示;字段名照样一条一份,所以换格式前先算清楚到底省了多少。
一、二进制 JSON 到底省了什么
先把期待放对位置。这三种格式都不是「另一种 JSON 语法」,而是用字节表达同一套数据模型:对象、数组、字符串、数字、布尔、空值。它们省掉的东西很具体:
- 引号、冒号、逗号、括号等结构性字符;
- 数字的十进制文本表示,比如
32变成一个字节; - 字符串前的长度声明由定长前缀表示,省去重新扫描;
- 二进制内容可以直接是字节串,不必先做 Base64。
省不掉的东西同样具体:字段名仍然以字符串形式逐条出现。MessagePack 的 map、CBOR 的 map、BSON 的 document,都没有「列名只出现一次」的机制。因此当你的 JSON 是一万条同构记录时,压缩后的文本 JSON 与二进制格式的体积差距会明显小于直觉,因为 gzip 最擅长的就是消除重复字段名。
二、三种格式的出身
| 格式 | 规范与年份 | 设计源头 | 典型用途 |
|---|---|---|---|
| MessagePack | msgpack.org 规范,2008 年起 | 独立设计,强调极简与紧凑 | 缓存值、进程间通信、日志管道 |
| CBOR | RFC 8949(STD 94),2020 年 | 受 MessagePack 影响,IETF 标准化 | COSE、WebAuthn、CWT、受限设备 |
| BSON | bsonspec.org,MongoDB 生态 | 为数据库文档设计 | MongoDB 存储与线协议 |
CBOR 的标准化程度最高,有 IANA 注册的 application/cbor 媒体类型、确定性编码规则与 CDDL 描述语言;MessagePack 最轻,规范一页能读完;BSON 最重,它把 ObjectId、Decimal128、日期、二进制子类型都做进了类型系统,也因此只在 MongoDB 生态里真正流行。
三、同一份数据,四种字节
以 {"name":"joe"} 为例,四种表示的字节开销差别很直观:
| 表示 | 字节数 | 字节序列 |
|---|---|---|
| JSON 文本 | 14 | 7b 22 6e 61 6d 65 22 3a 22 6a 6f 65 22 7d |
| MessagePack | 9 | 81 a3 6e 61 6d 65 a3 6a 6f 65 |
| CBOR | 10 | a1 64 6e 61 6d 65 63 6a 6f 65 |
| BSON | 19 | 总长 4 字节 + 类型 1 字节 + name\0 + 值长 4 字节 + joe\0 + 终止符 |
逐字节拆开 MessagePack 那 9 个字节:
81:高 4 位1000表示 map,低 4 位0001表示 1 个键值对;a3:高 3 位101表示定长字符串,长度 3;6e 61 6d 65:name的 ASCII;a3:又是一个长度 3 的字符串;6a 6f 65:joe。
CBOR 的 a1 是 major type 5(map)加长度 1,64 是 major type 3(文本字符串)加长度 4,语义与 MessagePack 几乎同构。BSON 反而更大:每个文档都要带 4 字节总长,每个字段都要重复类型字节与字段名,字符串还要再带一个长度和结尾的 \0。小文档上,BSON 比 JSON 文本还长并不奇怪。
把这段字节还原成十六进制,可以直接核对前缀:
const messagePackBytes = [0x81, 0xa3, 0x6e, 0x61, 0x6d, 0x65, 0xa3, 0x6a, 0x6f, 0x65];
const hexText = messagePackBytes.map((byte) => byte.toString(16)).join(' ');
console.log(hexText); // 81 a3 6e 61 6d 65 a3 6a 6f 65
3.1 体积与解析速度:别信宣传数字
关于「二进制快 10 倍」的说法,先拆成两件事看:
体积。 未压缩时,二进制确实更小,因为省掉了标点与空白。但真实链路几乎都会压缩:文本 JSON 经过 gzip 或 brotli 后,重复字段名被高度压缩,二进制格式的领先幅度会缩到很小。可以对照 JSON 压缩与 gzip 的取舍 里的判断方法:先量压缩后的实际字节,再决定要不要换格式。
解析速度。 二进制省掉了词法分析(扫描引号、逗号、数字字符),理论上更快。但 JavaScript 里真正的成本大头是创建对象与分配内存,解码器再快也绕不过这一步。数据形状、字段数量、实现质量都会影响结果,唯一可靠的结论来自用自己的数据做基准测试,而不是照抄宣传页上的倍数。如果单条负载已经大到无法整体读入内存,先用 大 JSON 的解析与流式处理 里的分片方法切块,再比较编码收益。
四、int64 与二进制字段:换格式的真正理由
很多时候,体积不是换格式的理由,类型表达力才是。
4.1 超过 2^53 的整数
JSON 数字在 JavaScript 里落到双精度浮点,9007199254740993 会被解析成 9007199254740992。三种二进制格式的处理方式:
| 格式 | 64 位整数 | 无符号 64 位 | 任意精度 |
|---|---|---|---|
| MessagePack | ✓ int64(0xd3),解码可给 BigInt |
✓ uint64(0xcf) |
✗(需扩展类型) |
| CBOR | ✓ major type 0/1 的 64 位形式 | ✓ 同一编码空间 | ✓ bignum 标签 2/3 |
| BSON | ✓ int64(0x12) |
✗ 只有有符号类型 | ✗(Decimal128 定点,不是任意精度整数) |
CBOR 是三者里唯一对任意精度整数有标准表示的:超过 64 位的整数用 tag 2(非负)或 tag 3(负)包一段字节串。MessagePack 与 BSON 遇到超大整数只能退回字符串,或者在扩展类型里自定义约定。
4.2 二进制内容
JSON 表达二进制只有一条路:Base64,体积膨胀约三分之一,解码还要额外一次拷贝。MessagePack 有 bin 类型(0xc4–0xc6),CBOR 有 byte string(major type 2),BSON 有 binary 子类型。传图片、哈希、向量、压缩块时,这个差异比「省几个标点」重要得多。
五、三者对比表
| 维度 | MessagePack | CBOR | BSON |
|---|---|---|---|
| 规范来源 | 社区规范 | IETF RFC 8949 | MongoDB 生态 |
| 媒体类型 | application/msgpack(非 IANA 标准) |
application/cbor(IANA 注册) |
无通用媒体类型 |
| 类型系统 | JSON 六类 + bin + ext | JSON 六类 + bin + tag + 简单值 | 更宽:ObjectId、Decimal128、日期、正则、时间戳 |
| 扩展机制 | extension type(如 timestamp -1) | tag(日期、bignum、内嵌 CBOR 等) | 固定类型码,扩展靠生态约定 |
| 确定性编码 | 无标准要求 | 有明确规则,可用于签名 | 无 |
| 自描述 | 是,但无字段级 schema | 是,可配 CDDL 描述 | 是,文档内重复字段名 |
| 小文档开销 | 低 | 低 | 高(长度前缀 + 字段名 + 终止符) |
| 生态 | 缓存、日志、部分 RPC | COSE、WebAuthn、IoT | MongoDB 专用 |
六、浏览器与 Node 支持
Node 24 没有内置这三种编解码器,需要装库。常用组合:
| 运行时 | MessagePack | CBOR | BSON |
|---|---|---|---|
| Node.js | @msgpack/msgpack、msgpackr |
cbor-x、borc |
bson |
| 浏览器 | @msgpack/msgpack |
cbor-x 的浏览器构建、cbor-web |
bson |
两类实现都围绕 Uint8Array 工作,不需要额外的 Buffer。需要注意版本差异:
import { encode, decode } from '@msgpack/msgpack';
/**
* 将对象编码为 MessagePack 字节并立即解码校验。
* @param payload 待编码的普通对象
* @returns 解码结果与编码后的字节长度
*/
function roundTrip(payload: Record<string, unknown>): { decoded: unknown; byteLength: number } {
const bytes = encode(payload);
return { decoded: decode(bytes), byteLength: bytes.byteLength };
}
大整数在多数库里默认解码成 number,超过安全整数范围时会丢精度;需要精确值时,要显式打开 BigInt 选项(@msgpack/msgpack 提供 useBigInt64,cbor-x 有对应开关),否则等于把 JSON 的老问题原样搬了过来。
七、可调试性与协议兼容
7.1 可调试性:最大的隐性成本
文本 JSON 最大的优势不是体积,而是任何时候都能打开看。换成二进制之后:
grep、awk、jq全部失效,日志管道要额外一步解码;- 排查线上问题时,先要拿到正确的解码器与版本,否则看到的字段名可能是乱的;
- 抓包结果在浏览器开发者工具里显示为一段不可读的字节;
- 代码评审时,diff 里出现的是一串十六进制。
CBOR 有诊断记法(diagnostic notation),能把字节还原成接近 JSON 的可读文本,例如 map 写作 {1: 2};MessagePack 也有若干命令行解码工具。但这些都需要额外安装,且不在标准运维链路里。
结论:只要这份数据需要人频繁查看,二进制格式的隐性成本就会超过它省下的带宽。
7.2 协议兼容与扩展
- CBOR 的 tag 机制最完整:tag 0 表示日期时间字符串,tag 1 表示 Unix 时间戳,tag 2/3 表示大整数,tag 24 表示内嵌一段 CBOR。配合确定性编码,它被 COSE、WebAuthn、CWT 这些需要签名与跨实现一致性的协议采用。
- MessagePack 的扩展类型留给实现方自行约定,常见的只有 timestamp(扩展类型 -1)。跨团队使用时,扩展类型必须写进协议文档,否则对方解不出来。
- BSON 的类型码是固定的,
ObjectId、Decimal128、MinKey/MaxKey都是 MongoDB 专属语义。把 BSON 当成通用交换格式,会带来一堆用不上的类型和用不上的长度前缀。
如果协议要求「同样的数据必须编码成同样的字节」,只有 CBOR 给出了标准答案;MessagePack 与 BSON 都需要额外约定字段顺序与编码细节。
八、什么时候值得换
值得换的信号:
- 单次传输的数据量大,且两端都能改代码,压缩后的体积仍有明显差距;
- 需要传二进制内容,Base64 的膨胀与解码成本已经可测;
- 需要 64 位整数、精确小数,而 JSON 的双精度会丢精度;
- 服务端到服务端的内部链路,排查工具可以统一配好。
不值得换的信号:
- 已经用 gzip 或 brotli 压缩,收益只剩个位数百分比;
- 对外公开 API,第三方只能用通用 HTTP 客户端;
- 日志、配置文件等需要人肉查看的内容;
- 团队没有维护解码工具与版本兼容的预算。
中间路线也成立:对外保持 JSON,内部缓存用 MessagePack。缓存值只被自家服务读取,体积与解析成本都能省,出问题时把缓存清掉即可,不影响契约。
九、实践清单
- [ ] 先用真实数据比较压缩后的字节数,而不是未压缩的字节数;
- [ ] 明确大整数策略:BigInt 选项、字符串承载,还是换成 CBOR 的 bignum;
- [ ] 二进制字段直接走 bin / byte string,不再套一层 Base64;
- [ ] 扩展类型与 tag 写进协议文档,并固定解码器版本;
- [ ] 保留一条文本 JSON 的调试通道,线上排障时能切过去;
- [ ] 做一次基准测试,把「快多少」写成可复现的数字,而不是口头结论。
MessagePack 和 CBOR 有什么区别?
两者编码结构高度相似:map、数组、字符串都用「类型位 + 长度」的紧凑前缀。主要差别在标准化程度:CBOR 是 IETF RFC,有 IANA 媒体类型、tag 机制与确定性编码规则;MessagePack 是社区规范,扩展类型靠自定义约定。需要签名、跨实现一致性或受限设备协议时选 CBOR。
BSON 能当通用 JSON 替代品吗?
不建议。BSON 为数据库文档设计,每个文档带 4 字节总长、每个字段重复类型与字段名,小文档比 JSON 文本还大;它还包含 ObjectId、MinKey 等 MongoDB 专属类型。离开 MongoDB 生态后,它的类型系统大部分用不上,体积劣势却照旧存在。
换成二进制格式后,体积能小多少?
取决于数据形状。字段名重复率高时,文本 JSON 压缩后与二进制的差距很小;字段名短、结构字符多、数字多时,二进制优势更明显。正确做法是拿真实样本分别测「未压缩」与「gzip 后」两组数字,再做决定,不要用通用倍数估算。
二进制格式还能用 jq 调试吗?
不能。jq 只处理文本 JSON,遇到 MessagePack 或 CBOR 字节会直接报解析错误。调试时需要先用对应库解码成 JSON,再交给 jq;CBOR 还可以用诊断记法工具转成可读文本。因此保留一条文本调试通道,比临时装工具更省时间。
常见问题
如何判断该不该换成二进制格式?
先量压缩后的字节数,再看类型表达是否真的吃亏,而不是看未压缩体积。单次传输量大、两端都能改代码、压缩后差距仍明显时值得换;已经用 gzip 或 brotli 且收益只剩个位数百分比、或是对外公开 API 时,保持文本 JSON 更划算。
为什么二进制格式在体积上不一定占优?
二进制省掉了引号、冒号等结构字符和数字的文本表示,但字段名仍然逐条出现,重复字段名要靠压缩算法消除。文本 JSON 经过 gzip 或 brotli 后,领先幅度会缩到很小,小文档上 BSON 甚至比 JSON 文本还长。
能否直接用 jq 或 grep 调试二进制数据?
不能,jq 只处理文本 JSON,遇到 MessagePack 或 CBOR 字节会直接报解析错误。需要先用对应库解码成 JSON 再交给 jq,CBOR 还可以用诊断记法工具转成可读文本;因此实践上要保留一条文本 JSON 的调试通道。