Protobuf編碼規則

支持類型

該表顯示了在 .proto 文件中指定的類型,以及自動生成的類中的相應類型:

.proto Type Notes C++ Type Java/Kotlin Type[1] Java/Kotlin 類型 [1] Python Type[3] Go Type Ruby Type C# Type PHP Type Dart Type
double double double float float64 Float double float double
float float float float float32 Float float float double
int32 varint編碼。對於負數編碼效率低下——如果字段可能有負值,建議改用 sint32。 int32 int int int32 Fixnum or Bignum (as required) int integer int
int64 varint編碼。對於負數編碼效率低下——如果字段可能有負值,建議改用 sint64。 int64 long int/long int64 Bignum long integer/string Int64
uint32 varint編碼。 uint32 int int/long uint32 Fixnum or Bignum (as required) uint integer int
uint64 varint編碼。 uint64 long int/long uint64 Bignum ulong integer/string Int64
sint32 zigzag和varint編碼。有符號的 int 值。比常規的 int32 能更高效地編碼負數。 int32 int int int32 Fixnum or Bignum (as required) ) int integer int
sint64 zigzag和varint編碼。有符號的 int 值。比常規的 int64 能更高效地編碼負數。 int64 long int/long int64 Bignum long integer/string Int64
fixed32 總是四個字節。如果值通常大於 2\(^{28}\) ,則比 uint32 更有效。 uint32 int int/long uint32 Fixnum or Bignum (as required) uint integer int
fixed64 總是八個字節。如果值通常大於 2\({^56}\) ,則比 uint64 更有效。 uint64 long int/long uint64 Bignum ulong integer/string Int64
sfixed32 總是四個字節。 int32 int int int32 Fixnum or Bignum (as required) int integer int
sfixed64 總是八個字節。 int64 long int/long int64 Bignum long integer/string Int64
bool bool boolean bool bool TrueClass/FalseClass bool boolean bool
string 字符串必須始終包含 UTF-8 編碼或 7 位 ASCII 文本,並且不能長於 2\(^{32}\) string String str/unicode string String (UTF-8) string string String
bytes 可以包含任何不超過 2\(^{32}\) 的任意字節序列。 string ByteString str (Python 2) bytes (Python 3) []byte String (ASCII-8BIT) ByteString string List

消息結構

對於傳統的 xml 或者 json 等方式的序列化中,編碼時直接將 key 本身加進去,例如:

{
    "foo": 1,
    "bar": 2
}

這樣最大的好處就是可讀性強,但是缺點也很明顯,傳輸效率低,每次都需要傳輸重複的字段名。Protobuf 使用了另一種方式,將每一個字段進行編號,這個編號被稱爲 field number 。通過 field_number 的方式解決 json 等方式重複傳輸字段名導致的效率低下問題,例如:

message {
  int32  foo = 1;
  string bar = 2;
}

field_number 的類型被稱爲wire types,目前有六種類型:VARINTI64LENSGROUPEGROUP, and I32 (注:類型3和4已廢棄),因此需要至少3位來區分:

ID Name Used For
0 VARINT int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 I64 fixed64, sfixed64, double
2 LEN string, bytes, embedded messages, packed repeated fields
3 SGROUP group start (deprecated)
4 EGROUP group end (deprecated)
5 I32 fixed32, sfixed32, float

當 message 被編碼時,每一個 key-value 包含 <tag> <type> <paylog>,其結構如下:

+--------------+-----------+---------+
| field_number | wire_type | payload |
+--------------+-----------+---------+
    |               |             |
    |               |             |          +---------------+
    +---------------+             +--------->| (length) data |
    |      tag      |                        +---------------+
    +---------------+

  • field_number 和 wire_type 被稱爲 tag,使用一個字節來表示(這裏指編碼前的一個字節,通過Varint編碼後可能並非一個字節)。其值爲 (field_number << 3) | wire_type ,換句話說低3位解釋了wire_type,剩餘的位則解釋了field_number。
  • payload 則爲 value 具體值,根據 wire_type 的類型決定是否是採用 Length-Delimited 記錄

額外一提的是由於 tag 結構如上所述,因此對於使用 Varint 編碼的 1個字節來說去除最高位標誌位和低三位保留給 wire_type使用,剩下四位能夠表示[0, 15] 的字段標識,超過則需要使用多於一個字節來存儲 tag 信息,因此儘可能將頻繁使用的字段的字段標識定義在 [0, 15] 直接。

編碼規則

