Google Protocol Buffer Encoding

Google Protocol Buffer的安裝使用參考官網(wǎng):
https://github.com/google/protobuf/tree/master/objectivec
Protocol Buffers 是一種輕便高效的結(jié)構(gòu)化數(shù)據(jù)存儲格式,可以用于結(jié)構(gòu)化數(shù)據(jù)串行化,或者說序列化。它很適合做數(shù)據(jù)存儲或 RPC 數(shù)據(jù)交換格式??捎糜谕ㄓ崊f(xié)議、數(shù)據(jù)存儲等領(lǐng)域的語言無關(guān)、平臺無關(guān)、可擴(kuò)展的序列化結(jié)構(gòu)數(shù)據(jù)格式。

Language Source
C++ (include C++ runtime and protoc) src
Java java
Python python
Objective-C objectivec
C# csharp
JavaNano javanano
JavaScript js
Ruby ruby
Go golang/protobuf
PHP php
Dart dart-lang/protobuf

Protobuf 的優(yōu)點(diǎn)

Protobuf 有如 XML,不過它更小、更快、也更簡單。你可以定義自己的數(shù)據(jù)結(jié)構(gòu),然后使用代碼生成器生成的代碼來讀寫這個(gè)數(shù)據(jù)結(jié)構(gòu)。你甚至可以在無需重新部署程序的情況下更新數(shù)據(jù)結(jié)構(gòu)。只需使用 Protobuf 對數(shù)據(jù)結(jié)構(gòu)進(jìn)行一次描述,即可利用各種不同語言或從各種不同數(shù)據(jù)流中對你的結(jié)構(gòu)化數(shù)據(jù)輕松讀寫。

它有一個(gè)非常棒的特性,即“向后”兼容性好,人們不必破壞已部署的、依靠“老”數(shù)據(jù)格式的程序就可以對數(shù)據(jù)結(jié)構(gòu)進(jìn)行升級。這樣您的程序就可以不必?fù)?dān)心因?yàn)橄⒔Y(jié)構(gòu)的改變而造成的大規(guī)模的代碼重構(gòu)或者遷移的問題。因?yàn)樘砑有碌南⒅械?field 并不會引起已經(jīng)發(fā)布的程序的任何改變。

Protobuf 語義更清晰,無需類似 XML 解析器的東西(因?yàn)?Protobuf 編譯器會將 .proto 文件編譯生成對應(yīng)的數(shù)據(jù)訪問類以對 Protobuf 數(shù)據(jù)進(jìn)行序列化、反序列化操作)。

使用 Protobuf 無需學(xué)習(xí)復(fù)雜的文檔對象模型,Protobuf 的編程模式比較友好,簡單易學(xué),同時(shí)它擁有良好的文檔和示例,對于喜歡簡單事物的人們而言,Protobuf 比其他的技術(shù)更加有吸引力。

Google Protocol Buffer 的 Encoding

Protobuf 序列化后所生成的二進(jìn)制消息非常緊湊,這得益于 Protobuf 采用的非常巧妙的 Encoding 方法。

考察消息結(jié)構(gòu)之前,讓我首先要介紹一個(gè)叫做 Varint 的術(shù)語。

Varint 是一種緊湊的表示數(shù)字的方法。它用一個(gè)或多個(gè)字節(jié)來表示一個(gè)數(shù)字,值越小的數(shù)字使用越少的字節(jié)數(shù)。這能減少用來表示數(shù)字的字節(jié)數(shù)。

比如對于 int32 類型的數(shù)字,一般需要 4 個(gè) byte 來表示。但是采用 Varint,對于很小的 int32 類型的數(shù)字,則可以用 1 個(gè) byte 來表示。當(dāng)然凡事都有好的也有不好的一面,采用 Varint 表示法,大的數(shù)字則需要 5 個(gè) byte 來表示。從統(tǒng)計(jì)的角度來說,一般不會所有的消息中的數(shù)字都是大數(shù),因此大多數(shù)情況下,采用 Varint 后,可以用更少的字節(jié)數(shù)來表示數(shù)字信息。下面就詳細(xì)介紹一下 Varint。

