☰
Protobuf与JSON字节对比:解析二进制序列化如何省一半流量
2026/10/16 11:21:30 网站建设 项目流程

如果你做过后端或者客户端开发,大概率在某个技术方案里见过 Protobuf 这三个字母。它全称 Protocol Buffers,是一套开源的数据序列化方案,简单说就是把内存里的结构体、对象变成一段可以传输、可以存储的二进制,到了对端再还原成对象。这几年微服务架构铺开后,Protobuf 的出镜率越来越高,尤其和 gRPC 这类 RPC 框架绑定在一起,几乎成了内部接口的默认选项。但很多人第一次想深入它,是因为一个非常直接的问题:Protobuf 和 JSON 比,字节大小到底差多少?真的像网上说的能省一半流量吗?

这篇文章我不会只给结论,我会拿一个实际的用户对象,分别用 JSON 和 Protobuf 序列化,把两种格式逐字节拆开给你看。你看完不光知道哪个小,还能明白为什么小、什么时候该用、什么时候别勉强换。适合正在做接口设计、面试前想搞懂序列化原理、或者被领导一句话“咱们把接口换成 protobuf 吧”整懵的开发朋友。几分钟读完,你会对这两个东西的取舍有自己的判断。

1. Protobuf 是什么:先搞懂它解决的问题

1.1 序列化到底在解决什么问题

在拆字节之前,得先把“序列化”这件事说清楚。你在代码里写一个结构体或者对象,它在内存里是什么样?字符串可能是一块连续内存加一个长度字段,整数可能占 4 个字节,布尔值可能占 1 个字节,更别说还有指针、对齐、引用关系。这些东西离开当前进程之后全都不能直接用。你要么把它写到磁盘里,要么把它发到网络对端,这时候必须把它转成一段连续的、没有歧义的字节流,这个动作就叫序列化;对端拿到这串字节,再还原成对象,叫反序列化。

JSON 本质上就是干这个事的一种方案,它把内存对象写成一段带格式的文本。而 Protobuf 是另一种方案,它把对象编码成紧凑的二进制。很多人在单机、单进程里传对象没感觉,一旦服务拆开、流量上去了,序列化方案的选择就变成实打实的成本问题。

1.2 Protobuf 的工作流程和设计目标

Protobuf 最早是某头部科技公司为了解决内部 RPC 传输效率问题设计的,后来开源出来。它的设计目标写得很直白:比 XML 更小、更快、更简单。工作方式和 JSON 有一个本质区别:JSON 是“无 schema”的,拿到一段文本就能解析;Protobuf 则要求你先用.proto文件定义数据结构,再通过编译器(protoc)生成目标语言的代码,业务代码直接调用生成的类来设置字段、序列化、反序列化。

比如你要定义一个用户对象,得先写一个.proto文件,里面描述有哪些字段、每个字段什么类型、编号是多少。然后你用 protoc 把它编译成 Python、Java、Go、C++ 等任意支持语言的代码。这样一来,跨语言传递的时候,大家根据同一份.proto生成各自语言的代码,字段规则完全对齐,不会出现同一个对象在 A 服务里叫user_name、在 B 服务里叫username这种问题。

这套流程听起来比 JSON 重,但它带来的收益,就是接下来要聊的“字节大小”优势。JSON 因为是文本,里面塞满了引号、冒号、逗号、字段名;Protobuf 则把这些全部丢掉,只留下对端解析时“够用”的最小信息。

1.3 为什么大家总拿它和 JSON 比,而不是和 XML 比

XML 是更早的序列化格式,当年 SOAP、配置文件里到处都是它的身影。但 XML 有个致命问题:标签冗余实在太严重了。一个简单的用户信息,在 XML 里可能要写<name>ZhangSan</name>,开标签、闭标签写两遍,光标签名就占了二十几个字节。JSON 出来之后,开发者的心智立刻被它占据了,因为同样的数据,JSON 用{"name":"ZhangSan"}就搞定了,小了一半还不止。

所以到现在,绝大多数人脑海里“文本序列化”的默认选项就是 JSON,而不是 XML。真正会拿来和 Protobuf 对比的,一定是 JSON。再往后你会发现,JSON 在文本格式里已经算非常精简了,但它在 Protobuf 面前还是显得“啰嗦”,因为 Protobuf 直接把文本这层皮给脱了。

