一、先搞懂:Protobuf 編解碼的核心目標

Protobuf 是 Google 推出的二進制序列化協議,編解碼的核心是:

  • 編碼(序列化):將結構化數據(如對象、結構體)轉換成緊湊的二進制字節流,便於存儲 / 傳輸;
  • 解碼(反序列化):將二進制字節流還原為原始的結構化數據。

相比 JSON/XML 的文本格式,Protobuf 的核心優勢是體積小、解析快—— 這源於它的二進制存儲方式和 “基於預定義協議(.proto 文件)” 的編解碼邏輯,無需像 JSON 那樣存儲字段名,僅用數字標識字段。

二、Protobuf 編解碼的前置基礎:.proto 文件與字段規則

編解碼的前提是預定義數據結構(.proto 文件),字段需指定「字段編號」「字段類型」「字段規則」,這是編解碼的 “協議約定”:

protobuf

// 示例:定義一個用户信息的.proto文件
syntax = "proto3"; // 指定Protobuf版本(主流為proto3)

message User {
  int32 id = 1;       // 字段編號1,類型int32
  string name = 2;    // 字段編號2,類型string
  repeated int32 tags = 3; // 字段編號3,重複字段(數組)
}

關鍵規則:

  1. 字段編號:唯一標識字段(1-15 佔 1 字節,16 + 佔 2 字節),編解碼時僅傳輸編號而非字段名;
  2. 字段類型:Protobuf 定義了基礎類型(int32/string/bool 等),每種類型對應固定的編碼方式;
  3. 字段規則:proto3 中默認「可選」,repeated表示數組,無需額外標記 “是否存在”。

三、核心原理 1:編碼(序列化)—— 如何把數據轉二進制?

Protobuf 編碼的核心是「Tag-Length-Value(TLV)」格式(部分類型簡化為 Tag-Value),逐個字段編碼:

1. 核心組成:Tag + Length(可選) + Value

部分

作用

Tag

字段編號 + 字段類型(wire type),用「可變長度整數(Varint)」編碼

Length

僅針對 “長度不固定” 的類型(如 string、bytes、嵌套 message),表示 Value 的字節數

Value

字段的實際值,按對應類型的編碼規則轉換為二進制

2. 關鍵編碼規則(新手必懂)

(1)Tag 編碼:字段編號 + Wire Type

Wire Type 是 Protobuf 定義的 “底層類型標識”(共 6 種),映射關係如下:

Wire Type

含義

適用 Protobuf 類型

0

Varint(可變長整數)

int32/int64/bool/enum

2

長度分隔

string/bytes/repeated/ 嵌套 message

Tag 的計算方式:(字段編號 << 3) | Wire Type(左移 3 位,騰出最後 3 位存 Wire Type),再用 Varint 編碼。

(2)Varint 編碼(核心優化點)

Protobuf 對整數的核心優化 ——小數字佔更少字節(對比固定 4/8 字節的 int32/int64):

  • 每個字節的最高位(第 8 位):1 表示後續還有字節,0 表示最後一個字節;
  • 低 7 位:存儲數字的二進制值,按 “小端序” 拼接。

示例:編碼整數150(十進制)

  • 二進制:10010110 → 拆分為10010110(最高位 1) + 00000001(最高位 0);
  • Varint 編碼結果:0x96 0x01(僅 2 字節,比固定 4 字節節省 50% 空間)。
(3)長度分隔類型編碼(如 string)

步驟:

  1. 計算 Value 的字節長度,用 Varint 編碼作為 Length;
  2. 將 Value 的原始二進制(如 string 的 UTF-8 字節)作為 Value;
  3. 拼接 Tag + Length + Value。

3. 完整編碼示例(以 User {id:1, name:"Tom"} 為例)

plaintext

// 步驟1:編碼id字段(字段編號1,Wire Type 0)
Tag計算:(1 << 3) | 0 = 8 → Varint編碼8 → 0x08
Value:1 → Varint編碼1 → 0x01
id字段編碼結果:0x08 0x01

