gRPC(二)入門:Protobuf入門

lin鍾一發表於2022-11-09

透過protubuf文件先了解一下protobuf語法。
個人網站:linzyblog.netlify.app/
示例程式碼已經上傳到github:點選跳轉

1、什麼是protobuf?

Protocol Buffers ( Protobuf ) 是一種免費的開源 跨平臺資料格式,用於序列化結構化資料。它是谷歌公司開發的一種資料描述語言,並於2008年開源。Protobuf剛開源時的定位類似於XML、JSON等資料描述語言,透過附帶工具生成程式碼並實現將結構化資料序列化的功能。

Protocol Buffers 是一種與語言、平臺無關,可擴充套件的序列化結構化資料的方法,常用於通訊協議,資料儲存等等。相較於 JSON、XML,它更小、更快、更簡單,因此也更受開發人員的青眯。

protobuf官方文件:點選跳轉

2、JSON、XML、Protobuf選擇

1)什麼是序列化和反序列化

  • 序列化是將資料結構或物件狀態轉換為格式(xml/json/protobuf)的過程可以儲存或傳輸。
  • 反序列化是從表示的格式(xml/json/protobuf)構造資料結構/物件狀態的過程

在這裡插入圖片描述

2)JSON、XML、Protobuf對比

JSON:最流行的主要還是json。因為瀏覽器對於json資料支援非常好,有很多內建的函式支援。

  • 具有可讀性/可編輯性
  • 無需預先知道模式即可解析
  • 優秀的瀏覽器支援
  • 比 XML 更簡潔

JSON資料格式:

{
    "title":"Protobuf article",
    "status":"DRAFT",
    "members" : [
    {
      "name" : "Molecule Man",
      "age" : 29,
      "secretIdentity" : "Dan Jukes",
      "powers" : [
        "Radiation resistance",
        "Turning tiny",
        "Radiation blast"
      ]
    },
}

XML:現在基本很少使用XML。json使用了鍵值對的方式,不僅壓縮了一定的資料空間,同時也具有可讀性。

  • 具有可讀性/可編輯性
  • 無需預先知道模式即可解析
  • SOAP等標準
  • 良好的工具支援(xsd、xslt、sax、dom 等)
  • 相當冗長

XML資料格式:

<medium>
    <title>Protobuf article</name>
    <status>DRAFT</status>
</medium>

Protobuf:適合高效能,對響應速度有要求的資料傳輸場景。因為profobuf是二進位制資料格式,需要編碼和解碼。資料本身不具有可讀性。因此只有在反序列化之後得到真正可讀的資料。

  • 非常密集的資料(輸出小)
  • 在不知道架構的情況下很難穩健地解碼(資料格式在內部是模稜兩可的,需要架構來解釋)
  • 處理速度非常快
  • 不具有可讀性/可編輯性(密集的二進位制資料)

Protobuf資料格式:

##.proto file
message Medium {
  required string title = 1;
  enum StatusType {
    DRAFT = 0;
    PUBLISHED = 1;
  }

  message Status {
      required StatusType type = 0[default = DRAFT];
  }
  required Status status = 2;
}
資料格式 資料儲存方式 可讀性/可編輯性 解析速度 語言支援 使用範圍
JSON 文字 一般 所有語言 檔案儲存、資料互動
XML 文字 所有語言 檔案儲存、資料互動
Protobuf 二進位制 不可讀 所有語言 檔案儲存、資料互動

3)使用Protobuf替代XML/JSON的好處

  • 與xml/json相比,Protobuf格式在表示資料結構方面更小、更快。
  • xml/Json以字串形式交換資料,然後在使用解析器檢索時解析它們,這個過程在處理和記憶體消耗方面可能非常昂貴。但是使用protobuf,它使用預定義模式,使得解析邏輯高效而簡單。
  • 解析json字串、陣列和物件需要順序掃描,這意味著沒有元素大小或體頭的計數。多層次xml文件也是如此。

