Protocol Buffer数据编码
這是一篇讓你對Protocol Buffer知其然亦知其所以然的文檔,即便你在并不了解這其中的技術(shù)細節(jié)和處理機制的情況下,仍然能夠在你的應(yīng)用程序中正常的使用Protocol Buffer,然而我相信,通過對這些細節(jié)和機制的深入了解,不僅可以讓你更好的使用和駕馭Protocol Buffer,而且還能深深地感受到Google工程師的智慧和高超的編程技藝,因此在我看來,深入的研習對我們編程能力的提高和思路的拓寬都是大有裨益的。不積跬步無以致千里。
簡單消息編碼布局
? ? ? 讓我們先看一下下面的消息定義示例:? ? ? message Test1 {
??? ?? ?? required int32 a = 1;
????? }
????? 假設(shè)我們在應(yīng)用程序中將字段a的值設(shè)置為150(十進制),此后再將該對象序列化到Binary文件中,你可以看到文件的數(shù)據(jù)為:
??????08 96 01
????? 這3個字節(jié)的含義又是什么呢?它們又是按照什么樣的編碼規(guī)則生成的呢?讓我們拭目以待。
?? ?
Base 128 Varints
?? ?? 在理解Protocol Buffer的編碼規(guī)則之前,你首先需要了解varints。varints是一種使用一個或多個字節(jié)表示整型數(shù)據(jù)的方法。其中數(shù)值本身越小,其所占用的字節(jié)數(shù)越少。?? ?? 在varint中,除了最后一個字節(jié)之外的每個字節(jié)中都包含一個msb(most significant bit)設(shè)置(使用最高位),這意味著其后的字節(jié)是否和當前字節(jié)一起來表示同一個整型數(shù)值。而字節(jié)中的其余七位將用于存儲數(shù)據(jù)本身。由此我們可以簡單的解釋一下Base 128,通常而言,整數(shù)數(shù)值都是由字節(jié)表示,其中每個字節(jié)為8位,即Base 256。然而在Protocol Buffer的編碼中,最高位成為了msb,只有后面的7位存儲實際的數(shù)據(jù),因此我們稱其為Base 128(2的7次方)。
????? 比如數(shù)字1,它本身只占用一個字節(jié)即可表示,所以它的msb沒有被設(shè)置,如:
??????0000 0001
?? ?? 再比如十進制數(shù)字300,它的編碼后表示形式為:
?? ???1010 1100 0000 0010
????? 對于Protocol Buffer而言又是如何將上面的字節(jié)布局還原成300呢?這里我們需要做的第一步是drop掉每個字節(jié)的msb。從上例中可以看出第一個字節(jié)(1010 1100)的msb(最高位)被設(shè)置為1,這說明后面的字節(jié)將連同該字節(jié)表示同一個數(shù)值,而第二個字節(jié)(0000 0010)的msb為0,因此該字節(jié)將為表示該數(shù)值的最后一個字節(jié)了,后面如果還有其他的字節(jié)數(shù)據(jù),將表示其他的數(shù)據(jù)。
????? 1010 1100 0000 0010
????? -> 010 1100 000 0010
????? 上例中的第二行已經(jīng)將第一行中每一個字節(jié)的msb去除。由于Protocol Buffer是按照Little Endian的方式進行數(shù)據(jù)布局的,因此我們這里需要將兩個字節(jié)的位置進行翻轉(zhuǎn)。
????? 010 1100 000 0010
????? -> 000 0010 010 1100??????? ???//翻轉(zhuǎn)第一行的兩個字節(jié)
????? -> 100101100?????????????????????????//將翻轉(zhuǎn)后的兩個字節(jié)直接連接并去除高位0
????? -> 256 + 32 + 8 + 4 = 300????//將上一行的二進制數(shù)據(jù)換算成十進制,其值為300
?? ?
消息結(jié)構(gòu)
?? ?? Protocol Buffer中的消息都是由一系列的鍵值對構(gòu)成的。每個消息的二進制版本都是使用標簽號作為key,而每一個字段的名字和類型均是在解碼的過程中根據(jù)目標類型(反序列化后的對象類型)進行配對的。在進行消息編碼時,key/value被連接成字節(jié)流。在解碼時,解析器可以直接跳過不識別的字段,這樣就可以保證新老版本消息定義在新老程序之間的兼容性,從而有效的避免了使用older消息格式的older程序在解析newer程序發(fā)來的newer消息時,一旦遇到未知(新添加的)字段時而引發(fā)的解析和對象初始化的錯誤。最后,我們介紹一下字段標號和字段類型是如何進行編碼的。下面先列出Protocol Buffer可以支持的字段類型。| 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 |
?? ?? 由于在編碼后每一個字段的key都是varint類型,key的值是由字段標號和字段類型合成編碼所得,其公式如下:
??????field_number << 3 | field_type
????? 由此看出,key的最后3個bits用于存儲字段的類型信息。那么在使用該編碼時,Protocol Buffer所支持的字段類型將不會超過8種。這里我們可以進一步計算出Protocol Buffer在一個消息中可以支持的字段數(shù)量為2的29次方減一。現(xiàn)在我們再來回顧一下之前給出的Test1消息被序列化后的第一個字節(jié)08的由來。
????? 0000 1000
?? ?? -> 000 1000??????????????????//drop掉msb(最高位)
????? 最低的3位表示字段類型,即0為varint。我們再將結(jié)果右移3位( >> 3),此時得到的結(jié)果為1,即字段a在消息Test1中的標簽號。通過這樣的結(jié)果,Protocol Buffer的解碼器可以獲悉當前字段的標簽號是1,其后所跟隨數(shù)據(jù)的類型為varint。現(xiàn)在我們可以繼續(xù)利用上面講到的知識分析出后兩個字節(jié)(96 01)的由來。
?? ?? 96 01 = 1001 0110 0000 0001
????????? -> 001 0110 000 0001???//drop兩個字節(jié)的msb
????????? -> 000 0001 001 0110???//翻轉(zhuǎn)高低字節(jié)
????????? -> 10010110???????????????????//去掉最高位中沒用的0
????????? -> 128 + 16 + 4 + 2 = 150
?? ?
更多的值類型
?? ?? 1. 有符號整型? ? ? 如前所述,類型0表示varint,其中包含int32/int64/uint32/uint64/sint32/sint64/bool/enum。在實際使用中,如果當前字段可以表示為負數(shù),那么對于int32/int64和sint32/sint64而言,它們在進行編碼時將存在著較大的差別。如果使用int32/int64表示一個負數(shù),該字段的值無論是-1還是-2147483648,其編碼后長度將始終為10個字節(jié),就如同對待一個很大的無符號整型一樣。反之,如果使用的是sint32/sint64,Protocol Buffer將會采用ZigZag編碼方式,其編碼后的結(jié)果將會更加高效。
????? 這里簡單講述一下ZigZag編碼,該編碼會將有符號整型映射為無符號整型,以便絕對值較小的負數(shù)仍然可以有較小的varint編碼值,如-1。下面是ZigZag對照表:
| Signed Original | Encoded As |
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| 2147483647 | 4294967294 |
| -2147483648 | 4294967295 |
?? ?? 其公式為:
? ? ? (n << 1) ^ (n >> 31)????//sint32
?? ?? (n << 1> ^ (n >> 63)???//sint64
?? ?? 需要補充說明的是,Protocol Buffer在實現(xiàn)上述位移操作時均采用的算術(shù)位移,因此對于(n >> 31)和(n >> 63)而言,如果n為負值位移后的結(jié)果就是-1,否則就是0。
?? ?? 注:簡單解釋一下C語言中的算術(shù)位移和邏輯位移。他們的左移操作都是相同的,即低位補0,高位直接移除。不同的是右移操作,邏輯位移比較簡單,高位全部補0。而算術(shù)位移則需要視當前值的符號位而定,補進的位和符號位相同,即正數(shù)全補0,負數(shù)全補1。換句話說,算術(shù)位移右移時要保證符號位的一致性。在C語言中,如果使用 int變量位移時就是算術(shù)位移,uint變量位移時是邏輯位移。
?? ?? 2. Non-varint數(shù)值型
?? ?? double/fixed64始終都占用8個字節(jié),float/fixed32始終占用4個字節(jié)。
? ? ? 3. Strings
? ? ? 其類型值為2,key信息之后是字節(jié)數(shù)組的長度信息,最后在緊隨指定長度的實際數(shù)據(jù)值信息。如:
?? ?? message Test2 {
?? ?? ??? required string b = 2;
?? ?? }
????? 現(xiàn)在我們設(shè)置b的值為"testing"。其編碼后數(shù)據(jù)如下:
??????12 07?74 65 73 74 69 6E 67
????? 第一個字節(jié)0x12表示key,通過解碼可以得到字段類型2和字段標號2。第二個字節(jié)07表示testing的長度。后面7個紅色高亮的字節(jié)則表示testing。
?? ?
嵌入消息
? ? ? 這里是一個包含嵌入消息的消息定義。? ? ? message Test3 {
??? ?? ?? required Test1 c = 3;
?? ?? }
?? ?? 此時我們先將Test1的a字段值設(shè)置為150,其編碼結(jié)果如下:
?? ???1A 03 08 96 01
? ? ? 從上面的結(jié)果可以看出08 96 01和之前直接編碼Test1時是完全一致的,只是在前面增加了key(字段類型 + 標號)和長度信息。新增信息的解碼方式和含義與前面的Strings完全相同,這里不再重復解釋了。
?? ?
Packed Repeated Fields
? ? ? Protocol Buffer從2.1.0版本開始引入了[pack = true]的字段級別選項。如果設(shè)置該選項,那么元素數(shù)量為0的repeated字段將不會被編碼,否則數(shù)組中的所有元素會被編碼成一個單一的key/value形式。畢竟數(shù)組中的每一個元素都具有相同的字段類型和標號。該編碼形式,對包含較小值的整型元素而言,優(yōu)化后的編碼結(jié)果可以節(jié)省更多的空間。如:? ? ? message Test4 {
?? ?? ??? repeated int32 d = 4 [pack=true];
? ? ? }
?? ?? 這里我們假設(shè)d字段包含3個元素,值分別為3,270,86942。編碼結(jié)果如下:
?? ?? 22?????????????//key (字段標號4,類型為2)
?? ?? 06?????????????//數(shù)據(jù)中所有元素所占用的字節(jié)數(shù)量
????? 03?????????????//第一個元素(varint 3)
?? ?? 8E 02????????//第二個元素(varint 270)
? ? ? 9E A7 05??//第三個元素(varint 86942)
?? ?
字段順序
? ? ? 在.proto文件中定義消息的字段標號時,可以是不連續(xù)的,但是如果將其定義為連續(xù)遞增的數(shù)值,將獲得更好的編碼和解碼性能
總結(jié)
以上是生活随笔為你收集整理的Protocol Buffer数据编码的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Protocol Buffer Java
- 下一篇: 怎样理解阻抗匹配?---非常好