Varint 中的每個(gè) byte 的最高位 bit 有特殊的含義,如果該位為 1,表示后續(xù)的 byte 也是該數(shù)字的一部分,如果該位為 0,則結(jié)束。其他的 7 個(gè) bit 都用來表示數(shù)字。因此小于 128 的數(shù)字都可以用一個(gè) byte 表示。大于 128 的數(shù)字,比如 300,會用兩個(gè)字節(jié)來表示:1010 1100 0000 0010

下圖演示了 Google Protocol Buffer 如何解析兩個(gè) bytes。注意到最終計(jì)算前將兩個(gè) byte 的位置相互交換過一次,這是因?yàn)?Google Protocol Buffer 字節(jié)序采用 little-endian 的方式。

圖 6. Varint 編碼
圖 6. Varint 編碼

消息經(jīng)過序列化后會成為一個(gè)二進(jìn)制數(shù)據(jù)流,該流中的數(shù)據(jù)為一系列的 Key-Value 對。如下圖所示:

圖 7. Message Buffer
圖 7. Message Buffer

采用這種 Key-Pair 結(jié)構(gòu)無需使用分隔符來分割不同的 Field。對于可選的 Field,如果消息中不存在該 field,那么在最終的 Message Buffer 中就沒有該 field,這些特性都有助于節(jié)約消息本身的大小。

以代碼清單 1 中的消息為例。假設(shè)我們生成如下的一個(gè)消息 Test1:

Test1.id = 10;

Test1.str = “hello”;

則最終的 Message Buffer 中有兩個(gè) Key-Value 對,一個(gè)對應(yīng)消息中的 id;另一個(gè)對應(yīng) str。

Key 用來標(biāo)識具體的 field,在解包的時(shí)候,Protocol Buffer 根據(jù) Key 就可以知道相應(yīng)的 Value 應(yīng)該對應(yīng)于消息中的哪一個(gè) field。

Key 的定義如下:

(field_number << 3) | wire_type

可以看到 Key 由兩部分組成。第一部分是 field_number,比如消息 lm.helloworld 中 field id 的 field_number 為 1。第二部分為 wire_type。表示 Value 的傳輸類型。

Wire Type 可能的類型如下表所示:

表 1. Wire Type
Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimi string, bytes, embedded messages, packed repeated fields
3 Start group Groups (deprecated)
4 End group Groups (deprecated)
5 32-bit fixed32, sfixed32, float

在我們的例子當(dāng)中,field id 所采用的數(shù)據(jù)類型為 int32,因此對應(yīng)的 wire type 為 0。細(xì)心的讀者或許會看到在 Type 0 所能表示的數(shù)據(jù)類型中有 int32 和 sint32 這兩個(gè)非常類似的數(shù)據(jù)類型。Google Protocol Buffer 區(qū)別它們的主要意圖也是為了減少 encoding 后的字節(jié)數(shù)。

在計(jì)算機(jī)內(nèi),一個(gè)負(fù)數(shù)一般會被表示為一個(gè)很大的整數(shù),因?yàn)橛?jì)算機(jī)定義負(fù)數(shù)的符號位為數(shù)字的最高位。如果采用 Varint 表示一個(gè)負(fù)數(shù),那么一定需要 5 個(gè) byte。為此 Google Protocol Buffer 定義了 sint32 這種類型,采用 zigzag 編碼。

Zigzag 編碼用無符號數(shù)來表示有符號數(shù)字,正數(shù)和負(fù)數(shù)交錯(cuò),這就是 zigzag 這個(gè)詞的含義了。

如圖所示:

圖 8. ZigZag 編碼
圖 8. ZigZag 編碼

使用 zigzag 編碼,絕對值小的數(shù)字,無論正負(fù)都可以采用較少的 byte 來表示,充分利用了 Varint 這種技術(shù)。

其他的數(shù)據(jù)類型,比如字符串等則采用類似數(shù)據(jù)庫中的 varchar 的表示方法,即用一個(gè) varint 表示長度,然后將其余部分緊跟在這個(gè)長度部分之后即可。