當然也不能一味的使用Protobuf,JSON適用的場景遠遠大於Protobuf,在有些時候Protocol Buffers 仍然沒有 JSON 要來的方便。

  • 與xml/json相比,protobuf的學習曲線略高。
  • 當你的資料是需要別人可讀的。
  • 你不打算直接處理接收的資料,而是從資料中取你想要的部分處理。
  • 不想經過特殊處理,直接能從瀏覽器中解讀的。
  • 在web服務還沒有準備好將資料模型繫結到特定模式的場景中,protobuf沒有多大用處。

4)Protobuf使用場景

  • 在考慮將 Protobuf 用於web服務之間的通訊(比如不與客戶端瀏覽器解析引擎互動)
  • 當文件大小在MB左右,且資料型別混合時,protobuf將在效能方面優於xml/json,protobuf在網路上對資料的編碼和解碼速度更快。如果資料是巨大的GB,那麼無論選擇什麼編碼技術棧(如protobug/json/xml),都需要壓縮。
  • 在需要雙重解碼的場景中(比如威脅搜尋對同一命令列進行多次解碼),protobuf比JSON要快得多。
  • 當web服務過渡到使用gRPC而不是傳統的REST框架時,protobuf是推薦使用的標準。

1、簡單示例

// 指明當前使用proto3語法,如果不指定,編譯器會使用proto2
syntax = "proto3";
// package宣告符,用來防止訊息型別有命名衝突
package msg;
// 選項資訊,對應go的包路徑
option go_package = "server/msg";
// message關鍵字,像go中的結構體
message FirstMsg {
  // 型別 欄位名 標識號
  int32 id = 1;
  string name=2;
  string age=3;
}

syntax: 用來標記當前使用proto的哪個版本。如果不指定,編譯器會使用proto2。
package: 指定包名,用來防止訊息型別命名衝突。
option go_package: 選項資訊,代表生成後的go程式碼包路徑。在生成 gRPC 程式碼時,必須指明。
message: 宣告訊息的關鍵字,類似Go語言中的struct。

FirstMsg 訊息定義指定了三個欄位(名稱/值對),每個欄位都有一個名稱和一個型別。
定義欄位語法格式: 型別 欄位名 編號,例如repeated int32 nums = 1;在生成gRPC程式碼時會自動生成陣列[]int32型別。

分配欄位編號說明:

  • 訊息定義中的每個欄位都有一個唯一的編號。這些欄位編號用於在 訊息二進位制格式中標識您的欄位,並且在使用訊息型別後不應更改。
  • [1, 15]之內的標識號在編碼的時候會佔用一個位元組。[16, 2047]之內的標識號則佔用2個位元組。
  • 最小的標識號可以從1開始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999],因為是預留資訊,如果使用,編譯時會報錯。

2、proto資料型別與Go資料型別對應

.proto Type Go Type Notes
double float64
float float32
int32 int32 使用變長編碼。對於負值的效率很低,如果有負值,使用sint32
int64 int64 使用變長編碼。對於負值的效率很低,如果有負值,使用sint64
uint32 uint32 使用變長編碼
uint64 uint64 使用變長編碼
sint32 int32 使用變長編碼,負值時比int32高效的多
sint64 int64 使用變長編碼,有符號的整型值。編碼時比通常的int64高效。
fixed32 uint32 總是4個位元組,如果數值比2^28^大的話,這個型別會比uint32高效。
fixed64 uint64 總是8個位元組,如果數值比2^56^大的話,這個型別會比uint64高效。
bool bool
string string 字串必須包含UTF-8編碼或7位ASCII文字,且長度不能超過2^32^。
bytes []byte 可以包含不超過2^32^的任意位元組序列。

3、指定訊息欄位規則

  • singular:訊息中至多存在一個該欄位的資料。使用 proto3 語法時,當沒有為給定欄位指定其他欄位規則時,這是預設欄位規則。
  • optional:與 singular 類似,不同之處在於可以檢查該值是否已經顯式設定了值。欄位有兩種可能的狀態:
    • 該欄位已設定,幷包含從連線中顯式設定或解析的值。它將被序列化到連線上。
    • 該欄位未設定,將返回預設值。它不會被序列化。
  • repeated:該欄位型別可以在訊息中可以重複設定多次,重複值的順序將被保留。(設定成為陣列型別)
  • map:成對的鍵/值欄位型別。