2. Protobuf 和 JSON 字节大小实测:同一个用户对象差多少

2.1 准备一个真实场景的数据结构

为了比较,我构造一个非常常见的用户信息对象,包含用户 ID、昵称、邮箱、年龄、是否 VIP、感兴趣的标签列表。这种结构在几乎任何一个带用户体系的系统里都能见到,拿来比较最有说服力。

先用 JSON 的视角看,这段数据长这样:

{"id":123456,"name":"ZhangSan","email":"zhangsan@example.com","age":30,"isVip":true,"tags":["tech","coding","reading"]}

为了公平对比,这里用的是“紧凑模式”JSON,去掉所有多余空格和缩进。你要是把 JSON 格式化成人眼友好模式,字节数还会更高,但紧凑模式已经是 JSON 能拿出的最好成绩了。

如果用 Protobuf,需要先写一份.proto定义:

syntax = "proto3"; message User { int32 id = 1; string name = 2; string email = 3; int32 age = 4; bool is_vip = 5; repeated string tags = 6; }

注意这里每个字段后面都有一个数字编号,1、2、3、4、5、6,这是 Protobuf 的关键设计,后面会详细解释。现在先看结果。

2.2 JSON 的序列化结果:119 字节

我把上面那段紧凑 JSON 逐字符数了一下,总共 119 字节。这 119 字节里包括什么?包括六个字段名(id、name、email、age、isVip、tags),包括每个字段名必须配的引号和冒号,包括对象的花括号、数组的方括号、每一对数据之间的逗号,然后才是真正的业务数据:用户 ID 的123456、字符串ZhangSan、邮箱地址、数字30、布尔值true,以及三个标签单词。

也就是说,JSON 为了让自己“可读”,必须反复地把字段名写在每一段数据里。一次传输还好,如果是一百万次调用,这些字段名字符串就会随着每一个消息在网络里反复跑,这部分成本是纯开销。

2.3 Protobuf 的序列化结果:63 字节

同样的数据,用上面那份.proto生成代码序列化,得到的是 63 字节二进制。直接看十六进制的话,长这样:

08 C0 C4 03 12 08 5A 68 61 6E 67 53 61 6E 1A 14 7A 68 61 6E 67 73 61 6E 40 65 78 61 6D 70 6C 65 2E 63 6F 6D 20 1E 28 01 32 04 74 65 63 68 32 06 63 6F 64 69 6E 67 32 07 72 65 61 64 69 6E 67

我数了一下,63 字节。对比 JSON 的 119 字节,小了大概 47%。也就是说,在同样的网络带宽下,用 Protobuf 传输这批数据,流量直接砍掉将近一半。这不是我精心构造的极端例子,就是非常普通的用户对象,字段名也不算长,如果字段起名再长一点,比如userProfileAvatarUrl这种,差距会更夸张。

2.4 逐字节拆开看 Protobuf 的布局

光看字节数还不够,我得拆开让各位明白这 63 字节是怎么来的。我按字段一行一行拆:

十六进制内容含义
08 C0 C4 03字段 id,编号 1,varint 类型,值 123456
12 08 5A 68 61 6E 67 53 61 6E字段 name,编号 2,字符串类型,长度 8,值 “ZhangSan”
1A 14 ... 6F 6D字段 email,编号 3,字符串类型,长度 20,邮箱地址
20 1E字段 age,编号 4,varint 类型,值 30
28 01字段 is_vip,编号 5,varint 类型,值 true
32 04 74 65 63 68字段 tags 的第一项,字符串 “tech”
32 06 63 6F 64 69 6E 67字段 tags 的第二项,字符串 “coding”
32 07 72 65 61 64 69 6E 67字段 tags 的第三项,字符串 “reading”

可以清楚地看到,每个字段在 Protobuf 里都由“编号 + 类型 + 实际内容”构成。08里的0表示这是字段 1,8表示类型是 varint;12表示这是字段 2,类型是长度分隔;1A表示这是字段 3,类型同样长度分隔。整个序列化结果里,完全没有任何字段名字符串,只有数字编号和二进制内容。

