protobuf編碼(翻譯)

原文https://7byte.github.io/2017/07/09/protobuf-encoding/

英文原文:https://developers.google.com/protocol-buffers/docs/encoding

本文描述了protocol buffer 消息的二進(jìn)制格式。當(dāng)你在你的應(yīng)用中使用protocol buffer 時(shí)無需了解這些細(xì)節(jié)。但是,要想理解不同的protocol buffer 格式對(duì)最終編碼生成的消息大小有何影響,了解這些將非常有幫助。

一個(gè)簡(jiǎn)單消息

假設(shè)你有如下簡(jiǎn)單的消息定義:

message Test1 {
  required int32 a = 1;
}

在應(yīng)用中,創(chuàng)建一個(gè)名為Test1的消息并且對(duì)a賦值150。然后將這個(gè)消息序列化到一個(gè)輸出流。如果查看編碼出的消息內(nèi)容,你將看到如下的三個(gè)字節(jié):

08 96 01

只有幾個(gè)數(shù)值——這些東西代表什么?且看下文……

Base 128 Varints

在理解上面的簡(jiǎn)單消息是如何編碼之前,你需要先了解什么是Varints。Varints是使用一個(gè)或多個(gè)字節(jié)對(duì)整型數(shù)字序列化的方法。數(shù)值越小,序列化后所占的字節(jié)數(shù)越少。

除了最后一個(gè)字節(jié),Varints中的每個(gè)字節(jié)設(shè)置了最高有效位(msb)用來標(biāo)示后續(xù)的字節(jié)也是該數(shù)字的一部分。一個(gè)以補(bǔ)碼表示的數(shù)字按7位一組的方式分成若干組,每組存儲(chǔ)在字節(jié)的低7位,低位數(shù)字在前面(即小端序)。

例如,數(shù)字1只有一個(gè)字節(jié),所以不需要設(shè)置msb:

0000 0001

數(shù)字300要稍微復(fù)雜一些:

1010 1100 0000 0010

你怎么知道這是300?首先,去掉每個(gè)字節(jié)中的msb,因?yàn)閙sb的作用只是告訴我們是否到達(dá)了數(shù)字的末尾(正如你所看到的,第一個(gè)字節(jié)設(shè)置了msb,表示后續(xù)字節(jié)也是varint的一部分):

1010 1100 0000 0010
→ 010 1100  000 0010

反轉(zhuǎn)兩組7位數(shù)值,因?yàn)関arints將數(shù)字的低位放在前面。然后把兩組數(shù)值拼接起來:

000 0010  010 1100
→  000 0010 ++ 010 1100
→  100101100
→  256 + 32 + 8 + 4 = 300

消息結(jié)構(gòu)

正如你所知道的,protocol buffer消息是一系列鍵值對(duì)。一個(gè)二進(jìn)制消息使用字段的編號(hào)作為鍵——字段的名字和聲明類型只有在解碼結(jié)束后參考消息類型定義(比如.proto文件)才能確定。

編碼消息時(shí),所有的鍵和值被拼接在一起寫入字節(jié)流。解碼消息時(shí),分析器需要能夠跳過無法識(shí)別的字段。這樣的話,就可以在消息中加入新的字段時(shí)無須破壞舊程序,即使舊程序不知道這些新字段。為了這個(gè)目的,每個(gè)鍵值對(duì)的“key”實(shí)際上是由兩個(gè)值組成——.proto文件中字段的編號(hào)和一個(gè)wire type,wire type提供了“value”的長度信息。

可用的wire type如下:

類型 含義 用途
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

消息流里的每個(gè)鍵是一個(gè)varint值:(field_number << 3) | wire_type,也就是說數(shù)字的低3位用來記錄wire type。

再次回到我們的簡(jiǎn)單示例,你現(xiàn)在知道了字節(jié)流的第一個(gè)數(shù)字永遠(yuǎn)是一個(gè)varint類型的鍵值,即示例中的08,也就是(去掉msb后):

000 1000

取最后3位可得wire type(0),然后右移3位可得字段編號(hào)(1)?,F(xiàn)在你知道了tag是1,并且字段的值是varint類型。用上一節(jié)中學(xué)到的varint解碼相關(guān)知識(shí),你將會(huì)看到后面兩個(gè)字節(jié)存儲(chǔ)了150這個(gè)數(shù)字。

