10分钟,掌握Protobuf编解码原理

引言

本文介绍了 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"
}

从这份数据中,我们可以观察到两个值得注意的现象:

  1. 数据中没有出现字段名

  2. accountType 字段似乎不见了

这主要是因为 PB 在序列化时的优化策略:

具体来说,PB 不需要存储字段名,因为字段名已经在生成的代码中定义好了,通过 ID 就能直接映射到对应的字段,这样避免了冗余信息。

另外,PB 在压缩过程中会省略值为默认值的字段;我们传递的 accountType 值为 TYPE_1,其数值为 0,恰好是默认值,因此它没有被包含在输出数据中。

三、总结

总结 Protobuf 编解码原理,它通过精简数据表示和省略冗余信息,显著提升性能。Protobuf 是一种高效实用的序列化工具,在开发中能大幅优化资源利用。

相关推荐
9号达人1 小时前
@NotBlank 不生效报错 No validator could be found:Hibernate Validator 版本匹配指北
后端·面试·程序员
SimonKing2 小时前
IntelliJ IDEA 2025.2.x的小惊喜和小BUG
java·后端·程序员
程序员西西4 小时前
SpringCloudGateway入门实战
java·spring boot·计算机·程序员·编程
舒一笑21 小时前
PandaCoder 的解构与新生:为中文开发者造一束专注的光
后端·程序员·intellij idea
AI大模型1 天前
全面掌握 AI Agent 30 个高频面试的问题与解答相关的核心知识点!
程序员·llm·agent
大模型教程2 天前
AI智能体(Agent)保姆级入门指南,零基础小白也能轻松上手
程序员·llm·agent
大模型教程2 天前
产品经理必看!AI大模型上线前,如何确保它不会“胡言乱语”?
程序员·llm·agent
大模型教程2 天前
别再堆模型了!构建智能体系统,你需要掌握这8个核心要点
程序员·llm·agent
大模型教程3 天前
大模型平民化:3块钱、2小时,MiniMind开源项目全解析
程序员·llm·agent