4、保留標示符 reserved

什麼是保留標示符?reserved 標記的編號、欄位名,都不能在當前訊息中使用。

保留識別符號的作用:對於特殊的欄位名或者編號透過完全刪除欄位或將其註釋掉來更新訊息型別,如果後面出現其他使用者對該訊息進行更新重用了特殊的欄位名或者編號,可能會導致嚴重的錯誤,包括資料損壞、出現隱私漏洞等。
為了確保這種情況不會發生的一種方法就是用保留識別符號指定保留已刪除的欄位名或者編號。如果有其他使用者試圖重用這些欄位名或編號,protobuf則會報錯預警。

syntax = "proto3";
package demo;

// 在這個訊息中標記
message DemoMsg {
  // 標示號:1,2,10,11,12,13 都不能用
  reserved 1, 2, 10 to 13;
  // 欄位名 test、name 不能用
  reserved "test","name";
  // 不能使用欄位名,提示:Field name 'name' is reserved
  string name = 3;
  // 不能使用標示號,提示:Field 'id' uses reserved number 11
  int32 id = 11;
}

// 另外一個訊息還是可以正常使用
message Demo2Msg {
  // 標示號可以正常使用
  int32 id = 1;
  // 欄位名可以正常使用
  string name = 2;
}

注意:不能在同一 reserved 語句中混合欄位名稱和欄位編號。

5、列舉型別

列舉:在定義訊息型別時,希望其中一個欄位只是預定義值列表中的一個值。
例如,假設您想為每個SearchRequest新增一個Corpus 欄位,其中列舉預定義值可以是UNIVERSAL、WEB、IMAGES、LOCAL、NEWS、PRODUCTS或VIDEO。您可以透過在訊息定義中新增一個列舉,為每個可能的值新增一個常量。

在下面的示例中,我們新增了一個包含所有可能值的 enum 呼叫Corpus,以及一個 type 欄位Corpus:

enum Corpus {
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  Corpus corpus = 4;
}

每個列舉型別必須將其第一個型別對映為編號0, 原因有兩個:

  • 必須有一個零值,以便我們可以使用 0 作為數字 預設值。
  • 零值必須是第一個元素,以便與第一個列舉值始終為預設值的proto2語義相容 。

可以對相同的編號分配給不同的列舉常量來定義別名。只需要將allow_alias選項設定為true,否則協議編譯器將在找到別名時生成錯誤訊息。儘管所有別名值在反序列化期間都有效,但在序列化時始終使用第一個值。

enum EnumAllowingAlias {
  option allow_alias = true;
  EAA_UNSPECIFIED = 0;
  EAA_STARTED = 1;
  EAA_RUNNING = 1;
  EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
  ENAA_UNSPECIFIED = 0;
  ENAA_STARTED = 1;
  // ENAA_RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
  ENAA_FINISHED = 2;
}

注意:

  • 列舉器常量必須在 32 位整數範圍內。
  • 列舉型別同樣可以使用保留識別符號。

6、引入其他proto檔案訊息型別

1)被引入檔案class.proto

檔案位置:proto/class.proto

syntax="proto3";
// 包名
package dto;
// 生成go後的檔案路徑
option go_package = "grpc/server/dto";

message ClassMsg {
  int32  classId = 1;
  string className = 2;
}

2)使用引入檔案user.proto

檔案位置:proto/user.proto

syntax = "proto3";

// 匯入其他proto檔案
import "proto/class.proto";

option go_package="grpc/server/dto";

package dto;

// 使用者資訊
message UserDetail{
  int32 id = 1;
  string name = 2;
  string address = 3;
  repeated string likes = 4;
  // 所屬班級
  ClassMsg classInfo = 5;
}

如果Goland提示:Cannot resolve import...
在這裡插入圖片描述

7、巢狀訊息型別

可以使用其他訊息型別作為欄位型別,也可以在其他訊息型別中定義和使用訊息型別。

