二进制序列化
在計算機世界,萬物皆01二進制,包括各種各樣的文件格式和網絡協議,二進制格式最為常見!NewLife.Core 內置了完整的二進制序列化框架 Binary,經過十多年洗禮,發展到了第三代支持Handler處理器擴展。Binary的同類框架有 Protobuf、Thrift、MessagePack。
Nuget包:NewLife.Core
源碼地址:https://github.com/NewLifeX/X/tree/master/NewLife.Core/Serialization/Binary
主要特性
Binary主要功能特性:
體積極小。Binary是Schemaless架構,不包含字段名和序號,用最少的字節去保存數據
壓縮整數。大多數時候Int32字段保存的數字很小,采用七位壓縮編碼整數保存可以減少體積
格式簡單。盡管Binary只有.NET版,但其格式非常簡單,可以很容易在其它語言上實現
支持性很廣。Binary設計初衷,就是用于實現各種已知文件格式和通信協議,例如ZipFile
支持動態特性。可根據某些字段值,生成不同消息類型,例如MQTT和DNS協議
可讀性較差。二進制格式且沒有Schema,可讀性較差
無版本支持。需要讀寫雙方約定好多版本格式的兼容
Binary設計理念,就是用最小的體積去保存數據,且能夠靈活實現各種文件格式和通信協議的序列化。
直接序列化對象,在沒有使用額外壓縮算法的條件下,Binary幾乎是結果體積最小的序列化框架。
快速用法
想要序列化一個對象,或者反序列化一個數據流到對象,最直接的想法就是這樣
// 快速讀取 public static T FastRead<T>(Stream stream, Boolean encodeInt = true); // 快速寫入 public static Packet FastWrite(Object value, Boolean encodeInt = true); public static void FastWrite(Object value, Stream stream, Boolean encodeInt = true);Binary.FastWrite 可以直接把一個對象序列化為數據包Packet,可以理解為字節數組Byte[]的包裝。
Binary.FastRead 從數據流中反序列化得到目標類型的對象,這里必須指定目標類型,否則Binary不知道應該如何解析。
例子
[Fact] public void Fast() {var model = new MyModel { Code = 1234, Name = "Stone" };var pk = Binary.FastWrite(model);Assert.Equal(8, pk.Total);Assert.Equal("D2090553746F6E65", pk.ToHex());Assert.Equal("0gkFU3RvbmU=", pk.ToArray().ToBase64());var model2 = Binary.FastRead<MyModel>(pk.GetStream());Assert.Equal(model.Code, model2.Code);Assert.Equal(model.Name, model2.Name);var ms = new MemoryStream();Binary.FastWrite(model, ms);Assert.Equal("D2090553746F6E65", ms.ToArray().ToHex()); } private class MyModel {public Int32 Code { get; set; }public String Name { get; set; } }序列化帶有一個整型和一個字符串的對象,結果只有8個字節!
Packet用法可參考
此處為語雀文檔,點擊鏈接查看:https://www.yuque.com/go/doc/31527106
標準讀寫
Binary主要成員
/// <summary>使用7位編碼整數。默認false不使用</summary> public Boolean EncodeInt { get; set; } /// <summary>小端字節序。默認false大端</summary> public Boolean IsLittleEndian { get; set; } /// <summary>使用指定大小的FieldSizeAttribute特性,默認false</summary> public Boolean UseFieldSize { get; set; } /// <summary>使用對象引用,默認true</summary> public Boolean UseRef { get; set; } = true; /// <summary>大小寬度。可選0/1/2/4,默認0表示壓縮編碼整數</summary> public Int32 SizeWidth { get; set; } /// <summary>要忽略的成員</summary> public ICollection<String> IgnoreMembers { get; set; } /// <summary>處理器列表</summary> public IList<IBinaryHandler> Handlers { get; private set; } /// <summary>數據流。默認實例化一個內存數據流</summary> public virtual Stream Stream { get; set; } /// <summary>主對象</summary> public Stack<Object> Hosts { get; private set; } /// <summary>成員</summary> public MemberInfo Member { get; set; } /// <summary>字符串編碼,默認utf-8</summary> public Encoding Encoding { get; set; } /// <summary>序列化屬性而不是字段。默認true</summary> public Boolean UseProperty { get; set; } // 處理器 public Binary AddHandler(IBinaryHandler handler); public Binary AddHandler<THandler>(Int32 priority = 0); public T GetHandler<T>(); // 寫入 public virtual Boolean Write(Object value, Type type = null); // 讀取 public virtual Object Read(Type type); public T Read<T>(); public virtual Boolean TryRead(Type type, ref Object value);Stream 最為重要,代表序列化和反序列化的數據流,默認實例化一個內存流。
EncodeInt 指定使用壓縮編碼整數,效果非常明顯!
IsLittleEndian 部分協議使用大端字節序。
UseFieldSize 部分協議的長度位和數據區并沒有挨在一起,需要借助FieldSizeAttribute特性。例如ZipEntry中有這么一段:
/// <summary>文件名長度</summary> private readonly UInt16 FileNameLength; /// <summary>擴展數據長度</summary> private readonly UInt16 ExtraFieldLength; // ZipDirEntry成員 /// <summary>注釋長度</summary> private readonly UInt16 CommentLength; // ZipDirEntry成員 /// <summary>分卷號。</summary> public UInt16 DiskNumber; // ZipDirEntry成員 /// <summary>內部文件屬性</summary> public UInt16 InternalFileAttrs; // ZipDirEntry成員 /// <summary>擴展文件屬性</summary> public UInt32 ExternalFileAttrs; // ZipDirEntry成員 /// <summary>文件頭相對位移</summary> public UInt32 RelativeOffsetOfLocalHeader; /// <summary>文件名,如果是目錄,則以/結束</summary> [FieldSize("FileNameLength")] public String FileName; /// <summary>擴展字段</summary> [FieldSize("ExtraFieldLength")] public Byte[] ExtraField; // ZipDirEntry成員 /// <summary>注釋</summary> [FieldSize("CommentLength")] public String Comment;IgnoreMembers 指定某些成員不參與序列化,支持動態指定。例如ZipFile的目錄實體和文件實體,需要序列化的字段有所不同。
Encoding 指定序列化字符串時使用的文本編碼。
設置好各種參數后,就可以Write/Read來序列化或反序列化對象了。安全起見,建議每個Binary只用一次,重復使用可能有意想不到的后果。
自定義擴展
Binary設計時使用Handler處理器架構,Write/Read內部實際上是逐個遍歷Handler,直到找到能夠處理的Handler為止。因此Handler也有優先級,其中基礎數據類型BinaryGeneral處理器優先級最高。
BinaryGeneral 負責處理數字、布爾、時間日期、字符串等等基礎數據類型。
BinaryNormal 負責處理字節數組、Guid、Packet等常見類型。
BinaryList 負責處理數組和列表。
BinaryDictionary 負責處理字典。
BinaryComposite 負責處理復雜對象,反射各成員,遞歸序列化。該處理器優先級最低。
來看看怎么樣自定義一個處理器,以顏色處理器為例:
/// <summary>顏色處理器。</summary> public class BinaryColor : BinaryHandlerBase {/// <summary>實例化</summary>public BinaryColor(){Priority = 0x50;}/// <summary>寫入對象</summary>/// <param name="value">目標對象</param>/// <param name="type">類型</param>/// <returns></returns>public override Boolean Write(Object value, Type type){if (type != typeof(Color)) return false;var color = (Color)value;WriteLog("WriteColor {0}", color);Host.Write(color.A);Host.Write(color.R);Host.Write(color.G);Host.Write(color.B);return true;}/// <summary>嘗試讀取指定類型對象</summary>/// <param name="type"></param>/// <param name="value"></param>/// <returns></returns>public override Boolean TryRead(Type type, ref Object value){if (type != typeof(Color)) return false;var a = Host.ReadByte();var r = Host.ReadByte();var g = Host.ReadByte();var b = Host.ReadByte();var color = Color.FromArgb(a, r, g, b);WriteLog("ReadColor {0}", color);value = color;return true;} }最后只需要掛載到Binary上即可序列化和反序列化帶有Color類型的成員
var bn = new Binary(); bn.AddHandler<BinaryColor>();總結
.NET內部自帶二進制序列化BinaryFormatter,它會帶上大量額外信息,導致體積很大,基本上很少用到。
Binary設計的初衷是序列化各種文件格式和通信協議,因此并沒有過多考慮作為RPC通信格式。實際上NewLife組件自己的RPC框架ApiServer并沒有使用Binary,而是選擇了兼容性比較好的Json。
在中通的100億Redis大數據中,盡管是二進制kv數據,同樣沒有用到Binary。因為它需要對字節數據進行極致控制,并且需要做多版本兼容。因此它實際上是直接讀寫二進制數據流,然后借用了Binary的一些輔助方法。
總結
- 上一篇: 谷歌微软高通反对英伟达收购ARM 值得国
- 下一篇: 让 gRPC 提供 REST 服务