3. 为什么 Protobuf 能比 JSON 小一半:三个核心编码手段

3.1 第一个杀招:字段名换成数字编号

JSON 之所以大,最大的原因就是必须重复字段名。"userProfileAvatarUrl"这样一个字段,在 JSON 里要原封不动写出来,甚至在一个数组里几百个对象出现几百次。Protobuf 则完全绕开了字段名,它在.proto里给每个字段绑定一个数字编号,序列化时只写编号。

拿上面的例子来说,JSON 里六个字段名id、name、email、age、isVip、tags本身加上引号、冒号,大概占了四十多字节。而 Protobuf 里,每个字段的编号加类型只占 1 个字节开头。就这一下,大头就省下来了。

提示:字段编号越小,tag 编码越短。字段编号 1 到 15 时,tag 只需要 1 个字节;16 到 2047 时,tag 需要 2 到 3 个字节。所以设计.proto时,把最频繁出现的字段编到 1 到 15,是省字节的最直接手段。

3.2 第二个杀招:Varint 变长整数

JSON 里写数字是直接写成十进制文本。123456在 JSON 里就是 6 个字符,占 6 字节。但 Protobuf 对整数用一种叫 Varint 的变长编码:数值越小,占的字节越少。123456用 Varint 编码只需要 3 字节,30只需要 1 字节,布尔值true也是 1 字节。

Varint 的原理不复杂:每个字节只用低 7 位存数据,最高位作为“续位标记”,如果最高位是 1,说明后面还有后续字节;如果最高位是 0,说明这个整数到此结束。这样 1 到 127 的整数只需 1 字节,128 到 16383 只需 2 字节,依此类推。对比 Java 里int永远占 4 字节、long永远占 8 字节,Varint 对大多数业务场景中“数值其实都不大”的整数非常友好。

在刚才的例子中,JSON 写"123456"用了 6 字节,Protobuf 用 3 字节;JSON 写"30"用了 2 字节,Protobuf 用 1 字节;JSON 写"true"用了 4 字节,Protobuf 用 1 字节。积少成多,差距就出来了。

3.3 第三个杀招:Wire Type 把类型信息压进 tag 里

Protobuf 里每个字段的 tag 本身不仅包含字段编号,还包含一个 3 位的 wire type,用来告诉解析方“后面这个字段的数据长什么样”。常见的 wire type 有这几种:

类型编号含义典型场景
0Varintint32、int64、bool、enum
164 位定长fixed64、double
2长度分隔string、bytes、repeated 嵌套消息
532 位定长fixed32、float

一个 tag 字节里,低 3 位是 wire type,高位是字段编号。这样解析器读到08就知道字段 1 是 varint,读到12就知道字段 2 是长度分隔字符串。JSON 靠引号、冒号这类标点来识别结构,Protobuf 靠 tag 里的二进制位来识别结构,后者的信息利用率高得多,这也是它更紧凑的原因之一。

3.4 二进制不可读,其实是刻意选的成本

有人会问:Protobuf 直接把字段名丢掉了,那调试的时候怎么知道这段字节是啥?这确实是它的代价,二进制格式人眼不可读,也没有自描述能力。拿到一段 Protobuf 二进制,如果不提前知道对应的.proto文件,你是完全看不懂的。

但这个“缺点”恰恰是它字节小的前提。JSON 之所以可读,是因为它把每一位结构信息都用人眼友好的文本写出来了;Protobuf 放弃人眼友好,换来的是字段名、标点、结构标记全部优化成紧凑二进制。对机器来说,解析一个“字段 2,长度 8,读 8 个字节”的指令,比扫描大段文本中匹配引号和冒号要快得多。

4. 什么时候别盲目换 Protobuf:场景选型与避坑指南

4.1 该用 Protobuf 的场景

如果你是内部服务之间的高并发 RPC,每次请求都是一份不小的结构化数据,那么 Protobuf 几乎是最优解。它字节小,意味着同样的带宽可以承载更高的 QPS;序列化反序列化速度快,意味着 CPU 开销更低;有.proto作为契约,意味着服务端和客户端不会悄悄改字段改出问题。