syntax = "proto3";
option go_package = "server/nested";
// 學員資訊
message UserInfo {
  int32 userId = 1;
  string userName = 2;
}
message Common {
  // 班級資訊
  message CLassInfo{
    int32 classId = 1;
    string className = 2;
  }
}
// 巢狀資訊
message NestedDemoMsg {
  // 學員資訊 (直接使用訊息型別)
  UserInfo userInfo = 1;
  // 班級資訊 (透過Parent.Type,調某個訊息型別的子型別)
  Common.CLassInfo classInfo =2;
}

8、map型別訊息

建立關聯對映作為資料定義的一部分,map資料結構格式:

map<key_type, value_type> map_field = N;

注意:

  • key_type只能是任何整數或字串型別(除浮點型別和任何標量bytes型別)。
  • enum 不能作為key_type和value_type定義的型別。
  • map欄位不能是repeated。

示例:

//protobuf原始碼
syntax = "proto3";
option go_package = "server/demo";

// map訊息
message DemoMapMsg {
  int32 userId = 1;
  map<string,string> like =2;
}


//生成Go程式碼
type DemoMapMsg struct {
 state         protoimpl.MessageState
 sizeCache     protoimpl.SizeCache
 unknownFields protoimpl.UnknownFields

 UserId int32             `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"`
 Like   map[string]string `protobuf:"bytes,2,rep,name=like,proto3" json:"like,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}

向後相容性
map 語法等效於以下內容,因此不支援 map 的Protobuf實現仍然可以處理你的資料:

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

任何支援對映的Protobuf實現都必須生成和接受上述定義可以接受的資料。

9、切片(陣列)欄位型別

需要建立切片(陣列)欄位型別:

//protobuf原始碼
syntax = "proto3";
option go_package = "server/demo";

// repeated允許欄位重複,對於Go語言來說,它會編譯成陣列(slice of type)型別的格式
message DemoSliceMsg {
  // 會生成 []int32
  repeated int32 id = 1;
  // 會生成 []string
  repeated string name = 2;
  // 會生成 []float32
  repeated float price = 3;
  // 會生成 []float64
  repeated double money = 4;
}


//生成Go程式碼
// repeated允許欄位重複,對於Go語言來說,它會編譯成陣列(slice of type)型別的格式
type DemoSliceMsg struct {
 state         protoimpl.MessageState
 sizeCache     protoimpl.SizeCache
 unknownFields protoimpl.UnknownFields

 // 會生成 []int32
 Id []int32 `protobuf:"varint,1,rep,packed,name=id,proto3" json:"id,omitempty"`
 // 會生成 []string
 Name []string `protobuf:"bytes,2,rep,name=name,proto3" json:"name,omitempty"`
 // 會生成 []float32
 Price []float32 `protobuf:"fixed32,3,rep,packed,name=price,proto3" json:"price,omitempty"`
 Money []float64 `protobuf:"fixed64,4,rep,packed,name=money,proto3" json:"money,omitempty"`
}

10、oneof(只能選擇一個)

如果需要一條包含多個欄位的訊息,並且最多同時設定一個欄位,可以強制執行此行為並使用 oneof 功能節省記憶體。

oneof 欄位與常規欄位一樣,在 oneof 共享記憶體中的所有欄位,最多可以同時設定一個欄位。設定 oneof 的任何成員會自動清除所有其他成員。

如果設定了多個值,則由 proto 中的 order 確定的最後一個設定的值將覆蓋所有以前的設定值。

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

在生成的程式碼中,oneof 欄位具有與常規欄位相同的 getter 和 setter。還可以獲得一種特殊的方法來檢查 oneof 中設定了哪個值(如果有)。

11、Any 任何型別

Any訊息型別允許您將訊息作為嵌入型別使用,而不需要它們的.proto定義。

Any以位元組的形式包含任意序列化的訊息,以及作為該訊息型別的全域性唯一識別符號並解析為該訊息型別的URL。要使用Any型別,您需要 import google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

本章我們瞭解proto3的語法,下一章我會詳細介紹gRPC以及如何載入 protoc-gen-go 外掛達到生成 Go 程式碼的目的。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章