protocol buffer的高效编码方式
文章目錄
- 簡(jiǎn)介
- 定義一個(gè)簡(jiǎn)單的message
- Base 128 Varints
- 消息體的結(jié)構(gòu)
- 符號(hào)整數(shù)
- 字符串
- 嵌套的消息
- 總結(jié)
簡(jiǎn)介
protocol buffer這種優(yōu)秀的編碼方式,究竟底層是怎么工作的呢?為什么它可以實(shí)現(xiàn)高效快速的數(shù)據(jù)傳輸呢?這一切都要從它的編碼方式說(shuō)起。
定義一個(gè)簡(jiǎn)單的message
我們知道protocol buffer的主體就是message,接下來(lái)我們從一個(gè)簡(jiǎn)單的message出發(fā),詳細(xì)講解protobuf中的編碼方式。
比如下面的一個(gè)非常簡(jiǎn)單的消息對(duì)象:
message Student {optional int32 age = 1; }在上面的例子中,我們定義了一個(gè)Student消息對(duì)象,并給他定義了一個(gè)名叫age的字段,并給它設(shè)置一個(gè)值叫做22。然后使用protobuf將其進(jìn)行序列化,這么大的一個(gè)對(duì)象,對(duì)其序列化之后的字節(jié)如下所示:
08 96 00很簡(jiǎn)單,使用三個(gè)字節(jié)就可以表示一個(gè)messag對(duì)象,數(shù)據(jù)量非常小。
那么這三個(gè)字節(jié)到底表示什么意思呢?一起來(lái)看看吧 。
Base 128 Varints
在解釋上面的三個(gè)字節(jié)的含義之前,我們需要了解一個(gè)varints的概念。
什么叫Varints呢?就是序列化整數(shù)的時(shí)候,占用的空間大小是不一樣的,小的整數(shù)占用的空間小,大的整數(shù)占用的空間大,這樣不用固定一個(gè)具體的長(zhǎng)度,可以減少數(shù)據(jù)的長(zhǎng)度,但是會(huì)帶來(lái)解析的復(fù)雜度。
那么怎么知道這個(gè)數(shù)據(jù)到底需要幾個(gè)byte呢?在protobuf中,每個(gè)byte的最高位是一個(gè)判斷位,如果這個(gè)位被置位1,則表示后面一個(gè)byte和該byte是一起的,表示同一個(gè)數(shù),如果這個(gè)位被置位0,則表示后面一個(gè)byte和該byte沒(méi)有關(guān)系,數(shù)據(jù)到這個(gè)byte就結(jié)束了。
舉個(gè)例子,一個(gè)byte是8位,如果表示的是整數(shù)1,那么可以用下面的byte來(lái)表示:
0000 0001如果一個(gè)byte裝不下的整數(shù),那么就需要使用多個(gè)byte來(lái)進(jìn)行連接操作,比如下面的數(shù)據(jù)表示的是300:
1010 1100 0000 0010為什么是300呢?首先看第一個(gè)byte,它的首位是1,表示后面還有一個(gè)byte。再看第二個(gè)byte,它的首位是0,表示到此就結(jié)束了。我們把判斷位去掉,變成下面的數(shù)字:
010 1100 000 0010這時(shí)候還不能計(jì)算數(shù)據(jù)的值,因?yàn)樵趐rotobuf中,byte的位數(shù)是反過(guò)來(lái)的,所以我們需要把上面的兩個(gè)byte交換一下位置:
000 0010 010 1100也就是:
10 010 1100=256 + 32 + 8 + 4 = 300
消息體的結(jié)構(gòu)
從message的定義可以知道,protobuf中的消息體的結(jié)構(gòu)是key=value的形式,其中的key就是message中定義的字段的整數(shù)值1,2,3,4等。而value就是真正對(duì)其設(shè)置的值。
當(dāng)一個(gè)消息被編碼之后,這些key和value會(huì)被連接在一起,組成一個(gè)byte stream。當(dāng)要對(duì)其進(jìn)行解析的時(shí)候,需要定位到key和value的具體長(zhǎng)度,所以在key中需要包含兩部分,第一個(gè)部分就是字段在proto文件中的值,第二個(gè)部分就是value部分占用的長(zhǎng)度大小。
只有通過(guò)這兩個(gè)部分的值結(jié)合起來(lái),解析器才能夠正確的對(duì)字段進(jìn)行解析。
key的這種格式,被稱為 wire types,有哪些 wire types呢?我們看一下:
| 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 |
可以看到除了3,4兩種類型之外,其他的類型可以分為三類,一類是固定長(zhǎng)度的類型,如1,5,他們分別是64位和32位的數(shù)字。
第二類是0,表示Varint,這是一種可變類型,用來(lái)表示通用的數(shù)字類型,bool類型和枚舉類型。第三類2,表示長(zhǎng)度區(qū)分的類型,這種類型通常用來(lái)表示字符串,字節(jié)數(shù)字等。
所有的key都是一個(gè)varint類型,它的值是:(field_number << 3) | wire_type,也就是說(shuō)key的最后三個(gè)位,用來(lái)存儲(chǔ)wire類型。
上面我們例子中的key的值是08,用二進(jìn)制表示:
000 1000最后三位是0,表示是一個(gè)Varint類型,將08右移三位,得到1,表示key表示的字段是1這個(gè)字段,也就是age。
然后我們看下剩下的部分96 00,換成二進(jìn)制是:
96 00 = 1001 0110 0000 0000根據(jù)Varint的定義,第一位表示的是連接位,表示第二個(gè)字節(jié)的內(nèi)容和第一個(gè)字節(jié)的內(nèi)容是一起的。對(duì)于Varint來(lái)說(shuō),需要將低位的字節(jié)和高位的字節(jié)進(jìn)行交換,如下:
1001 0110 0000 0000 去掉最高位的1 : 001 0110 0000 0000 交換低位字節(jié)和高位字節(jié): 0000 0000 001 0110上面的值是16 + 4 + 2 = 22
這樣我們就得到了值為1的key,對(duì)應(yīng)的value是22。
符號(hào)整數(shù)
我們知道有兩種表示符號(hào)整數(shù)的方式,一種是標(biāo)準(zhǔn)的int類型:int32 和 int64,一種是帶符號(hào)的int類型:sint32 和 sint64。
這兩種類型的區(qū)別在于對(duì)應(yīng)負(fù)整數(shù)的表示上。對(duì)于int32和int64來(lái)說(shuō),所有的負(fù)整數(shù)都是以十個(gè)字節(jié)來(lái)表示的,所以占用的空間會(huì)比較大,不適合用來(lái)表示負(fù)整數(shù)。
如果使用sint32 和 sint64,那么使用的編碼方式是ZigZag,對(duì)于負(fù)整數(shù)來(lái)說(shuō)更加有效。
ZigZag將帶符號(hào)的整數(shù)和無(wú)符號(hào)的整數(shù)進(jìn)行映射,對(duì)于每個(gè)n來(lái)說(shuō),將會(huì)使用下面的公式來(lái)編碼:
(n << 1) ^ (n >> 31)對(duì)于sint64來(lái)說(shuō)就是:
(n << 1) ^ (n >> 64)舉個(gè)例子:
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| 2147483647 | 4294967294 |
| -2147483648 | 4294967295 |
字符串
字符串的wire類型是2,說(shuō)明它的值是一個(gè)varint編碼的長(zhǎng)度。舉個(gè)例子:
message Student {optional string name = 2; }上我們給Student定義了第二個(gè)屬性name,假如給name賦值 “testing” ,那么得到的編碼是:
12 07 [74 65 73 74 69 6e 67]中括號(hào)的編碼就是"testing"的UTF8表示。
0x12 可以這樣解析:
0x12 → 0001 0010 (binary representation) → 00010 010 (regroup bits) → field_number = 2, wire_type = 20x12表示字段2的類型是2,后面跟著的07就表示后續(xù)byte字節(jié)的長(zhǎng)度了。
嵌套的消息
消息中可以嵌套消息,我們看一個(gè)例子:
message Teacher {optional Student s = 3; }假如我們把s的age字段設(shè)置為22,就和第一個(gè)例子一樣,那么上面的編碼就是:
1a 03 08 96 00可以看到后面的三個(gè)字節(jié)和第一個(gè)例子是一樣的。前面兩個(gè)字節(jié)的判斷方式和字符串是一值的,這樣就不再多講。
總結(jié)
好了,protobuf的基本編碼規(guī)則和實(shí)現(xiàn)已經(jīng)講完了。聽(tīng)起來(lái)是不是很奇妙?
本文已收錄于 http://www.flydean.com/03-protobuf-encoding/
最通俗的解讀,最深刻的干貨,最簡(jiǎn)潔的教程,眾多你不知道的小技巧等你來(lái)發(fā)現(xiàn)!
歡迎關(guān)注我的公眾號(hào):「程序那些事」,懂技術(shù),更懂你!
超強(qiáng)干貨來(lái)襲 云風(fēng)專訪:近40年碼齡,通宵達(dá)旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的protocol buffer的高效编码方式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: protocol buffer没那么难,
- 下一篇: netty系列之:在netty中使用pr