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 是一种高效实用的序列化工具,在开发中能大幅优化资源利用。

相关推荐
程序员cxuan16 分钟前
分享一下我最近常用的 10 个 Codex 小技巧。
人工智能·后端·程序员
Moonbit1 小时前
MoonBit ×CCF开源创新大赛 倒计时24天!快来提交你的作品
程序员·编程语言
zzzzzz3101 小时前
假如我是掘金管理员,我先给评论区装个'代码审查'系统
python·程序员·机器人
demo007x15 小时前
Docling 文档转换以及技术架构分析
前端·后端·程序员
保持当下17 小时前
分享一些程序员很棘手但是却又简单的工具
程序员·免费·js·工具
Hilaku20 小时前
AI 写代码越快,为什么 Code Review 越不能省?
前端·javascript·程序员
程序员cxuan20 小时前
LobsterAI 快把职业门槛打没了
人工智能·程序员
Coffeeee1 天前
Codachi — 藏在 Claude Code 状态栏里的电子宠物
人工智能·程序员·claude
小闹5491 天前
Docker 如何才能学的更扎实
后端·程序员