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

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

protobuf-net的使用方式 - r282
protobuf-net的使用與Google的protobuf完全不一樣,他采用.NET的編程方式,可以非常方便的在.NET的序列化場景下使用,
支持WCF的DataContact,WCF程序幾乎不需要什么修改就能使用protobuf-net
下載protobuf-net,項目引用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); |
運行結果:

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

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