Protobuf 是什么?一篇文章搞懂这个高性能序列化神器
在聊 RPC 的时候,我们提到它会用 Protobuf 这类序列化协议来提升性能。很多人第一次听到 Protobuf 会有点懵:它到底是什么?和 JSON 有啥区别?为什么 RPC 都爱用它?今天咱们就把这些问题说透。
一、Protobuf 本质:高效的「二进制序列化协议」
Protobuf 全称 Protocol Buffers,是 Google 开源的一种语言无关、平台无关、可扩展的序列化机制。简单来说,它的核心作用就是:
把内存里的复杂数据结构(比如对象、结构体),转换成紧凑的二进制字节流,方便网络传输或持久化存储;反过来,也能把字节流还原成原来的数据结构(反序列化)。
🌰 生活化比喻:JSON 是书信,Protobuf 是电报
- JSON:就像用自然语言写的书信,内容清晰易读,但有很多冗余的字符(比如
{}、""、逗号),体积大,传输慢。 - Protobuf:就像用密文写的电报,只有懂「密码本」(
.proto定义文件)的人才能看懂,体积小、传输快,但可读性差。
二、Protobuf 核心优势:为什么 RPC 都爱用它?
相比 JSON、XML 这类文本序列化格式,Protobuf 有几个关键优势,刚好契合 RPC 「高性能、跨语言、可兼容」的需求:
1. 🚀 体积更小、速度更快
Protobuf 是二进制格式,没有 JSON 里的冗余字符,相同数据的体积比 JSON 小 3~10 倍,序列化 / 反序列化速度快 20~100 倍。
举个例子:
一个包含 name(字符串)、age(整数)、email(字符串)的用户对象,用 JSON 序列化后可能是:
{ "name":"张三","age":25,"email":"zhangsan@example.com"}用 Protobuf 序列化后,只是一串二进制字节,长度更短,解析更快。
这种性能提升在高并发的微服务场景下(比如 RPC 调用),能显著降低网络带宽消耗和延迟。
2. 🌍 跨语言、跨平台
只要你用 .proto 文件定义好数据结构,就能通过 Protobuf 编译器(protoc)生成 Java、Python、Go、C++ 等几十种语言的代码。
这意味着:
- 你的微服务可以用 Go 写,另一个用 Java 写,它们通过 Protobuf 序列化后,就能互相通信,不用关心语言差异。
- 这对大型分布式系统、多语言混合开发的场景非常友好。
3. 🛡️ 向后兼容,服务升级不翻车
Protobuf 支持增量更新字段,且保证新旧版本兼容:
- 新增字段时,旧版本的客户端会自动忽略新增的字段;
- 旧版本的字段被删除时,新版本的客户端会给缺失的字段用默认值(比如
int32默认为 0,string默认为空)。
这种兼容性让微服务升级时不用停服,也不用同步更新所有依赖的服务,大大降低了运维风险。
4. 📊 强类型,避免类型错误
在 .proto 文件里,你需要明确定义每个字段的类型(比如 int32、string、bool),编译器会生成强类型的代码。
比如定义一个用户消息:
message User {
string name = 1; // 字段编号1,类型string
int32 age = 2; // 字段编号2,类型int32
string email = 3; // 字段编号3,类型string
}
生成的代码会严格校验类型,避免了 JSON 里常见的「字符串转数字失败」等问题。
三、Protobuf 工作流程:从定义到使用(Java 版本)
1. 编写 .proto 文件:定义数据结构
先保持 .proto 文件内容不变(这是跨语言的核心,也是 Protobuf 的优势),文件名建议命名为 user.proto:
syntax = "proto3"; // 使用Protobuf 3版本
package user; // 包名,避免命名冲突
option java_package = "com.example.protobuf"; // 生成Java代码的包路径
option java_outer_classname = "UserProto"; // 生成的外层类名(必须大写开头)
// 定义用户消息
message User {
string name = 1;
int32 age = 2;
repeated string tags = 3; // repeated表示数组/列表类型
}
// 定义获取用户的请求和响应
message GetUserRequest {
int32 user_id = 1;
}
message GetUserResponse {
User user = 1;
bool success = 2;
string error_msg = 3;
}
注意:Java 版本需要额外添加
java_package和java_outer_classname两个选项,指定生成代码的包路径和外层类名,否则会按默认规则生成,不利于项目管理。
2. 用 protoc 生成 Java 代码
前置条件:
- 下载 Protobuf 编译器
protoc(https://github.com/protocolbuffers/protobuf/releases),并配置到系统环境变量; - 确保本地有 Java 开发环境(JDK 8+)。
生成命令:
在 user.proto 文件所在目录执行以下命令:
# 生成Java代码,--java_out 后指定生成代码的输出目录(这里是当前目录下的src/main/java)
protoc --java_out=./src/main/java user.proto
执行完成后,会在 src/main/java/com/example/protobuf/ 目录下生成 UserProto.java 文件,这个文件包含了所有消息类的定义和序列化 / 反序列化方法,无需手动修改。
3. 在 Java 代码中使用(核心示例)
第一步:引入 Protobuf 依赖(Maven/Gradle)
Java 项目需要引入 Protobuf 的核心依赖,才能使用生成的代码:
Maven(pom.xml):
<dependencies>
<!-- Protobuf 核心依赖 -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.25.3</version> <!-- 建议和protoc版本一致 -->
</dependency>
</dependencies>
Gradle(build.gradle):
dependencies {
implementation 'com.google.protobuf:protobuf-java:3.25.3'
}
第二步:编写 Java 示例代码
package com.example.protobuf;
import com.google.protobuf.InvalidProtocolBufferException;
public class ProtobufJavaDemo {
public static void main(String[] args) throws InvalidProtocolBufferException {
// ====================== 1. 序列化:将对象转为二进制字节流 ======================
// 1.1 创建User对象(通过Builder构建,Protobuf生成的类默认不可变)
UserProto.User user = UserProto.User.newBuilder()
.setName("张三") // 设置字符串字段
.setAge(25) // 设置整数字段
.addTags("程序员") // 添加数组元素(单个添加)
.addTags("Java") // 继续添加数组元素
.build(); // 构建不可变的User对象
// 1.2 序列化:调用toByteArray()方法,得到二进制字节数组
byte[] serializedData = user.toByteArray();
System.out.println("序列化后的字节数组长度:" + serializedData.length); // 体积远小于JSON
// ====================== 2. 反序列化:将二进制字节流转回对象 ======================
// 2.1 反序列化:调用parseFrom()方法,从字节数组还原User对象
UserProto.User deserializedUser = UserProto.User.parseFrom(serializedData);
// 2.2 验证反序列化结果
System.out.println("反序列化后的姓名:" + deserializedUser.getName());
System.out.println("反序列化后的年龄:" + deserializedUser.getAge());
System.out.println("反序列化后的标签列表:" + deserializedUser.getTagsList());
}
}
代码关键解释:
- 对象构建:Protobuf 生成的 Java 类是不可变的,必须通过
newBuilder()构建器模式创建对象,设置字段后调用build()得到最终对象; - 序列化核心方法:
toByteArray()—— 将对象转为二进制字节数组(网络传输 / 存储的核心格式); - 反序列化核心方法:
parseFrom(byte[] data)—— 从字节数组还原对象,会抛出InvalidProtocolBufferException(字节流损坏 / 格式错误时触发); - 数组字段操作:
repeated类型在 Java 中对应List,通过addTags()单个添加,或addAllTags(List<String>)批量添加,获取时用getTagsList()。
运行结果示例:
序列化后的字节数组长度:20
反序列化后的姓名:张三
反序列化后的年龄:25
反序列化后的标签列表:[程序员, Java]
Java 中使用 Protobuf 需先通过 protoc 生成代码,且 .proto 文件要指定 java_package 和 java_outer_classname 确保代码结构规范;
核心操作:通过 newBuilder() 构建对象,toByteArray() 序列化,parseFrom() 反序列化;
Protobuf 生成的 Java 类是不可变的,数组字段需通过 addXXX()/addAllXXX() 操作,获取时用 getXXXList()。
四、Protobuf vs JSON:怎么选?
| 特性 | Protobuf | JSON |
|---|---|---|
| 格式 | 二进制 | 文本 |
| 体积 | 小(3~10 倍于 JSON) | 大 |
| 速度 | 快(20~100 倍于 JSON) | 慢 |
| 可读性 | 差(需要工具解析) | 好(人类可直接阅读) |
| 跨语言 | 原生支持 | 原生支持 |
| 向后兼容 | 原生支持 | 需要自己实现 |
| 适用场景 | RPC、微服务、内部通信 | 前后端交互、API 对外提供 |
简单总结:
- ✅ 内部服务通信(如 RPC):选 Protobuf,追求性能和效率。
- ✅ 对外 API、前后端交互:选 JSON,追求可读性和兼容性。
五、总结
Protobuf 是为「高性能、跨语言、可兼容」的序列化场景设计的,尤其适合 RPC、微服务、分布式系统等需要高效通信的场景。它不是要取代 JSON,而是在特定场景下提供更好的选择。
搞懂它的核心优势和工作流程,你就能明白为什么 RPC 框架(如 gRPC、Dubbo)都把 Protobuf 作为默认序列化协议了。