翻譯查閱外網(wǎng)資料過程中遇到的比較優(yōu)秀的文章和資料,一是作為技術(shù)參考以便日后查閱,二是訓(xùn)練英文能力。
此文翻譯自 Protocol Buffers 官方文檔 Encoding 部分
翻譯為意譯,不會照本宣科的字字對照翻譯
以下為原文內(nèi)容翻譯
編碼
本文檔描述了 Protocol Buffer 的 Message 的二進(jìn)制格式。你無需了解這個知識點也能夠在你的應(yīng)用程序中順利使用 protocol buffers,但了解不同 protocol buffer 的格式如何影響最終編碼消息的大小這一點非常有用。
一個簡單的 Message
假設(shè)你有以下一個非常簡單的消息定義:
message Test1 {
optional int32 a = 1;
}
在一個應(yīng)用程序中,你創(chuàng)建一個 Test1 message,并將 a 設(shè)置為150。然后,將 message 序列化為輸出流。如果你能夠查看相應(yīng)的編碼后的結(jié)果,你會看到以下三個字節(jié):
08 96 01
到目前為止,是如此的小巧和簡單-但是這幾個字節(jié)具體代表什么含義?請往下讀...
Base 128 Varints (編碼方法)
要理解上述涉及到的簡單編碼,你首先需要理解 varints 的概念。所謂的 varints 是一種用一個或多個字節(jié)序列化(編碼)整數(shù)的方法。較小的數(shù)字將占用較少的字節(jié)數(shù)。
varint 中的每個字節(jié)都設(shè)置了一個標(biāo)識位(msb) - msb 表示后面還有字節(jié)需要讀取(當(dāng)前編碼還未結(jié)束)。每個字節(jié)的低 7 位用于以 7 位一組的方式存儲數(shù)字的二進(jìn)制補碼,二進(jìn)制補碼的低位排在編碼序列的前頭。
譯者注:
可以總結(jié)出 varint 編碼最主要的三點:
- 存儲數(shù)字對應(yīng)的二進(jìn)制補碼
- 在每個字節(jié)開頭設(shè)置了 msb,標(biāo)識是否需要繼續(xù)讀取下一個字節(jié)(這種標(biāo)識實際上代替長度的作用)
- 補碼的低位排在前面
另外這里原文為 Each byte in a varint, except the last byte, has the most significant bit (msb) set,即 varint 中的每個字節(jié),除了最后一個字節(jié),都設(shè)置了最高有效位 msb。
原文的意思不容易讓人理解,實際上應(yīng)該是每個字節(jié)的最高 bit 都作為一個標(biāo)識作用,1 標(biāo)識需要繼續(xù)讀取下一個字節(jié),0 標(biāo)識當(dāng)前字節(jié)為最后一個字節(jié)。
而原文中 msb 可能僅僅指得是最高比特位等于 1 的情況,所以它有個 “除了最后一個字節(jié)” 這樣的表述。
我們來看一個例子:數(shù)字 1 該如何編碼 – 這只需要單個字節(jié),所以無需設(shè)置 msb:
0000 0001
譯者注:
第一個 bit 位還是需要用來標(biāo)識是否還有后續(xù)字節(jié),這里為 0 表示無后續(xù)字節(jié)。因為 msb 的作用相當(dāng)于長度,所以哪怕只有一個字節(jié),也是需要設(shè)置 msb 的,不然解碼的時候無法識別該讀多少個字節(jié)。這里的 “無需設(shè)置 msb” 估計又是將 msb 寓意成 bit = 1 的 msb。
來看一個稍微復(fù)雜點的例子,數(shù)字 300 該如何編碼:
1010 1100 0000 0010
以上編碼如何得出是整型 300?首先我們將每個字節(jié)的 msb 位去掉,因為這只是告訴我們是否已達(dá)到數(shù)字的末尾(如你所見,它在第一個字節(jié)中設(shè)置成 1 ,因為后面還有字節(jié)(第二個字節(jié))需要繼續(xù)讀取):
1010 1100 0000 0010
→ 010 1100 000 0010
譯者注:
這里去掉 msb 位時又將第二個字節(jié)的首位比特 0 當(dāng)做 msb 去掉了。說實話 PB 的官方文檔在很多細(xì)節(jié)還是容易讓人產(chǎn)生誤解的。
接下來你可以反轉(zhuǎn)這兩組 7 位,因為如前所述,varints 會將補碼的低位排在前面。反轉(zhuǎn)過程如下所示:
000 0010 010 1100 // 將兩個 7 位組反轉(zhuǎn)
→ 000 0010 + 010 1100
→ 100101100 // 去掉計算時多余的 0
→ 256 + 32 + 8 + 4 = 300 // 計算對應(yīng)的整型
譯者注:
varints 之所以將低位的放在前頭,是為了進(jìn)行位操作(還原數(shù)值)的時候更加方便。
Message 結(jié)構(gòu)
如我們所知,一個 protocol buffer message 實際上是一系列的鍵值對。消息的二進(jìn)制版本只使用字段的數(shù)字作為 key - 而每個字段的名稱和聲明的類型只能通過引用 message 類型的定義(即 .proto 文件)在解碼端確定。
在對一個 message 進(jìn)行編碼時,其鍵值將連接成字節(jié)流。在解碼消息時,解析器需要能夠跳過它無法識別的字段。這樣,可以將新字段添加到消息中,而不會破壞那些無法識別(新字段)的舊程序。為此,識別 message 編碼中每個字段的 key 實際上是兩個值 - 來自 .proto 文件的字段編號,以及一個提供足夠信息以查找 “值的(字節(jié))長度” 的類型。在大多數(shù)語言實現(xiàn)中,該 key 被稱為一個 tag (標(biāo)記)。
可用的類型如下:
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated,遺棄) |
4 | End group | groups (deprecated,遺棄) |
5 | 32-bit | fixed32, sfixed32, float |
message 消息流中的每個 Tag (field_number + wire_type) 都使用 varint 進(jìn)行編碼,且最后三位 bit 存儲類型 wire_type(其它位存儲字段編號 field_number)。
現(xiàn)在讓我們再看一下上面提到的簡單例子。你現(xiàn)在知道 message 消息流中的第一個數(shù)字總是一個 varint 編碼的 tag ,這里是 08,即(刪除 msb):
000 1000
我們?nèi)∽詈笕?bit 從而得到類型為 0 (varint),右移 3 位得到 varint 編碼的 1。所以我們現(xiàn)在知道字段編號為 1,并且接下來要讀取的 value 的類型為 varint。使用上一節(jié)講解的 varint 編碼相關(guān)知識,我們可以得到接下來的兩個字節(jié)代表 150。
96 01 = 1001 0110 0000 0001
→ 000 0001 + 001 0110 (drop the msb and reverse the groups of 7 bits)
→ 10010110
→ 128 + 16 + 4 + 2 = 150
更多的值類型
Signed Integers
正如我們在上一節(jié)中看到的,類型 0 對應(yīng)的各種 protocol buffer types 會被編碼為 varints。但是,在對負(fù)數(shù)進(jìn)行編碼時,signed int 類型(sint32 和 sint64)與標(biāo)準(zhǔn)的 int 類型(int32 和 int64)之間存在著重要差異。如果使用 int32 或 int64 作為負(fù)數(shù)的類型,則生成的 varint 總是十個字節(jié)長-它實際上被視為一個非常大的無符號整數(shù)。如果使用 signed int 類型(sint32 和 sint64),則生成的 varint 將使用 ZigZag 編碼,這樣效率更高。
譯者注:
如果是負(fù)數(shù),那么必須有最高位表示符號位,也就是說天然的會用盡所有字節(jié)。如果是 4 個字節(jié)的 int,那么負(fù)數(shù)總是要占 4 個字節(jié)。可以為什么 int32 會占十個字節(jié)呢?不是應(yīng)該占 5 個字節(jié)嗎(每個字節(jié)還需要拿出一個 bit 位做 msb,所以要 5 個字節(jié))?
這里是 protobuf 為了兼容性做的一種措施,為了 int32 和 int64 能夠兼容(比如在 .proto 文件中將 int32 改成 int64),所以 int32 的負(fù)數(shù)會擴展到 int64 的長度。
那么正數(shù)的兼容呢?請仔細(xì)品味上述的 varints 編碼,這種編碼天然使得 int32 和 int64 的正數(shù)兼容。
ZigZag 編碼將有符號整數(shù)映射到無符號整數(shù),因此具有較小絕對值(例如 -1)的數(shù)字也具有較小的 varint 編碼值。它通過正負(fù)整數(shù)來回 “zig-zags” 的方式做到這一點,因此 -1 被編碼為 1, 1 被編碼為 2,-2 被編碼為 3,依此類推,如同下表所示:
Signed Original | Encoded As |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2 | 4 |
... | ... |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
換句話說,每個 sint32 類型的 n 編碼處理如下:
(n << 1) ^ (n >> 31)
而對于每個 sint64 類型的 n:
(n << 1) ^ (n >> 63)
注意,第二個移位(n >> 31)部分是算術(shù)移位。因此,換句話說,移位的結(jié)果將會是一個全 0(如果 n 為正)或是全 1(如果n為負(fù))。
解析 sint32 或 sint64 時,其值將被解碼回原始的 signed 版本。
Non-varint Numbers
non-varint 數(shù)字類型很簡單 - double 和 fixed64 對應(yīng)的類型(wire type)為 1,它告訴解析器期讀取一個固定的 64 位數(shù)據(jù)塊。類似地,float 和 fixed32 對應(yīng)的類型(wire type)為 5,這意味著它期望 32 位。在這兩種情況下,值都以 little-endian (二進(jìn)制補碼低位在前)字節(jié)順序存儲。
Strings
類型(wire type)為 2(長度劃分)表示接下來的字節(jié)將是 varint 編碼的長度,之后跟指定長度的數(shù)據(jù)字節(jié)。
message Test2 {
optional string b = 2;
}
將 b 的值設(shè)置為 "testing" 后得到如下編碼:
12 07 | 74 65 73 74 69 6e 67
后七個字節(jié)為 "testing" 的 UTF8 編碼。第一個字節(jié) 0x12 為 key → 字段編碼 field_number = 2, 類型 wire type = 2。 varint 值的長度為 7,并且看到它后面有七個字節(jié) -即為我們需要讀取的值(字符串)。
譯者注:
0x12 -> 0001 0010,先進(jìn)行 varints 解碼,解碼結(jié)果為 001 0010,如果還是不清楚這個結(jié)果是怎么得出來的,可能需要重新看一遍上面的內(nèi)容。取后面三位 010 得出 wire_type = 2。剩下的位表示 field_number,容易看出 0010 = 2。
Embedded Messages(內(nèi)嵌 message)
下面的 message,內(nèi)嵌了我們之前的簡單例子 Test1:
message Test3 {
optional Test1 c = 3;
}
設(shè)置其中的 Test1 的 a = 150,最終編碼如下:
1a 03 08 96 01
正如我們所見,后三個字節(jié)和我們的第一個例子結(jié)果相同(08 96 01),在這三個字節(jié)之前為 03 編碼(代表著字節(jié)長度)-嵌入消息的處理方式與字符串完全相同(wire type = 2)。
Optional 和 Repeated 元素
如果一個 proto2 message 定義有 repeated
字段(沒有使用 [packed=true]
選項),則對應(yīng)的 message 編碼將具有零個或多個相同字段編號的鍵值對。這些重復(fù)值不必連續(xù)出現(xiàn)。它們可能與其他字段交錯。解析時將保留這些 repeated
元素彼此的相對順序,雖然相對于其他字段的排序?qū)G失。 而在 proto3 中,repeated
字段默認(rèn)使用 packed 編碼,下面將講解該方法。
譯者注:
這里官方文檔的描述又不太嚴(yán)謹(jǐn),到目前為止,proto3 只會對 primitive 類型的repeated
字段默認(rèn)進(jìn)行打包處理,string 之類的repeated
字段是不會默認(rèn)進(jìn)行打包的。詳見下面 [打包 Repeated 字段] 部分。
對于 proto3 中的任何 non-repeated 字段或 proto2 中的 optional 字段,message 編碼可能具有或不具有該字段編號對應(yīng)的鍵值對。
通常,編碼消息永遠(yuǎn)不會有多個 non-repeated 字段的實例。但是,解析器應(yīng)該能夠處理這個情況。對于數(shù)字類型和字符串,如果多次出現(xiàn)相同的字段,則解析器接受它 “看到” 的最后一個值。
對于嵌入式消息字段,解析器合并同一字段的多個實例,就像使用 Message::MergeFrom 方法一樣 - 也就是說,后面實例中的所有單個標(biāo)量字段都替換前者,單個嵌入消息被合并,而 repeated 字段將被串聯(lián)起來。這些規(guī)則的作用是使得兩個消息(字符串)串聯(lián)起來解析產(chǎn)生的結(jié)果與分別解析兩個消息(字符串)后合并結(jié)果對象產(chǎn)生的結(jié)果完全相同。也就是:
MyMessage message;
message.ParseFromString(str1 + str2);
等價于:
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
此屬性偶爾會有用,因為它允許我們合并兩條消息,即使我們不知道它們的類型。
打包 Repeated 字段
protobuf 版本 2.1.0 引入了對打包的 repeated 字段,在 proto2 中聲明為具有特殊的 [packed = true] 選項的 repeated 字段。在 proto3 中,repeated 字段默認(rèn)被打包。packed repeated 和 repeated 在編碼方式上會有所不同。沒有包含任何元素的 packed repeated 字段不會出現(xiàn)在編碼消息中。否則(包含有元素),該字段的所有元素都被打包成一個鍵值對,其中類型(wire type)為 2(length-delimited)。每個元素的編碼方式與正常情況相同,只是前面沒有鍵。
譯者注:
string 類型不會被打包,為什么?譯者認(rèn)為的原因:
將 primitive 進(jìn)行打包后編碼格式為 tag-length-value-value...,因為 primitive 使用 varints 進(jìn)行編碼,讀取 length 長度的字節(jié)后有 msb 作為讀取 value 的邊界標(biāo)識,因此可以對 value-value-value 進(jìn)行正確的邊界識別,正確的一個個取出 value。如果 value 是 string 類型,對應(yīng)的 packed 結(jié)果如果是 tag-length-value-value-value...(其中 length 是后續(xù)的字節(jié)數(shù))因為 string 直接 UTF-8 編碼,讀取出對應(yīng)的字節(jié)后,無法正確劃分 value 邊界。
如果 packed 結(jié)果為 tag-length-[length-value-length-value],那么和不打包的編碼結(jié)果 tag-length-value-tag-length-value-tag-length-value.... 相比,packed 結(jié)果多出了一個 length (第一個出現(xiàn)的 length,這個 length 表達(dá)后續(xù)所有的字節(jié)數(shù),這個可能會很大),同時少了后續(xù)每個字節(jié) tag (tag 的大小通常很小,最大不會超過 5 字節(jié))的開銷,那么最后 packed 的結(jié)果是增大開銷還是減少開銷就不一定了。所以不對 Length-delimited 系列的類型進(jìn)行打包。
例如,假設(shè)我們有如下消息類型:
message Test4 {
repeated int32 d = 4 [packed=true];
}
現(xiàn)在假設(shè)你構(gòu)造一個 Test4,為重復(fù)的字段 d 分別賦值為 3、270 和 86942。那么對應(yīng)的編碼將是:
22 // key (或稱 tag,字段編碼 4, wire type 2)
06 // payload 長度 (即后面需要讀取的所有值的長度6 bytes)
03 // first element (varint 3)
8E 02 // second element (varint 270)
9E A7 05 // third element (varint 86942)
只有原始數(shù)字類型(使用 Varint,32-bit 或 64-bit)的 repeated 字段才能聲明為 “packed”。
請注意,雖然通常沒有理由為打包的 repeated 字段編碼多個鍵值對,但編碼器必須準(zhǔn)備好接受多個鍵值對。在這種情況下,應(yīng)該連接 payload (對應(yīng)的 value)。每對必須包含許多元素。
protocol buffer 解析器必須能夠解析打包的 repeated 字段,就好像它們沒有打包一樣,反之亦然。這允許以前向和后向兼容的方式將 [packed = true] 添加到現(xiàn)有字段。
字段排序
雖然我們可以在 .proto
中以任何順序使用字段編號,但在序列化消息時,其已知字段應(yīng)按字段編號順序編寫,如提供的 C ++,Java 和 Python 序列化代碼實現(xiàn)的那樣。這允許解析代碼能夠依賴于字段序列進(jìn)行優(yōu)化。但是,protocol buffer 解析器必須能夠以任何順序解析字段,因為并非所有 message 都是通過簡單地序列化對象來創(chuàng)建的-例如,通過簡單連接來合并兩個消息(有時很有用)。
如果一個 message 具有 [未知字段],當(dāng)前的 Java 和 C++ 實現(xiàn)將在按順序排序的已知字段之后以任意順序?qū)懭胨鼈儯?dāng)前的 Python 實現(xiàn)不會記錄(跟蹤)未知字段。
汪
汪