从 JSON 到 Protobuf 的选择与实践

在构建高并发、低延迟的后端系统时,数据传输协议的选择往往决定了系统的性能上限和维护成本。最近,我将项目的核心服务从传统的 JSON 协议全面迁移到了 Protocol Buffers (Protobuf)

这篇文章记录了这次迁移背后的思考、遇到的坑以及最终的架构收益。

为什么放弃 JSON?

JSON 是 Web 开发的“通用语”,它直观、易读、调试方便。但在高负载的情况下,它的弱点也愈发明显:

  1. 体积臃肿:大量的冗余字符(双引号、冒号、空格)浪费了带宽,尤其是在高频率同步位置和状态时。
  2. 缺乏严格约束:前端和后端对同一个字段的类型理解可能存在偏差(例如 ID 应该是 int64 还是 string),这种隐错往往在运行时才会报错。
  3. 序列化性能:对于大规模数据,JSON 的解析开销在 CPU 密集型的后端中不可忽视。
    总之主要就是,费钱。

协议选型:多维度对比

在决定使用 Protobuf 之前,我对比了主流的几种序列化方案:JSON、MessagePack 和 Protobuf。

维度 JSON MessagePack Protobuf
序列化格式 文本 (UTF-8) 二进制 二进制
Schema 约束 无 (弱类型) 无 (动态类型) 必须 (强类型)
序列化速度 极快
消息体积 极小
可读性/调试 极佳 (原生支持) 较差 (需工具) 差 (需 .proto 文件)
跨语言支持 完美 优秀 优秀

为什么最终选择了 Protobuf?
虽然 MessagePack 在不需要预定义 Schema 的情况下也能提供不错的性能和压缩比,但Protobuf显然更好。另外对于我们这种需要多端协同(Go/TS)的项目,Protobuf 的强类型契约价值更高。它能让我们在编译阶段而非运行阶段发现接口变更导致的问题。他唯一不好的地方就是,需要预定义Schema,写起来不太方便。而我们现在完全可以利用AI辅助,这个缺点也就不存在了。

Protobuf 的“契约式”开发

Protobuf 的核心是 .proto 定义文件。它不仅仅是一个协议,更是一份强有力的技术契约

1
2
3
4
5
6
7
message User {
int64 id = 1;
string username = 2;
string nickname = 3;
int32 gold = 4;
int64 login_time = 9; // Unix 毫秒级时间戳
}

通过这一份文件,我同时生成了:

  • Go 代码:用于后端服务之间以及与数据库的交互。
  • TypeScript 代码:前端直接获得完整的类型提示,杜绝了“拼错字段名”的情况。

实践中的关键环节

1. 后端的“双模”处理

在初期迁移时,为了保证兼容性,我在 Go 后端实现了一个简单的分流逻辑:首选 ProtoBuf 解析,失败则回退 JSON。

1
2
3
4
5
6
7
8
9
func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var req pb.SomeRequest

// 优先尝试反序列化 Protobuf
if err := proto.Unmarshal(body, &req); err != nil {
// Fallback to JSON...
}
}

2. 那个令人头疼的 “Illegal Tag” 报错

在迁移过程中,前端频繁报出一个错误:illegal tag: field no 13 wire type 7

排查结论:这是典型的“跨频道对话”。
当后端因为 Token 失效或 401 错误通过 JSON 返回错误信息(如 {"error":"..."})时,前端却尝试将其作为二进制 Protobuf 解析。解析器在二进制流中撞上了字母 "o",根据编码规则,它把 "o" 误认为了一个非法的标签。

解决方案:在前端封装一层 Content-Type 检查,确保只有在 Header 为 application/x-protobuf 时才启动二进制解码,否则作为 JSON 处理报错。

3. 服务发现的深度集成

我们不仅在业务接口使用了 Protobuf,甚至在 Service Discovery (调度寻址) 环节也引入了它。

现在的调度流程是:

  1. 前端请求 Lobby 调度中心。
  2. Lobby 以 Protobuf 二进制格式返回目标服务器地址 (dev-s1, dev-s2 等)。
  3. 前端将此结果缓存,并按需建立连接。

总结:优化存量,规范增量

通过这次改造,项目的整体通信效率提升了约 30%,更重要的是,代码的鲁棒性提升了一个量级

在实际应用中,我的心得如下:

  • 存量老项目:没必要全量重构。针对数据量大、请求频率高的“负荷重”部分进行 Protobuf 优化,性能提升立竿见影。
  • 新项目:不要犹豫,直接全量使用 Protobuf。从一开始就建立起强类型的通讯契约,能规避后期大量的维护成本。

Protobuf 确实增加了编译步骤(需要 protoc 一下),但相比它带来的性能和工程质量收益,这点维护成本微不足道。


无论是为了性能优化还是代码规范,Protobuf 都是现代分布式系统的必选项。


从 JSON 到 Protobuf 的选择与实践
https://erdianzhang.cn/2026/03/10/from-json-to-protobuf/
作者
兔特科技
发布于
2026年3月10日
许可协议