移动端和服务端之间通信也值得考虑 Protobuf,尤其是在网络环境不稳定、流量套餐宝贵的场景下,省下来的流量是用户可感知的。大数据场景里,把数据落盘时用 Protobuf 或者更专门的二进制格式,也能显著减少存储成本。

4.2 继续用 JSON 的场景

如果你的接口要面对浏览器环境,几乎没得选,JSON 就是事实标准。JavaScript 原生支持 JSON,你给一个网页发 Protobuf 二进制,前端还得引入额外解码库和.proto文件,体验直线下降。公开 API 也建议继续用 JSON,因为外部开发者拿到你的接口文档就能直接调试,不用先学一套 Protobuf 工具链。

配置类数据、日志类数据、一次性导入导出的数据,也请老老实实用 JSON。这类场景强调“随时能被人看懂、方便脚本处理”,哪怕多占点磁盘或带宽,也比让运维同学在凌晨三点对着二进制抓瞎要强。

4.3 四个实战中容易踩的坑

第一个坑是随意改字段编号。.proto一旦发布,字段编号就是协议的一部分,后端改了编号,旧客户端按老编号解析,直接数据错乱。正确的是把废弃字段reserved掉,而不是删除重用编号。

第二个坑是更新兼容性。你的规则应该是:只能新增字段,不能删除或修改已有字段的类型。新增字段用新的编号,老客户端解析时会自动忽略它,新客户端解析老数据时用默认值填充,这样协议才能平滑升级。

第三个坑是调试工具链没跟上。JSON 可以直接curl看,Protobuf 不行,你得配 grpcurl、反射、或者用各类工具把二进制转成 JSON 再排查。如果团队以前全是 JSON 习惯,切换 Protobuf 后要第一时间把调试链路建立起来,否则出问题会非常痛苦。

第四个坑是语言绑定差异。有的语言对 Protobuf 的运行时支持很成熟,速度很快;有的语言则主要依赖纯运行时实现,性能并不一定比 JSON 快。做技术选型前,最好先在自己目标语言里跑一下基准测试,别光看二进制大小优化,结果序列化耗时不降反升。

4.4 字节变小之外,还要看整体性能

字节大小是 Protobuf 最直观的优势,但“序列化方案”到最后还是要看综合表现。序列化速度、反序列化速度、内存分配次数、CPU 缓存友好度,都是关键指标。很多做过压测的人会发现,文本格式在小对象上的性能差距没有想象中大,因为对象小、网络开销主导;而在大对象和很深的嵌套结构上,二进制格式的优势才彻底拉开。

还有一个容易被忽视的事实:如果所有数据都在传输前做了 gzip 压缩,JSON 和 Protobuf 的体积差距会明显缩小,因为 JSON 里大量重复的字段名会被压缩算法吃掉。但压缩本身消耗 CPU,而且 Protobuf 即使压缩后通常仍然更小,只是差距从“一倍”变成“百分之二三十”。所以别拿“反正要压缩”当不选 Protobuf 的借口,也别拿“Protobuf 小很多”当不搞压缩的挡箭牌,两者是不同层面的优化。

5. 最后分享几点实战体会

我实际用下来,最大的感受是:Protobuf 的收益是确定的,但成本也是确定的。确定收益在字节大小、解析速度、接口契约;确定成本在工具链、学习曲线、调试复杂度。如果你在一个全是 JSON 的团队里突然引入 Protobuf,最大的阻力往往不是技术,而是同伴们每一次“这个二进制到底啥意思”的崩溃。

所以我的建议是:别把 Protobuf 当银弹,先看场景。内部高 QPS RPC、存储格式、跨语言强契约接口,大胆用;浏览器、公开 API、一次性脚本、配置管理,继续用 JSON,不丢人。若真要切换,先把.proto的版本管理和调试工具搭好,再逐步迁移,别一夜之间把所有接口都换掉。

另外一个小技巧:如果只是想快速体验 Protobuf 的效果,不一定要立刻搭一套工程环境。你可以在线把.proto转成各种语言的代码,再用本地 Python 或 Node 脚本跑一小段序列化对比,几分钟就能得到和你业务真实数据非常接近的字节数结论。数据不会骗人,自己跑一次,比看十篇对比文章都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询