解密Protobuf:高效資料傳輸的秘密武器

深坑妙脆角發表於2024-08-18

簡介

Protocol Buffers(簡稱Protobuf)是由Google開發的一種用於資料序列化技術。與傳統的XMLJSON相比,Protobuf具有更高的效能和更小的訊息體積,特別適用於需要高效資料交換的場景

特點

  1. 速度快Protobuf在序列化與反序列化資料時速度極快
  2. 佔空間小Protobuf序列化後的二進位制資料非常小,可節省大量的儲存和頻寬
  3. 跨平臺Protobuf支援多種程式語言(常見的幾乎都支援),相容性好
  4. 易擴充套件:使用.proto檔案定義資料結構(包括欄位型別、預設值和驗證規則等),新增新內容時,也可以輕鬆做到,不會破壞現有系統
  5. 簡單易用:只需專注.proto資料結構檔案的編寫,對應的序列化與反序列化程式碼可自動生成

使用場景

  • 分散式系統:各服務之間需要頻繁地進行資料交換,Protobuf可以顯著提高通訊效率
  • 儲存和持久化Protobuf能節省很多儲存空間,常用於日誌記錄、配置檔案和資料持久化
  • 移動應用:在網路頻寬和儲存空間有限的情況下,Protobuf能更好地提高效能

Proto

protoprotobuf定義資料結構的一種格式,用.proto字尾的檔案儲存,使用proto編譯器可以把對應的proto結構編譯為各目標語言的序列化與反序列化程式碼庫

簡單的使用模擬

  • 使用proto語法(規則)定義我們的資料結構,儲存在.proto字尾的檔案內

  • 使用proto編譯器編譯.proto檔案,指定要編譯成目標語言環境(JavaPython等)

    • 結果將會生成對應語言的一個程式碼檔案(內包含對應資料序列化與反序列化相關操作的類或函式)
  • 接著使用生成的程式碼檔案即可

Proto語法

首先,先看一則訊息結構,如下:

syntax = "proto3";

message Student {
  string id = 1;
  string name = 2;
  int32 age = 3;
}
  • proto3表示使用的proto版本,如不如上指明,編譯器會預設為proto2
  • message Student表示定義一個名為Student的訊息結構
  • {}內為訊息的欄位,採用型別 欄位名 = 編號;的形式描述

欄位型別

proto的型別主要有三類:標量型別列舉或複合型別(如其它訊息型別)

標量型別

常用的標量型別如下:

proto型別 註釋 對應C++型別
double 雙精度浮點數 double
float 單精度浮點數 float
int32 使用可變長度編碼,對負數編碼效率不高 - 如果欄位可能具有負值,可使用 sint32 int32
int64 使用可變長度編碼,對負數編碼效率不高 - 如果欄位可能具有負值,可使用 sint64 int64
uint32 使用可變長度編碼 uint32
uint64 使用可變長度編碼 uint64
sint32 使用可變長度編碼,有符號 int 值,與常規 int32 相比,可更有效地對負數進行編碼 int32
sint64 使用可變長度編碼,有符號 int 值,與常規 int64 相比,可更有效地對負數進行編碼 int64
fixed32 始終為四個位元組,如果值通常大於 228,則比 uint32 更高效 uint32
fixed64 始終為八個位元組,如果值通常大於 256,則比 uint64 更高效 uint64
sfixed32 始終為四個位元組 int32
sfixed64 始終為八個位元組 int64
bool 布林值 bool
string 字串必須始終包含 UTF-8 編碼或 7 位 ASCII 文字,並且不能長於 232 string
bytes 可能包含不長於 232 的任何任意位元組序列 string

上表值給出了C++中對應的型別參考,其它語言亦有與proto對應的型別,具體可自行搜查

預設值:當解析訊息時,如果編碼的訊息不包含某些元素,則訪問解析物件中的相應欄位將返回該欄位的預設值,預設值相關於型別:

  • 對於字串,預設值為空字串
  • 對於位元組,預設值為空位元組
  • 對於布林值,預設值是false
  • 對於數字型別,預設值是0
  • 對於列舉,預設值是第一個定義的列舉值,它必須是0
