什么是 Protocol Buffers?

Protocol Buffers(简称 protobuf) 是 Google 开发的一种语言中立、平台无关的结构化数据序列化机制。它使用 .proto 文件定义数据结构,通过编译器生成多语言代码,实现高效的数据交换。

核心特点

特性 说明
高效紧凑 二进制编码,数据体积约为 JSON 的 1/10,XML 的 1/20
解析快速 解析速度比 JSON 快 5-10 倍,比 XML 快 20-100 倍
跨语言 支持 C++、Java、Python、Go、JavaScript 等多种语言
强类型 编译期类型检查,避免运行时错误
可扩展 支持协议升级,向后兼容

proto 文件是什么?

.proto 文件是 Protocol Buffers 的接口定义文件,用于描述数据结构。它类似于数据库的表结构定义,但更加灵活和强大。

基本语法示例(proto3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
syntax = "proto3";

package tutorial;

// 定义一个 Person 消息
message Person {
string name = 1; // 字段类型 + 字段名 + 字段编号
int32 id = 2;
string email = 3;

// 嵌套消息
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}

repeated PhoneNumber phones = 4; // 重复字段(数组)
}

// 枚举类型
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}

// 定义地址簿
message AddressBook {
repeated Person people = 1;
}

关键概念

  1. 字段编号(Field Number):每个字段都有唯一的编号,用于二进制编码识别,一旦确定不可随意更改
  2. 字段规则
    • singular:默认,0 或 1 个值
    • repeated:0 或多个值(数组)
  3. 数据类型int32int64stringbytesboolenum

工作原理

编码机制

Protobuf 采用 Tag-Length-Value(TLV) 结构进行编码:

1
[Tag] [Length] [Value]

1. Tag 编码

1
Tag = (field_number << 3) | wire_type
Wire Type 含义 适用类型
0 (Varint) 变长编码 int32, int64, bool, enum
1 (64-bit) 固定64位 double, fixed64
2 (Length-delimited) 长度前缀 string, bytes, 嵌套消息
5 (32-bit) 固定32位 float, fixed32

2. Varints 变长编码

小数值使用更少的字节存储:

1
2
数字 1:0000 0001(1字节)
数字 666:1001 1010 0000 0101(2字节)

编码规则:每个字节的最高位(MSB)为标志位

  • 1:还有后续字节
  • 0:这是最后一个字节

3. ZigZag 编码(处理负数)

负数直接编码会占用大量空间(如 -5 占 10 字节)。ZigZag 将负数映射为正数:

1
2
原始值 → ZigZag 编码 → Varints 编码
-5 → 9 → 仅 2 字节

使用 sint32/sint64 类型自动启用 ZigZag 编码。

为什么 protobuf 高效?

  1. 无字段名:只存储字段编号,不存储字段名
  2. 二进制格式:无需解析文本,直接按类型读取
  3. 变长编码:小数值占用更少空间
  4. 预编译:生成静态类型代码,运行时无需反射

如何使用?

1. 安装 Protocol Buffers 编译器

1
2
3
4
5
6
7
8
# macOS
brew install protobuf

# Ubuntu/Debian
apt-get install -y protobuf-compiler

# 验证安装
protoc --version

2. 编写 .proto 文件

创建 person.proto

1
2
3
4
5
6
7
syntax = "proto3";

message Person {
string name = 1;
int32 age = 2;
repeated string hobbies = 3;
}

3. 编译生成代码

1
2
3
4
5
6
7
8
9
10
11
# 生成 Python 代码
protoc --python_out=. person.proto

# 生成 Go 代码
protoc --go_out=. person.proto

# 生成 Java 代码
protoc --java_out=. person.proto

# 生成多种语言
protoc --python_out=. --go_out=. --java_out=. person.proto

4. 在代码中使用(Python 示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import person_pb2

# 创建对象
person = person_pb2.Person()
person.name = "张三"
person.age = 25
person.hobbies.extend(["读书", "编程", "旅行"])

# 序列化(转为二进制)
serialized_data = person.SerializeToString()
print(f"序列化后大小: {len(serialized_data)} 字节")

# 反序列化(从二进制恢复)
new_person = person_pb2.Person()
new_person.ParseFromString(serialized_data)
print(f"姓名: {new_person.name}, 年龄: {new_person.age}")

5. 与 JSON 对比

1
2
3
4
5
6
7
8
9
10
11
12
import json

# 相同数据的 JSON 表示
json_data = json.dumps({
"name": "张三",
"age": 25,
"hobbies": ["读书", "编程", "旅行"]
})

print(f"JSON 大小: {len(json_data)} 字节")
print(f"Protobuf 大小: {len(serialized_data)} 字节")
print(f"压缩比: {len(json_data) / len(serialized_data):.1f}x")

版本兼容性

向后兼容规则

操作 兼容性 说明
新增字段 ✅ 兼容 旧代码忽略新字段,新代码使用默认值
删除字段 ⚠️ 需谨慎 标记为 reserved 避免编号复用
修改字段类型 ❌ 不兼容 可能导致数据截断或解析错误
修改字段编号 ❌ 不兼容 会导致数据错乱

reserved 关键字

1
2
3
4
5
6
7
message Foo {
reserved 2, 15, 9 to 11; // 保留字段编号
reserved "foo", "bar"; // 保留字段名

string name = 1;
// int32 id = 2; // 不能再用 2
}

适用场景

✅ 推荐使用

  • 微服务通信:gRPC 默认使用 protobuf
  • 游戏开发:高频网络传输,需要极致性能
  • 物联网:带宽受限环境
  • 大数据存储:列式存储、日志收集
  • 移动端:减少流量消耗

❌ 不适合使用

  • 人类可读的配置文件:二进制不可读
  • Web 前端直接解析:需要额外库支持
  • 动态类型需求:schema 变更需要重新编译

与 JSON/XML 对比

特性 Protobuf JSON XML
数据体积 ⭐⭐⭐ 极小 ⭐⭐ 中等 ⭐ 较大
解析速度 ⭐⭐⭐ 极快 ⭐⭐ 中等 ⭐ 较慢
可读性 ⭐ 二进制 ⭐⭐⭐ 文本 ⭐⭐⭐ 文本
类型安全 ⭐⭐⭐ 强类型 ⭐ 弱类型 ⭐⭐ Schema
跨语言 ⭐⭐⭐ 优秀 ⭐⭐⭐ 优秀 ⭐⭐⭐ 优秀
学习成本 ⭐⭐ 中等 ⭐⭐⭐ 低 ⭐⭐ 中等

学习资源


总结

Protocol Buffers 是现代分布式系统中高性能数据交换的首选方案。通过 .proto 文件定义数据结构,利用二进制编码实现极致的传输效率,同时保持跨语言和版本兼容性。

核心要点

  1. .proto 文件是结构定义,不是代码
  2. 字段编号一旦确定就不能更改
  3. 优先使用 proto3 语法(更简洁)
  4. 与 gRPC 配合使用效果更佳

本文整理于 2026-03-29,参考资料来自腾讯云开发者社区和 CSDN 博客