// 步驟2:編碼name字段(字段編號2,Wire Type 2)
Tag計算:(2 << 3) | 2 = 18 → Varint編碼18 → 0x12
Length:"Tom"的UTF-8字節數=3 → Varint編碼3 → 0x03
Value:"Tom"的UTF-8字節 → 0x54 0x6F 0x6D
name字段編碼結果:0x12 0x03 0x54 0x6F 0x6D

// 最終完整二進制(拼接所有字段):
0x08 0x01 0x12 0x03 0x54 0x6F 0x6D

對比 JSON:{"id":1,"name":"Tom"}(文本佔 18 字節),Protobuf 僅 6 字節,體積縮減 70%+。

四、核心原理 2:解碼(反序列化)—— 如何還原二進制?

解碼是編碼的逆過程,依賴.proto 文件的字段約定,步驟如下:

  1. 讀取 Tag:按 Varint 解析第一個字節(或多個),得到 Tag 值;
  2. 解析 Tag
  • 右移 3 位 → 字段編號;
  • 取最後 3 位 → Wire Type;
  1. 根據 Wire Type 解析 Value
  • Wire Type=0(Varint):繼續讀取字節直到最高位為 0,拼接為整數;
  • Wire Type=2(長度分隔):先讀取 Varint 得到 Length,再讀取 Length 個字節作為 Value;
  1. 映射到結構化數據:根據字段編號和.proto 定義的類型,將 Value 轉換為對應類型(如 string 的 UTF-8 字節轉字符串);
  2. 循環解析:直到二進制流讀取完畢。

示例(解析上述 User 的二進制):

  • 讀取 0x08 → Varint 解析為 8 → 字段編號 1,Wire Type 0;
  • 讀取 0x01 → Varint 解析為 1 → id=1;
  • 讀取 0x12 → Varint 解析為 18 → 字段編號 2,Wire Type 2;
  • 讀取 0x03 → Length=3 → 讀取後續 3 字節 0x54 0x6F 0x6D → 轉字符串 "Tom";
  • 最終還原 User {id:1, name:"Tom"}。

五、Protobuf 編解碼的核心優勢(對比 JSON/XML)

特性

Protobuf

JSON

存儲格式

二進制

文本

字段標識

數字編號(佔 1-2 字節)

字符串字段名(佔多字節)

整數存儲

Varint(小數字省空間)

十進制字符串(如 "150")

解析速度

快(二進制直接解析)

慢(需解析文本 / 轉義字符)

版本兼容性

強(字段編號不變即可)

弱(字段名變更則失效)

可讀性

差(二進制)

好(人類可讀)

六、關鍵注意事項

  1. 版本兼容:字段編號一旦確定不能修改(解碼依賴編號),新增字段只需用未使用的編號,舊版本解碼會忽略未知字段;
  2. 類型匹配:解碼時必須嚴格匹配.proto 定義的類型(如 int32 不能解碼為 string),否則會解析錯誤;
  3. 默認值:proto3 中未設置的字段,解碼時返回類型默認值(int=0,string=""),無需存儲 “是否存在” 的標記;
  4. Repeated 字段:proto3 中 repeated 字段無需額外長度標記,直接按單個字段重複編碼(如 tags=[1,2] → 編碼兩次 tag=3 的字段)。

總結

  1. Protobuf 編解碼基於「TLV(Tag-Length-Value)」格式,核心是用數字編號替代字段名Varint 編碼優化整數存儲,實現二進制緊湊存儲;
  2. 編碼流程:按.proto 定義逐個字段計算 Tag → 按類型編碼 Value → 拼接 Tag-Length-Value;
  3. 解碼流程:解析 Tag 得到字段編號和類型 → 按類型解析 Value → 映射為結構化數據;
  4. 高效的核心原因:二進制格式 + 精簡的字段標識 + Varint 整數優化,比 JSON/XML 體積更小、解析更快,適合高性能數據傳輸 / 存儲場景。