引言
本文介绍了 Protobuf 的基本概念和编解码原理。原理部分使用了直观的表格解析编码过程,帮助开发者在快速掌握核心机制。
一、Protobuf 介绍
Protobuf 全称 Protocol Buffer(后文简称 PB),是由 Google 开发的一种语言和平台无关的数据序列化协议。
它能将结构化数据序列化为更简洁的二进制格式,用于数据存储和网络通信。
与 JSON 或 XML 相比,PB 更高效,但是会牺牲可读性。
使用 PB 前,必须预先定义好数据结构,然后通过工具生成对应语言的代码来读写数据。
官网链接:protobuf.dev/
💡预定义结构示意:
使用 PB 的第一步,就是需要预先定义好你的数据结构。创建一个 .proto 文件。在这个文件里,你需要明确描述数据的结构,比如有哪些字段,它们是什么类型。
ini
syntax = "proto3";
message Account {
int32 id = 1;
string name = 2;
string email = 3;
AccountType accountType = 4;
enum AccountType {
TYPE_1 = 0;
TYPE_2 = 1;
TYPE_3 = 2;
TYPE_4 = 3;
}
}
需要注意的是,在 proto3 中,不能显式定义自定义默认值 ,字段后面的数字为字段的编号,并非默认值,编号不能重复。
更多编写规范可以参考官方文档: protobuf.dev/programming...
💡生成平台代码示意:
定义好结构之后,Protobuf 提供的编译器工具就能根据这个 .proto 文件,自动生成你所需编程语言的代码。这些生成的代码包含了用于读写你定义的数据结构的方法。下面以 Kotlin 使用 wire 为例:
kotlin
public class Account(
@field:WireField(tag = 1,adapter = "com.squareup.wire.ProtoAdapter#INT32",label = WireField.Label.OMIT_IDENTITY,schemaIndex = 0,)
public val id: Int = 0,
@field:WireField(tag = 2,adapter = "com.squareup.wire.ProtoAdapter#STRING",label = WireField.Label.OMIT_IDENTITY,schemaIndex = 1,)
public val name: String = "",
@field:WireField(tag = 3,adapter = "com.squareup.wire.ProtoAdapter#STRING",label = WireField.Label.OMIT_IDENTITY,schemaIndex = 2,)
public val email: String = "",
@field:WireField(tag = 4,adapter = "com.example.mykuikly.proto.Account${'$'}AccountType#ADAPTER",label = WireField.Label.OMIT_IDENTITY,schemaIndex = 3,)
public val accountType: AccountType = AccountType.TYPE_1,
unknownFields: ByteString = ByteString.EMPTY,
) : Message<Account, Nothing>(ADAPTER, unknownFields) {
public companion object {
@JvmField
public val ADAPTER: ProtoAdapter<KAccount> = object : ProtoAdapter<KAccount>(
FieldEncoding.LENGTH_DELIMITED,
KAccount::class,
"type.googleapis.com/com.netease.mail.mmsharedkmp.proto.KAccount",
PROTO_3,
null,
"com/netease/mail/mmsharedkmp/proto/account.proto"
) {
override fun encodedSize(`value`: KAccount): Int {//省略}
override fun encode(writer: ProtoWriter, `value`: KAccount) {//省略}
override fun encode(writer: ReverseProtoWriter, `value`: KAccount) {//省略}
override fun decode(reader: ProtoReader): KAccount {//省略}
override fun redact(`value`: KAccount): KAccount = //省略
}
}
//其他部分省略
}
可以看到,工具帮我们自动生成了.proto 中定义的数据结构,包含了字段和用来编码解码的方法。
💡编码后数据示意
如上文所说,PB 会将我们的结构化数据序列化成一种非常紧凑的二进制格式。但是可读性会很差:

可以看到,PB 编码后已经无法直接看出内部的数据含义了。
二、编解码原理
我们以上面定义的数据结构为例,PB 编码后数据是一个紧凑的二进制格式,他的字节数组十进制表示如下:
| PB 编码后数据 | [ 8, 1, 18, 4, 110, 97, 109, 101, 26, 12, 116, 101, 115, 116, 64, 49, 54, 51, 46, 99, 111, 109 ](22 字节) |
|---|
💡 PB 编码过程解析
为了方便演示,我将每个十进制表示的字节,转换成二进制,放在表的第一列;
第二列为 每个字节解析出的内容,以及解释;
为了参考对照,第三列附了 ASCII 码表。
阅读时建议从上到下,左右对照。

💡 PB 编码的优化策略
通过前面的分析,我们得到了 PB 压缩后的数据内容,如下:
perl
{
"1" : 1,
"2" : "name" ,
"3" : "test@163.com"
}
从这份数据中,我们可以观察到两个值得注意的现象:
-
数据中没有出现字段名
-
accountType 字段似乎不见了
这主要是因为 PB 在序列化时的优化策略:
具体来说,PB 不需要存储字段名,因为字段名已经在生成的代码中定义好了,通过 ID 就能直接映射到对应的字段,这样避免了冗余信息。
另外,PB 在压缩过程中会省略值为默认值的字段;我们传递的 accountType 值为 TYPE_1,其数值为 0,恰好是默认值,因此它没有被包含在输出数据中。
三、总结
总结 Protobuf 编解码原理,它通过精简数据表示和省略冗余信息,显著提升性能。Protobuf 是一种高效实用的序列化工具,在开发中能大幅优化资源利用。