列舉

定義一個型別,值為預定義值列表中的一個值

如下,是一個列舉參考:

enum Week {
  MONDAY = 0;
  TUESDAY = 1;
  WEDNESDAY = 2;
  THURSDAY = 3;
  FRIDAY = 4;
  SATURDAY = 5;
  SUNDAY = 6;
}
  • 每個列舉都需要一個常量(32位整數範圍內)
  • 第一個需要為零

保留值:定義完一個列舉後,之後某個時間點可能由於業務需要,要修改其結構,或刪除一些項,如果僅是註釋掉或直接刪除,會導致列舉的常量值不連續,後續其它開發者可以使用這些值新增其它列舉項,如果他們後續又載入相同.proto的舊版本,可能會導致嚴重的問題(如資料損壞等)

為了防止這種問題發生,我們可以用reserved將這些刪除項的常量值宣告為保留值,防止後續開發者使用

enum Test {
  reserved 3, 5, 7 to 12, 37 to max;
  reserved "FIR", "CAR";
}
  • reserved後跟保留內容

  • 數字表示保留常量

  • 字串表示保留欄位名稱

  • to用於連線連續的一段值

  • max表示指定常量數字範圍的最大可能值

  • 同一個reserved語句中不能同時混合欄位名稱和數字值

其它訊息型別

表示用message定義的其它訊息型別

巢狀訊息

訊息除了一個個定義,還可巢狀定義,如下:

message UserInfo {
  message Avatar {
    string url = 1;
    string date = 2;
  }
  string nickname = 1;
  string sign = 2;
  Avatar avatar = 3;
}
  • 在訊息內部定義的訊息,即為巢狀訊息,內部可透過其名稱直接使用
  • 外部如想使用巢狀訊息,可透過_Parent_._Type_的形式引用,如:UserInfo.Avatar

如果想用其它.proto檔案內定義的型別,可在檔案頭使用import檔案路徑匯入指定檔案使用

對映

使用關鍵字map建立、宣告,配對鍵/值欄位型別

形式為:map<key, value> name = N;

  • key可以是數字或字串型別(即除了浮點數和 bytes 外的所有標量型別)
  • value可以是任何型別,處理另一個map

舉例一個對映如下

message Request {
  string url = 1;
  map<string, string> headers = 2;
}

欄位編號

定義訊息欄位時,必須給每個欄位都分配一個編號,數值在1536,870,911之間

  • 給定的數值必須在該訊息的所有欄位中唯一
  • 編號19,00019,999Protobuf實現保留,不可用
  • 不可使用reserved定義的保留值

建議使用115的數字設定常用欄位,因為它們只需一個位元組進行編碼,16之後的數字至少需要兩個位元組

欄位標籤

用於宣告在定義欄位的型別前,起到某種作用

  • required:表示欄位為必填欄位,建立訊息時,必須設定該欄位的值

  • optional:表示欄位是可選欄位,建立訊息時,可設定值也可不設定,訊息接收方如可識別,則對應處理,如不可識別則忽略

  • repeated:表示該欄位可重複0~N個,保留重複值的順序,即相當於一個陣列、列表

保留欄位

與上方列舉提到的用法型別,透過reserved定義保留編號和保留欄位名稱

註釋

proto採用與C/C++同樣式的註釋方法:///* ... */

為了防止協議訊息型別之間的名稱衝突,可以在.proto檔案中新增可選的package說明符

package foo;
message Test { ... }

定義訊息型別的欄位時可帶上包說明符foo.Test bar = 1;

在編譯為不同語言時,會做不同處理,如:對於java解析為java中的包,對於C++則解析為名稱空間

後言

protobuf使用非常廣泛,如很多平臺的影片、直播間彈幕流即是使用這種技術傳輸,如有需要,還可對序列化訊息進行gzip壓縮,進一步減少訊息體積,節省頻寬,提高傳輸速度

相關文章