Protobuf 協議語法
-
message: Protobuf中定義一個數據結構需要用到關鍵字message,這一點和Java的class,Go語言中的struct類似。
-
標識號: 在消息的定義中,每個字段等號后面都有唯一的標識號,用於在反序列化過程中識別各個字段的,一旦開始使用就不能改變。標識號從整數1開始,依次遞增,每次增加1,標識號的范圍為1~2^29 – 1,其中[19000-19999]為Protobuf協議預留字段,開發者不建議使用該范圍的標識號;一旦使用,在編譯時Protoc編譯器會報出警告。
-
字段規則: 字段規則有三種:
- 1、required:該規則規定,消息體中該字段的值是必須要設置的。
- 2、optional:消息體中該規則的字段的值可以存在,也可以為空,optional的字段可以根據defalut設置默認值。
- repeated:消息體中該規則字段可以存在多個(包括0個),該規則對應java的數組或者go語言的slice。
-
數據類型: 常見的數據類型與protoc協議中的數據類型映射如下:
-
-
枚舉類型: proto協議支持使用枚舉類型,和正常的編程語言一樣,枚舉類型可以使用enum關鍵字定義在.proto文件中:
enum Age{ male=1; female=2; }
-
字段默認值: .proto文件支持在進行message定義時設置字段的默認值,可以通過default進行設置,如下所示:
message Address { required sint32 id = 1 [default = 1]; required string name = 2 [default = '北京']; optional string pinyin = 3 [default = 'beijing']; required string address = 4; required bool flag = 5 [default = true]; }
-
導入: 如果需要引用的message是寫在別的.proto文件中,可以通過import "xxx.proto"來進行引入:
-
嵌套: message與message之間可以嵌套定義,比如如下形式:
syntax = "proto2"; package example; message Person { required string Name = 1; required int32 Age = 2; required string From = 3; optional Address Addr = 4; message Address { required sint32 id = 1; required string name = 2; optional string pinyin = 3; required string address = 4; } }
-
message更新規則: message定義以后如果需要進行修改,為了保證之前的序列化和反序列化能夠兼容新的message,message的修改需要滿足以下規則:
- 不可以修改已存在域中的標識號。
- 所有新增添的域必須是 optional 或者 repeated。
- 非required域可以被刪除。但是這些被刪除域的標識號不可以再次被使用。
- 非required域可以被轉化,轉化時可能發生擴展或者截斷,此時標識號和名稱都是不變的。
- sint32和sint64是相互兼容的。
- fixed32兼容sfixed32。 fixed64兼容sfixed64。
- optional兼容repeated。發送端發送repeated域,用戶使用optional域讀取,將會讀取repeated域的最后一個元素。
Protobuf 序列化后所生成的二進制消息非常緊湊,這得益於 Protobuf 采用的非常巧妙的 Encoding 方法。接下來看一看Protobuf協議是如何實現高效編碼的。
Protobuf序列化原理
之前已經做過描述,Protobuf的message中有很多字段,每個字段的格式為:修飾符 字段類型 字段名 = 域號;
Varint
Varint是一種緊湊的表示數字的方法。它用一個或多個字節來表示一個數字,值越小的數字使用越少的字節數。這能減少用來表示數字的字節數。
Varint中的每個byte的最高位bit有特殊的含義,如果該位為1,表示后續的byte也是該數字的一部分,如果該位為0,則結束。其他的7個bit都用來表示數字。因此小於128的數字都可以用一個byte表示。大於128的數字,比如300,會用兩個字節來表示:1010 1100 0000 0010。下圖演示了 Google Protocol Buffer 如何解析兩個bytes。注意到最終計算前將兩個byte的位置相互交換過一次,這是因為 Google Protocol Buffer 字節序采用little-endian的方式。

在序列化時,Protobuf按照TLV的格式序列化每一個字段,T即Tag,也叫Key;V是該字段對應的值value;L是Value的長度,如果一個字段是整形,這個L部分會省略。
序列化后的Value是按原樣保存到字符串或者文件中,Key按照一定的轉換條件保存起來,序列化后的結果就是 KeyValueKeyValue…依次類推的樣式,示意圖如下所示:

采用這種Key-Pair結構無需使用分隔符來分割不同的Field。對於可選的Field,如果消息中不存在該field,那么在最終的Message Buffer中就沒有該field,這些特性都有助於節約消息本身的大小。比如,我們有消息order1:
Order.id = 10; Order.desc = "bill";
則最終的 Message Buffer 中有兩個Key-Value對,一個對應消息中的id;另一個對應desc。Key用來標識具體的field,在解包的時候,Protocol Buffer根據Key就可以知道相應的Value應該對應於消息中的哪一個field。
Key 的定義如下:
(field_number << 3) | wire_type
可以看到 Key 由兩部分組成。第一部分是 field_number,比如消息lm.helloworld中field id 的field_number為1。第二部分為wire_type。表示 Value的傳輸類型。而wire_type有以下幾種類型:
參考鏈接:golangroadmap