支援型別
該表顯示了在 .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,目前有六種型別:VARINT
, I64
, LEN
, SGROUP
, EGROUP
, 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 引入 sint32
和 sint64
,在編碼時先將數字使用 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_type
為 LEN
,由於其具有動態長度,因此其由一個 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