Protobuf 使用一種緊湊的二進制格式來編碼消息。編碼規則包括以下幾個方面:

  • 每個字段都有一個唯一的標識符和一個類型,標識符和類型信息一起構成了字段的 tag。
  • 字段的 tag 採用 Varint 編碼方式進行編碼,可以節省空間。
  • 字符串類型的字段採用長度前綴方式進行編碼,先編碼字符串的長度,再編碼字符串本身。
  • 重複的字段可以使用 repeated 關鍵字進行定義,編碼時將重複的值按照順序編碼成一個列表。

Varint 編碼

Varint 是一種可變長度的編碼方式,可以將一個整數編碼成一個字節序列。值越小的數字,使用越少的字節數表示。它的原理是通過減少表示數字的字節數從而實現數據體積壓縮。
Varint 編碼的規則如下:

  • 對於值小於 128 的整數,直接編碼爲一個字節;
  • 對於值大於等於 128 的整數,將低 7 位編碼到第一個字節中,將高位編碼到後續的字節中,並在最高位添加一個標誌位(1 表示後續還有字節,0 表示當前字節是最後一個字節)。每個字節的最高位也稱 MSB(most significant bit)。
    在解碼的時候,如果讀到的字節的 MSB 是 1 話,則表示還有後序字節,一直讀到 MSB 爲 0 的字節爲止。
    例如,int32類型、field_number爲1、值位 300 的 Varint 編碼爲:
// 300 的二進制
00000001 00101100
// 按7位切割
00 0000010 0101100
// 高位全0省略
0000010 0101100
// 逆序,使用的小端字節序
0101100 0000010
// 每一組加上msb,除了最後一組是msb是0,其他的都爲1
10101100 00000010
// 十六進制指
ac 02

// 按照 protobuf 的消息結構,其完整位
08 ac 02
|   |__|__ payload
|   
|----------- tag (field-number << 3 | wire-type) = (1 << 3 | 0) = 0x08

ZigZag編碼

對於 int32/int64 的 proto type,值大於 0 時直接使用 Varint 編碼,而值爲負數時做了符號拓展,轉換爲 int64 的類型,再做 Varint 編碼。負數高位爲1,因此對於負數固定需要十個字節( ceil(64 / 7) = 10 )。(這裏有個值得思考的問題是對於 int32 類型的負數爲什麼要轉換爲 int64 來處理?不轉換的話使用5個字節就能夠完成編碼了。網上的一個說法是爲了轉換爲 int64 類型時沒有兼容性問題,此處由於還未閱讀過源碼,不知道內部是怎麼處理的,因此暫時也沒想通爲什麼因爲兼容性問題需要做符號拓展。因爲按照 Varint 編碼規則解碼的話,直接讀取出來的值賦值給 int64 的類型也沒有問題。int32 negative numbers

很明顯,這樣對於負數的編碼是非常低效的。因此 protobuf 引入 sint32sint64,在編碼時先將數字使用 ZigZag 編碼,然後再使用 Varint 編碼。
ZigZag 編碼將有符號數映射爲無符號數,對應的編解碼規則如下:

static uint32_t ZigZagEncode32(int32_t v) {  
	// Note: the right-shift must be arithmetic  
	// Note: left shift must be unsigned because of overflow
    return (static_cast<uint32_t>(v) << 1) ^ static_cast<uint32_t>(v >> 31);  
}

static uint64_t ZigZagEncode64(int64_t v) {  
	// Note: the right-shift must be arithmetic  
	// Note: left shift must be unsigned because of overflow
    return (static_cast<uint64_t>(v) << 1) ^ static_cast<uint64_t>(v >> 63);  
}

int32_t ZigZagDecode32(uint32_t n) {
    // Note: Using unsigned types prevent undefined behavior
    return static_cast<int32_t>((n >> 1) ^ (~(n & 1) + 1));
} 

static int64_t ZigZagDecode64(uint64_t n) {
    // Note: Using unsigned types prevent undefined behavior
    return static_cast<int64_t>((n >> 1) ^ (~(n & 1) + 1));
}

因此如果傳輸的數據中可能包含有負數,那麼應該使用 sint32/sint64 類型。因爲 protobuf 中只定義了爲這兩種數據類型進行 ZigZag 編碼再使用 Varint 編碼。

Length-delimited 編碼

wire_typeLEN,由於其具有動態長度,因此其由一個 Length 值保存長度大小,這個 Length 同樣通過 Varint 編碼,最後是其內容。
參照以下例子:

message Test2 {
  optional string b = 2;
}

b = "testing"

12 07 [74 65 73 74 69 6e 67]
|  |   t  e  s  t  i  n  g
|  |  |__|__|__|__|__|__ body 的 ASCII 碼
|  |
|  |__ length = 6 = 0x06
|      
|__ Tag (field-number << 3 | wire-type) = (2 << 3 | 2) = 18 = 0x12
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章