96 01 = 1001 0110  0000 0001
       → 000 0001  ++  001 0110 (drop the msb and reverse the groups of 7 bits)
       → 10010110
       → 2 + 4 + 16 + 128 = 150

其它值類型

有符號(hào)整型

正如你在上一節(jié)里看到的,protocol buffer中所有wire type為0的類型都被編碼成varint。然而,有符號(hào)int類型(sint32sint64)和“標(biāo)準(zhǔn)”int類型(int32int64)這兩者在處理負(fù)數(shù)編碼時(shí)有很重要的區(qū)別。如果你使用int32或者int64作為一個(gè)負(fù)數(shù)的類型,varint編碼后的結(jié)果永遠(yuǎn)有10字節(jié)之長,實(shí)際上就像是在處理一個(gè)非常大的無符號(hào)整型。如果你使用有符號(hào)int類型(sint32sint64),varint將使用更高效的ZigZag(之字形)編碼。

ZigZag編碼將有符號(hào)整數(shù)映射到無符號(hào)整數(shù),這樣,具有較小絕對(duì)值的數(shù)字(例如-1)也具有較小的varint編碼值。它以“之字形”來回處理正負(fù)整數(shù),所以-1被編碼成1,1被編碼成2,-2被編碼成3,以此類推,如下表所示:

有符號(hào)原始數(shù)字 編碼為
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295

換句話說,對(duì)每個(gè)sint32類型的n值以如下方式編碼:

(n << 1) ^ (n >> 31)

對(duì)64位:

(n << 1) ^ (n >> 63)

注意,第二個(gè)位移操作——(n >> 31)——是一個(gè)算數(shù)位移。也就是說,位移的結(jié)果要么所有位全是0(如果n是正數(shù)),要么全是1(如果n是負(fù)數(shù))。

當(dāng)解析到sint32或是sint64時(shí),對(duì)應(yīng)的值被解碼為原始的、有符號(hào)形式。

非varint數(shù)字

非varint數(shù)字類型比較簡(jiǎn)單——doublefixed64使用wire type 1來告訴解析器有一塊64位大小的數(shù)據(jù),同理,floatfixed32使用wire type 5來告訴解析器有一塊32位大小的數(shù)據(jù)。兩種類型中的數(shù)值均以小端字節(jié)序編碼。

字符串

wire type 2(長度分隔)表示值為一個(gè)包含長度信息、攜帶指定個(gè)數(shù)字節(jié)的varint。

message Test2 {
  required string b = 2;
}

對(duì)b賦值“testing”將會(huì)得到:

12 07 74 65 73 74 69 6e 67

紅色的字節(jié)(74 65 73 74 69 6e 67)是“testing”的UTF8編碼。這里的鍵是0x12→ tag = 2, type = 2。表示長度的varint值為7,你瞧,我們看到在它后面有七個(gè)字節(jié)——我們的字符串。

嵌套消息

這里有一個(gè)message定義,它以嵌套了我們的示例消息:

message Test3 {
  required Test1 c = 3;
}

同樣對(duì)Test1中的a賦值為150,編碼后:

1a 03 08 96 01

可以看到,最后三個(gè)字節(jié)與第一個(gè)例子中的完全相同(08 96 01)。在它們前面是數(shù)字3——嵌套消息與字符串(wire type = 2)的處理方式完全相同。

可選和repeated元素

如果一個(gè)proto2消息定義了repeated元素(沒有設(shè)置[packed]=true),那么編碼后的消息會(huì)有0個(gè)或多個(gè)擁有相同tag編號(hào)的鍵值對(duì)。這些重復(fù)值不必連續(xù),它們可能和其它的字段交錯(cuò)在一起。在解析時(shí),元素相對(duì)于彼此的順序保持不變,但是相對(duì)于其它字段的順序信息會(huì)丟失。在proto3中,repeated字段使用packed 編碼,你可以閱讀下面的內(nèi)容。

對(duì)proto3中的任何一個(gè)non-repeated字段,或是proto2中的optional字段,編碼后的消息可能有,也可能沒有這個(gè)tag編號(hào)字段的鍵值對(duì)。

