Protocol
buffers是google使用的一種結(jié)構(gòu)化數(shù)據(jù)序列化編碼解碼方式,采用簡單的二進(jìn)制格式,他比XML、JSON格式體積更小,編碼解碼效率更高
下面是項(xiàng)目官方網(wǎng)站與XML對比的描述:
# are 3 to 10 times smaller
# are 20 to 100 times faster
這里有一個(gè).NET環(huán)境下的對比測試:Results of Northwind database rows serialization
benchmarks,用的是.NET下面的實(shí)現(xiàn)ProtoBuf.net
protobuf項(xiàng)目(C++),.NET
下的實(shí)現(xiàn)有:protobuf-net、protobuf-csharp-port。
另外一個(gè).NET的項(xiàng)目是Proto#,不過作者似乎沒有維護(hù)了
使用方式簡介
首先定義消息類型:
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
Field Rules: 屬性規(guī)則,required: 必須的屬性;optional: 可選屬性;repeated: 可重復(fù)多個(gè)的屬性
Field Type: 屬性數(shù)據(jù)類型,標(biāo)量值類型(scalar value types)支持double, float, int32,
int64, uint32, uint64, sint32, sint64, fixed32, fixed64, bool, string,
bytes等,另外支持枚舉、嵌套/引用的消息類型等
Field Tags: 屬性標(biāo)簽(例如name=1中的1),使用正整數(shù)表示,在序列化的二進(jìn)制中使用這個(gè)標(biāo)簽來標(biāo)記屬性,比使用屬性名稱體積更小
詳細(xì)的語法參考官方網(wǎng)站:Language
Guide
消息類型定義在.proto文件中,使用protoc.exe根據(jù).proto文件生成C++、Java、Python等類文件,這些類文件中定義了表示
消息的對象,以及用于編碼、解碼的方法
體積方面,首先從上面消息類型的定義中可以看出,使用屬性標(biāo)簽代替屬性名稱可以減小體積,另外在編碼協(xié)議上對各種數(shù)據(jù)類型的處理,也盡量采用了壓縮的表示
方式以減小體積。速度方面,二進(jìn)制協(xié)議比基于文本的解析更有優(yōu)勢
編碼協(xié)議簡介 - 2.3.0
詳細(xì)的編碼協(xié)議參考官方網(wǎng)站的Encoding
Base 128 Varints
32位整數(shù)使用4字節(jié)存儲,32位的整數(shù)值1同樣要使用4個(gè)字節(jié),比較浪費(fèi)空間。Varint采用變長字節(jié)的方式存儲整數(shù),將高位為0的字節(jié)去掉,節(jié)約空
間
高位為0的字節(jié)去掉以后,用來存儲整數(shù)的每一個(gè)字節(jié),其最高有效位(most significant
bit)用作標(biāo)識位,0表示這是整數(shù)的最后一個(gè)字節(jié),1表示不是最后一個(gè)字節(jié);其他7位用于存儲整數(shù)的數(shù)值。字節(jié)序采用little-endian
示例:
整數(shù)1,Varint的二進(jìn)制值為0000 0001。因?yàn)?個(gè)字節(jié)就足夠,所以最高有效位為0,后7位則為1的原碼形式
整數(shù)300,Varint需要2字節(jié)表示,二進(jìn)制值為1010 1100 0000
0010。第一個(gè)字節(jié)最高有效位設(shè)為1,最后一個(gè)字節(jié)最高有效位設(shè)為0。解碼過程如下:
a). 首先每個(gè)字節(jié)去掉最高有效位,得到:010 1100 000 0010
b). 按照little-endian方式處理字節(jié)序,得到:000 0010 010 1100
c). 二進(jìn)制值100101100即為300
ZigZag編碼
Varint對于無符號整數(shù)有效,對負(fù)數(shù)無法進(jìn)行壓縮,protocol buffer對有符號整數(shù)采用ZigZag編碼后,再以varint形式存儲
對32位有符號數(shù),ZigZag編碼算法為 (n << 1) ^ (n >> 31),對64位有符號數(shù)的算法為(n
<< 1) ^ (n >> 63)
注意:32位有符號數(shù)右移31位后,對于正數(shù)所有位為0,對于負(fù)數(shù)所有位為1
編碼后的效果是0=>0, -1=>1, 1=>2, -2=>3,
2=>4……,即將無符號數(shù)編碼為有符號數(shù)表示,這樣就能有效發(fā)揮varint的優(yōu)勢了
Protocol buffer用32位表示float和fixed32,用64位表示double和fixed64
String, bytes,
嵌入式消息等數(shù)據(jù)均采用定長數(shù)據(jù)類型(length-delimited)表示,這類數(shù)據(jù)在開始位置使用一個(gè)varint表示數(shù)據(jù)的字節(jié)長度,后面接著是
數(shù)據(jù)值
消息結(jié)構(gòu)
消息的所有屬性都序列化為key-value
pair(鍵-值對)的字節(jié)流形式,字節(jié)流中不包含屬性的名稱和聲明的類型,這些信息必須從定義的消息類型中獲取
key里面包含2個(gè)東西,一個(gè)是在消息類型里面為該屬性指定的field tag,另一個(gè)是protocol buffer協(xié)議的封裝類型(wire
type)。這2個(gè)部分都是正整數(shù),使用 (field_tag << 3) | wire_type
方式生成一個(gè)正整數(shù),然后使用base 128 varint方式表示。key后面跟著是屬性的值
wire type:
|
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 Test1 {
required int32 attr = 1;
}
創(chuàng)建一個(gè)Test1的對象,將其屬性attr的值設(shè)置為150,則對該對象編碼過程如下
屬性數(shù)據(jù)類型為int32,其wire type為0,所以key值為
(1 << 3 ) | 0 => 0000 1000
屬性值150采用Varint編碼
150
=> 10010110 //二進(jìn)制
=> 000 0001 001 0110 //7位一組分開
=> 001 0110 000 0001 //little-endian字節(jié)序
=> 1001 0110 0000 0001 //設(shè)置最高標(biāo)識位
=> 96 01 //16進(jìn)制
所以這個(gè)Test1對象編碼后的16進(jìn)制值為:08 96 01
如果有嵌入式消息類型定義如下
message Test3 {
required Test1 c = 3;
}
編碼后的16進(jìn)制值形如:1A 03 08 96 01,其中08 96
01就是上面示例的Test1對象,在Test3的屬性中他與字符串的處理方式一樣,前面的03就是表示其長度的varint
protobuf-
csharp-port的使用方式
protobuf-csharp-port跟protobuf的使用方式一樣,即在開發(fā)過程中使用protoc.exe、ProtoGen.exe生成用
于序列化、反序列化時(shí)的消息對象,在運(yùn)行時(shí)通過這些對象進(jìn)行編碼解碼
從GitHub下
載項(xiàng)目源代碼(目前還沒有發(fā)布包),項(xiàng)目中帶有示例AddressBook
生成消息通訊用的C#類分2個(gè)步驟
步驟1:使用lib目錄下的protoc.exe生成二進(jìn)制表示
protoc --descriptor_set_out=addressbook.protobin --proto_path=..\protos
--include_imports ..\protos\tutorial\addressbook.proto
步驟2:使用編譯生成的ProtoGen.exe從二進(jìn)制表示生成C#類
ProtoGen.exe addressbook.protobin
會生成幾個(gè).cs文件,其中包括AddressBookProtos.cs,這個(gè)就是在addressbook.proto中定義的消息類型
運(yùn)行時(shí)的項(xiàng)目需要引用編譯生成的Google.ProtocolBuffers.dll,使用AddressBookProtos.cs完成編碼解碼操
作,詳細(xì)用法查看示例項(xiàng)目AddressBook
運(yùn)行AddressBook.exe如下圖:

輸入的對象序列化為二進(jìn)制后,默認(rèn)保存在addressbook.data文件中,可以使用ProtoDump.exe讀取這個(gè)二進(jìn)制文件:

protobuf-net的使用方式 - r282
protobuf-net的使用與Google的protobuf完全不一樣,他采用.NET的編程方式,可以非常方便的在.NET的序列化場景下使用,
支持WCF的DataContact,WCF程序幾乎不需要什么修改就能使用protobuf-net
下載protobuf-net,項(xiàng)目引用protobuf-net.dll,測試對象定義如下:
02 |
public class TestObject |
05 |
public string StringAttr1 { get; set; } |
07 |
public string StringAttr2 { get; set; } |
09 |
public int IntAttr { get; set; } |
11 |
public long LongAttr { get; set; } |
13 |
public decimal DecimalAttr { get; set; } |
15 |
public float FloatAttr { get; set; } |
17 |
public int[] ArrayAttr { get; set; } |
19 |
public IList<string>
ListAttr { get; set; } |
21 |
public InnerObject
EmbeddedAttr { get; set; } |
22 |
public override string ToString() |
24 |
StringBuilder
sb = new StringBuilder() |
25 |
.Append("TestObject
{\r\n") |
26 |
.Append("
StringAttr1: \"").Append(this.StringAttr1).Append("\",\r\n") |
27 |
.Append("
StringAttr2: \"").Append(this.StringAttr2).Append("\",\r\n") |
28 |
.Append("
IntAttr: ").Append(this.IntAttr).Append(",\r\n") |
29 |
.Append("
LongAttr: ").Append(this.LongAttr).Append(",\r\n") |
30 |
.Append("
DecimalAttr: ").Append(this.DecimalAttr).Append(",\r\n") |
31 |
.Append("
FloatAttr: ").Append(this.FloatAttr).Append(",\r\n"); |
32 |
if (this.ArrayAttr
!= null) |
34 |
sb.Append("
ArrayAttr: [ "); |
35 |
foreach (int i in this.ArrayAttr) sb.Append(i).Append(", "); |
36 |
sb.Remove(sb.Length - 2, 2); |
39 |
if (this.ListAttr != null) |
41 |
sb.Append(" ListAttr: [ "); |
42 |
foreach
(string
s in
this.ListAttr)
sb.Append('"').Append(s).Append("\", "); |
43 |
sb.Remove(sb.Length - 2, 2); |
46 |
if (this.EmbeddedAttr != null) |
47 |
sb.Append("
EmbeddedAttr: ").Append(this.EmbeddedAttr.ToString()).Append("\r\n"); |
48 |
return sb.Append("}").ToString(); |
52 |
public class InnerObject |
55 |
public string Attr1 { get; set; } |
57 |
public DateTime Attr2 { get; set; } |
59 |
public bool Attr3 { get; set; } |
61 |
public byte Attr4 { get; set; } |
63 |
public sbyte Attr5 { get; set; } |
64 |
public override string ToString() |
66 |
return
new StringBuilder() |
68 |
.Append("
Attr1: \"").Append(this.Attr1).Append("\",\r\n") |
69 |
.Append("
Attr2: \"").Append(this.Attr2.ToString("yyyy-MM-dd")).Append("\",\r\n") |
70 |
.Append("
Attr3: ").Append(this.Attr3).Append(",\r\n") |
71 |
.Append("
Attr4: ").Append(this.Attr4).Append(",\r\n") |
72 |
.Append("
Attr5: ").Append(this.Attr5).Append("\r\n") |
73 |
.Append(" }").ToString(); |
測試代碼如下:
01 |
using (MemoryStream
ms = new MemoryStream()) |
03 |
TestObject obj = new TestObject() |
05 |
StringAttr1 = "string 1", |
06 |
StringAttr2 = "string 2", |
09 |
DecimalAttr = 34.10091M, |
11 |
ArrayAttr = new int[] { 600,
-9, 0 }, |
12 |
ListAttr = new List<string> { "string 3", "string 5" }, |
13 |
EmbeddedAttr = new InnerObject() |
16 |
Attr2 = new
DateTime(2010, 2, 1), |
22 |
Serializer.Serialize<TestObject>(ms, obj); |
25 |
TestObject obj2 = Serializer.Deserialize<TestObject>(ms); |
26 |
Console.WriteLine(obj2); |
運(yùn)行結(jié)果:

附錄
原碼、反碼、補(bǔ)碼
對有符號數(shù),最高位是符號位。正數(shù)的原碼反碼和補(bǔ)碼都是一樣的,就是本身。負(fù)數(shù)的反碼是原碼求反,補(bǔ)碼是反碼加1。例如-1的原碼是1000
0001,反碼是1111 1110,補(bǔ)碼是1111
1111。負(fù)數(shù)都是用補(bǔ)碼表示,從正數(shù)的原碼推負(fù)數(shù)的二進(jìn)制表示(補(bǔ)碼)時(shí),只須將正數(shù)各個(gè)位(包括符合位)取反加1
補(bǔ)碼有2種,即one's complement (1's complement,1的補(bǔ)碼) 和 two's complement (2's
complement,2的補(bǔ)碼) 。按照定義,one's complement就是對各個(gè)位取反,two's
complement是對各個(gè)位取反后加1。例如在8位處理器情況下,9的二進(jìn)制是0000 1001,one's complement是1111
0110,two's complement是1111 0111
采用one's complement表示負(fù)數(shù)時(shí)存在正0 (0x00)和負(fù)0 (0xff),并且有符號數(shù)相加必須采用end-around
carry(循環(huán)進(jìn)位)處理,例如

相加之后發(fā)生溢出,則必須將溢出位加到最低位上,這樣導(dǎo)致有符號數(shù)相加和無符號數(shù)相加算法不一致,而采用two's
complement表示時(shí)不存在這些問題
關(guān)于2的補(bǔ)碼表示可以參考阮一峰的關(guān)于2的補(bǔ)
碼一文,更專業(yè)的說明可以參考wikipedia上的Method of
complements:二進(jìn)制的基數(shù)補(bǔ)碼(radix complement)叫做2的補(bǔ)碼,二進(jìn)制的基數(shù)減一補(bǔ)碼(diminished
radix complement)叫做1的補(bǔ)碼;十進(jìn)制的基數(shù)補(bǔ)碼叫做10的補(bǔ)碼,基數(shù)減一補(bǔ)碼叫做9的補(bǔ)碼
Big-endian, little-endian可以參考wikipedia上的Endianness,
講解的很詳細(xì)
Tag標(biāo)簽: Serialization
|