C#中protobuf-net的編碼結構及使用方法
protobuf-net簡介
Protocol Buffer(簡稱Protobuf) 是 Google 公司內部提供的數據序列化和反序列化標準,與 JSON 和 XML 格式類似,同樣大小的對象,相比 XML 和 JSON 格式, Protobuf 序列化後所佔用的空間最小。
Protocol Buffers 是一種輕便高效的結構化數據存儲格式,可用於通訊協議、數據存儲等領域的語言無關、平台無關、可擴展的序列化結構數據格式。
protobuf-net是用於.NET程式碼的基於契約的序列化程式,它以Google設計的「protocol buffers」序列化格式寫入數據,適用於大多數編寫標準類型並可以使用屬性的.NET語言。
protobuf-net可通過NuGet安裝程式包,也可直接訪問github下載源碼://github.com/protobuf-net/protobuf-net 。
ProtoBuf編碼原理
這裡只是簡單介紹一下ProtoBuf的編碼結構,然後通過一個簡單的序列化示例熟悉ProtoBuf的大致編碼過程,具體編碼規則參考ProtoBuf官網://developers.google.cn/protocol-buffers
編碼結構
TLV (Tag – Length – Value)格式:Tag 作為該欄位的唯一標識,Length 代表 Value 數據域的長度,最後的 Value 便是數據本身。
ProtoBuf 編碼採用類似TLV的結構,其編碼結構可見下圖:
註:其中的 Start group 和 End group 兩種類型已被遺棄。
一個 message 編碼將由一個個的 field 組成,每個 field 根據類型將有如下兩種格式:
- Tag – Length – Value:編碼類型表中 Type = 2 即 Length-delimited 編碼類型將使用這種結構,
- Tag – Value:編碼類型表中 Varint、64-bit、32-bit 使用這種結構。
Tag 由欄位編號 field_number 和 編碼類型 wire_type 組成,Tag 整體採用 Varints 編碼,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 | vfixed32, sfixed32, float |
Varints 編碼:在每個位元組開頭的 bit 設置了 msb(most significant bit ),標識是否需要繼續讀取下一個位元組,存儲數字對應的二進位補碼,補碼的低位排在前面,類似小端模式。
ZigZag 編碼:有符號整數映射到無符號整數,然後再使用 Varints 編碼,sint32、sint64 將採用 ZigZag 編碼(編碼結構依然為 Tag – Value)。
解析一個編碼結果
準備一個Person類(來自github示例):
[ProtoContract]
class Person
{
[ProtoMember(1)]
public int Id { get; set; }
[ProtoMember(2)]
public string Name { get; set; }
[ProtoMember(3)]
public Address Address { get; set; }
}
[ProtoContract]
class Address
{
[ProtoMember(1)]
public string Line1 { get; set; }
[ProtoMember(2)]
public string Line2 { get; set; }
}
實例化並賦值:
var person = new Person
{
Id = 12345,
Name = "Fred",
Address = new Address
{
Line1 = "Flat 1",
Line2 = "The Meadows"
}
};
序列化後的結果:
//十六進位
08-B9-60-12-04-
46-72-65-64-1A-
15-0A-06-46-6C-
61-74-20-31-12-
0B-54-68-65-20-
4D-65-61-64-6F-
77-73
//二進位
00001000-10111001-01100000-00010010-00000100-
01000110-01110010-01100101-01100100-00011010-
00010101-00001010-00000110-01000110-01101100-
01100001-01110100-00100000-00110001-00010010-
00001011-01010100-01101000-01100101-00100000-
01001101-01100101-01100001-01100100-01101111-
01110111-01110011
- 第1個位元組 00001000 :表示filed_name=1,write_type=0,既Id欄位的Tag;
- 第2個位元組 10111001 :Id欄位的Value,高位1表示繼續讀取下一位元組;
- 第3個位元組 01100000 :Id欄位的Value的高位,高位0表示不繼續讀取下一位元組,組合後的值為1100000 0111001(Varints 編碼),十進位值為12345;
- 第4個位元組 00010010 :表示filed_name=2,write_type=2(需顯式告知長度),既Name欄位的Tag;
- 第5個位元組 00000100 :Name欄位的Length,高位0表示不繼續讀取下一位元組,長度為4;
- 第6-9個位元組 46-72-65-64 :Name欄位的Value,”Fred”的ASCII碼;
- 第10個位元組 00011010 :表示filed_name=3,write_type=2,既Address欄位的Tag;
- 第11個位元組 00010101 :Address欄位的Length,高位0表示不繼續讀取下一位元組,長度為21;
- 第12個位元組 00001010 :表示filed_name=1,write_type=2,既Address的Line1欄位的Tag;
- 第13個位元組 00000110 :Address的Line1欄位的Length,高位0表示不繼續讀取下一位元組,長度為6;
- 第14-19個位元組 46-6C-61-74-20-31 :Address的Line1欄位的Value,”Flat 1″的ASCII碼;
- 第20個位元組 00010010 : 表示filed_name=2,write_type=2,既Address的Line2欄位的Tag;
- 第21個位元組 00001011 :Address的Line2欄位的Length,高位0表示不繼續讀取下一位元組,長度為11;
- 第22-32個位元組 54-68-65-20-4D-65-61-64-6F-77-73 :Address的Line2欄位的Value,”The Meadows”的ASCII碼。
使用方法
下面是一個ProtoBuf-Net的擴展方法類,提供了字元串、位元組數組、二進位文件與對象實例之間的互相轉換方法,程式碼如下:
using System;
using System.IO;
/*
* 部落格園首發 //www.cnblogs.com/timefiles/
* 創建時間:2021-04-10
*/
/// <summary>
/// ProtoBuf-Net擴展方法類
/// </summary>
public static class ProtoBufExtension
{
/// <summary>
/// 將對象實例序列化為字元串(Base64編碼格式)——ProtoBuf
/// </summary>
/// <typeparam name="T">對象類型</typeparam>
/// <param name="obj">對象實例</param>
/// <returns>字元串(Base64編碼格式)</returns>
public static string SerializeToString_PB<T>(this T obj)
{
using (MemoryStream ms = new MemoryStream())
{
ProtoBuf.Serializer.Serialize(ms, obj);
return Convert.ToBase64String(ms.GetBuffer(), 0, (int)ms.Length);
}
}
/// <summary>
/// 將字元串(Base64編碼格式)反序列化為對象實例——ProtoBuf
/// </summary>
/// <typeparam name="T">對象類型</typeparam>
/// <param name="txt">字元串(Base64編碼格式)</param>
/// <returns>對象實例</returns>
public static T DeserializeFromString_PB<T>(this string txt)
{
byte[] arr = Convert.FromBase64String(txt);
using (MemoryStream ms = new MemoryStream(arr))
return ProtoBuf.Serializer.Deserialize<T>(ms);
}
/// <summary>
/// 將對象實例序列化為位元組數組——ProtoBuf
/// </summary>
/// <typeparam name="T">對象類型</typeparam>
/// <param name="obj">對象實例</param>
/// <returns>位元組數組</returns>
public static byte[] SerializeToByteAry_PB<T>(this T obj)
{
using (MemoryStream ms = new MemoryStream())
{
ProtoBuf.Serializer.Serialize(ms, obj);
return ms.ToArray();
}
}
/// <summary>
/// 將位元組數組反序列化為對象實例——ProtoBuf
/// </summary>
/// <typeparam name="T">對象類型</typeparam>
/// <param name="arr">位元組數組</param>
/// <returns></returns>
public static T DeserializeFromByteAry_PB<T>(this byte[] arr)
{
using (MemoryStream ms = new MemoryStream(arr))
return ProtoBuf.Serializer.Deserialize<T>(ms);
}
/// <summary>
/// 將對象實例序列化為二進位文件——ProtoBuf
/// </summary>
/// <typeparam name="T">對象類型</typeparam>
/// <param name="obj">對象實例</param>
/// <param name="path">文件路徑(目錄+文件名)</param>
public static void SerializeToFile_PB<T>(this T obj, string path)
{
using (var file = File.Create(path))
{
ProtoBuf.Serializer.Serialize(file, obj);
}
}
/// <summary>
/// 將二進位文件反序列化為對象實例——ProtoBuf
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
public static T DeserializeFromFile_PB<T>(this string path)
{
using (var file = File.OpenRead(path))
{
return ProtoBuf.Serializer.Deserialize<T>(file);
}
}
}
使用方法如下:
static void Main(string[] args)
{
var person = new Person
{
Id = 12345,
Name = "Fred",
Address = new Address
{
Line1 = "Flat 1",
Line2 = "The Meadows"
}
};
string str = person.SerializeToString_PB();
var strPerson = str.DeserializeFromString_PB<Person>();
Console.WriteLine("序列化結果(字元串):" + str);
var arr = person.SerializeToByteAry_PB();
var arrPerson = arr.DeserializeFromByteAry_PB<Person>();
Console.WriteLine("序列化結果(位元組數組):" + BitConverter.ToString(arr));
string path = "person.bin";
person.SerializeToFile_PB(path);
var pathPerson = path.DeserializeFromFile_PB<Person>();
Console.WriteLine("序列化結果(二進位文件):" + BitConverter.ToString(File.ReadAllBytes(path)));
Console.ReadLine();
}
結果如下:
序列化結果(字元串):CLlgEgRGcmVkGhUKBkZsYXQgMRILVGhlIE1lYWRvd3M=
序列化結果(位元組數組):08-B9-60-12-04-46-72-65-64-1A-15-0A-06-46-6C-61-74-20-31-12-0B-54-68-65-20-4D-65-61-64-6F-77-73
序列化結果(二進位文件):08-B9-60-12-04-46-72-65-64-1A-15-0A-06-46-6C-61-74-20-31-12-0B-54-68-65-20-4D-65-61-64-6F-77-73
參考資料
- ProtoBuf官網
- 【Protobuf】Protobuf的編解碼規則詳解——CSDN
- [翻譯] ProtoBuf 官方文檔(五)- 編碼——簡書
- 深入 ProtoBuf – 編碼——簡書
- protobuf-net源碼及dll備份-20210410 提取碼: sv1p