通常來說,編碼的消息永遠(yuǎn)不會(huì)有一個(gè)non-repeated字段的多個(gè)示例。然而,真碰到這種情況時(shí)我們期望解析器也能夠正常處理。對(duì)數(shù)字類型和字符串,如果同一個(gè)字段出現(xiàn)多次,解析器將接受它所看到的最后哪個(gè)值。對(duì)于嵌套消息字段,解析器會(huì)合并同一個(gè)字段的的多個(gè)實(shí)例,就像使用Message::MergeFrom方法——所有的歧義字段都會(huì)用后一個(gè)實(shí)例中的字段替換,歧義嵌套消息被合并,并且repeated字段會(huì)拼接起來。這些規(guī)則的效果就是,解析兩個(gè)消息的串聯(lián),與你分別解析這兩條消息然后合并的結(jié)果完全相同。即:

MyMessage message;
message.ParseFromString(str1 + str2);

等于:

MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

這個(gè)特性有時(shí)候很有用,因?yàn)樗试S你在完全不知道兩個(gè)消息的類型時(shí)合并它們。

Packed repeated字段

2.1.0版本中引入了packed repeated字段,在proto2中它被聲明成帶有[packed=true]選項(xiàng)的repeated字段。在proto3中,repeated字段默認(rèn)會(huì)按packed處理。這些功能與repeated字段很類似,但是有不一樣的編碼規(guī)則。編碼生成的消息中不會(huì)出現(xiàn)包含零元素的packed repeated字段。否則,所有的元素都被打包成一個(gè)wire type為2(長度分隔)的鍵值對(duì)。每個(gè)元素都按正常的、相同的方式編碼,除了前面沒有tag。

例如,想象你有這樣一個(gè)消息類型:

message Test4 {
  repeated int32 d = 4 [packed=true];
}

現(xiàn)在,假設(shè)你構(gòu)造了一個(gè)Test4,給repeated字段d設(shè)值3、270和86942。然后,編碼的形式將是:

22        // tag (field number 4, wire type 2)
06        // payload size (6 bytes)
03        // first element (varint 3)
8E 02     // second element (varint 270)
9E A7 05  // third element (varint 86942)

只有原始數(shù)字類型的repeated字段(使用varint、32-bit、或者64-bit類型)才能聲明為“packed”。

請(qǐng)注意,盡管通常沒有理由將多個(gè)鍵值對(duì)編碼成一個(gè)packed repeated字段,但編碼器必須做好接受多個(gè)鍵值對(duì)的準(zhǔn)備。在這種情況下,所有載荷(payloads)應(yīng)該拼接到一起。每一對(duì)都必須包含完整的元素。

Protocol buffer解析器必須能夠解析以packed方式編譯而成的repeated字段,就好像它們沒有被打包一樣,反之亦然。這樣就能允許以向前和向后兼容的方式向現(xiàn)有字段添加[packed=true]。

字段順序

雖然可以在.proto中以任意順序使用字段號(hào),但當(dāng)消息被序列化時(shí),已知字段應(yīng)該按字段號(hào)順序?qū)懭?,正如所提供的C++、Java和Python序列化代碼。這使得解析代碼可以使用依賴于字段號(hào)的優(yōu)化。但是,protocol buffer解析器必須能夠以任意順序解析字段,因?yàn)椴⒎撬邢⒍际峭ㄟ^簡(jiǎn)單地序列化一個(gè)對(duì)象來創(chuàng)建的——例如,通過簡(jiǎn)單地拼接兩個(gè)消息來合并它們有時(shí)候是很有用的。

如果一個(gè)消息具有未知字段,當(dāng)前的Java和C++實(shí)現(xiàn)會(huì)在順序排列已知字段之后按任意順序?qū)懭胛粗侄?。?dāng)前的Python實(shí)現(xiàn)不處理未知字段。

版權(quán)說明

除另有說明外,本頁面的內(nèi)容是根據(jù)知識(shí)共享署名3.0許可證授權(quán)的,代碼示例根據(jù)Apache 2.0許可證授權(quán)。有關(guān)詳細(xì)信息,請(qǐng)參閱我們的網(wǎng)站政策。Java是甲骨文和/或其子公司的注冊(cè)商標(biāo)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容