通過以上對 protobuf Encoding 方法的介紹,想必您也已經(jīng)發(fā)現(xiàn) protobuf 消息的內(nèi)容小,適于網(wǎng)絡(luò)傳輸。假如您對那些有關(guān)技術(shù)細(xì)節(jié)的描述缺乏耐心和興趣,那么下面這個(gè)簡單而直觀的比較應(yīng)該能給您更加深刻的印象。

對于代碼清單 1 中的消息,用 Protobuf 序列化后的字節(jié)序列為:

08 65 12 06 48 65 6C 6C 6F 77

而如果用 XML,則類似這樣:

|

31 30 31 3C 2F 69 64 3E 3C 6E 61 6D 65 3E 68 65

6C 6C 6F 3C 2F 6E 61 6D 65 3E 3C 2F 68 65 6C 6C

6F 77 6F 72 6C 64 3E

一共 55 個(gè)字節(jié),這些奇怪的數(shù)字需要稍微解釋一下,其含義用 ASCII 表示如下:

<``helloworld``>

<``id``>101</``id``>

<``name``>hello</``name``>

</``helloworld``>

Base 128 Varints

Google Protobuf里面提出了“Base 128 Varints”編碼,這是一種變字節(jié)長度的編碼,官方描述為:varints是用一個(gè)或多個(gè)字節(jié)序列化整形的一種方法。我理解要點(diǎn)有三個(gè)(1)操作是序列化(2)操作對象是整形(3)變長編碼。重點(diǎn)是最后一點(diǎn),他是如何編碼的呢?

(1)除了最后一個(gè)字節(jié),varint中的每個(gè)字節(jié)的最高位設(shè)為1,表示后面還有字節(jié)出現(xiàn)

(2)每個(gè)字節(jié)的低7位看成是一個(gè)組(group),這個(gè)組和他相鄰的下一個(gè)7位組共同存儲某個(gè)整形的“組合表示”,最低有效組在前面。
(1)一個(gè)字節(jié)。下面只有一個(gè)字節(jié),所以最高位是0,表示十進(jìn)制1
0000 0001
(2)兩個(gè)字節(jié)。
1010 1100 0000 0010
由于第一個(gè)字節(jié)后面還有一個(gè)字節(jié),所以第一個(gè)字節(jié)的最高位設(shè)置為1,表示后面還有后繼字節(jié),第二個(gè)字節(jié)的最高位為0。去掉每個(gè)字節(jié)的最高位,我們對兩個(gè)字節(jié)進(jìn)行分組。第一個(gè)7位組:0101100,第二個(gè)7位組:0000010,組合起來就是:0101100 0000010,由于最低有效組在前面,所以兩個(gè)7位組進(jìn)行調(diào)換,結(jié)果為:0000010 0101100,中間的空格是為了大家區(qū)分,去掉前面的0,也就是:100101100,十進(jìn)制為12^8 + 12^5 + 12^3 + 12^2 = 256 + 32 + 8 + 4 = 300。

(3)三個(gè)字節(jié)。我們換種方式,給定某個(gè)整形,要求寫出他的varint表示,這里當(dāng)然是落在需要3個(gè)字節(jié)表示的整形。16380可以嗎?不可以!因?yàn)?6380 < 2^14,所以2個(gè)字節(jié)足以,我們就以27491為例,27491的二進(jìn)制表示為:
0110 1011 0110 0011
注意這里是高字節(jié)之前,低字節(jié)在后,和varint的編碼規(guī)則相反,首先按照7位一組劃分為:0000001 1010110 1100011,然后反轉(zhuǎn)為:1100011 1010110 0000001,最后將末尾的7位組前面補(bǔ)0組成一個(gè)字節(jié),兩個(gè)7位組都補(bǔ)1分別組成一個(gè)字節(jié):
11100011 11010110 00000001

(4)更多字節(jié)。聰明的你可能發(fā)現(xiàn),varint編碼中4個(gè)字節(jié)最大表示的數(shù)為228,非常正確,同時(shí)說明了天下沒有免費(fèi)的午餐,有得有失。Protobuf引入了fixed32解決這個(gè)問題的,如果某個(gè)字段經(jīng)常大于228,應(yīng)當(dāng)優(yōu)選fixed32,他是固定長度為32位的。

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

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

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