首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

二进制序列化

在计算机世界,万物皆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几乎是结果体积最小的序列化框架。

快速用法

想要序列化一个对象,或者反序列化一个数据流到对象,最直接的想法就是这样

// 快速读取

publicstaticTFastRead(Streamstream,BooleanencodeInt=true);

// 快速写入

publicstaticPacketFastWrite(Objectvalue,BooleanencodeInt=true);

publicstaticvoidFastWrite(Objectvalue,Streamstream,BooleanencodeInt=true);

Binary.FastWrite 可以直接把一个对象序列化为数据包Packet,可以理解为字节数组Byte[]的包装。

Binary.FastRead 从数据流中反序列化得到目标类型的对象,这里必须指定目标类型,否则Binary不知道应该如何解析。

例子

[Fact]

publicvoidFast()

{

varmodel=newMyModel{Code=1234,Name="Stone"};

varpk=Binary.FastWrite(model);

Assert.Equal(8,pk.Total);

Assert.Equal("D2090553746F6E65",pk.ToHex());

Assert.Equal("0gkFU3RvbmU=",pk.ToArray().ToBase64());

varmodel2=Binary.FastRead(pk.GetStream());

Assert.Equal(model.Code,model2.Code);

Assert.Equal(model.Name,model2.Name);

varms=newMemoryStream();

Binary.FastWrite(model,ms);

Assert.Equal("D2090553746F6E65",ms.ToArray().ToHex());

}

privateclassMyModel

{

publicInt32Code{get;set; }

publicStringName{get;set; }

}

序列化带有一个整型和一个字符串的对象,结果只有8个字节!

Packet用法可参考

标准读写

Binary主要成员

/// 使用7位编码整数。默认false不使用

publicBooleanEncodeInt{get;set; }

/// 小端字节序。默认false大端

publicBooleanIsLittleEndian{get;set; }

/// 使用指定大小的FieldSizeAttribute特性,默认false

publicBooleanUseFieldSize{get;set; }

/// 使用对象引用,默认true

publicBooleanUseRef{get;set; }=true;

/// 大小宽度。可选0/1/2/4,默认0表示压缩编码整数

publicInt32SizeWidth{get;set; }

/// 要忽略的成员

publicICollectionIgnoreMembers{get;set; }

/// 处理器列表

publicIListHandlers{get;privateset; }

/// 数据流。默认实例化一个内存数据流

publicvirtualStreamStream{get;set; }

/// 主对象

publicStackHosts{get;privateset; }

/// 成员

publicMemberInfoMember{get;set; }

/// 字符串编码,默认utf-8

publicEncodingEncoding{get;set; }

/// 序列化属性而不是字段。默认true

publicBooleanUseProperty{get;set; }

// 处理器

publicBinaryAddHandler(IBinaryHandlerhandler);

publicBinaryAddHandler(Int32priority=);

publicTGetHandler();

// 写入

publicvirtualBooleanWrite(Objectvalue,Typetype=null);

// 读取

publicvirtualObjectRead(Typetype);

publicTRead();

publicvirtualBooleanTryRead(Typetype,refObjectvalue);

Stream 最为重要,代表序列化和反序列化的数据流,默认实例化一个内存流。

EncodeInt 指定使用压缩编码整数,效果非常明显!

IsLittleEndian 部分协议使用大端字节序。

UseFieldSize 部分协议的长度位和数据区并没有挨在一起,需要借助FieldSizeAttribute特性。例如ZipEntry中有这么一段:

/// 文件名长度

privatereadonlyUInt16FileNameLength;

/// 扩展数据长度

privatereadonlyUInt16ExtraFieldLength;

// ZipDirEntry成员

/// 注释长度

privatereadonlyUInt16CommentLength;

// ZipDirEntry成员

/// 分卷号。

publicUInt16DiskNumber;

// ZipDirEntry成员

/// 内部文件属性

publicUInt16InternalFileAttrs;

// ZipDirEntry成员

/// 扩展文件属性

publicUInt32ExternalFileAttrs;

// ZipDirEntry成员

/// 文件头相对位移

publicUInt32RelativeOffsetOfLocalHeader;

/// 文件名,如果是目录,则以/结束

[FieldSize("FileNameLength")]

publicStringFileName;

/// 扩展字段

[FieldSize("ExtraFieldLength")]

publicByte[]ExtraField;

// ZipDirEntry成员

/// 注释

[FieldSize("CommentLength")]

publicStringComment;

IgnoreMembers 指定某些成员不参与序列化,支持动态指定。例如ZipFile的目录实体和文件实体,需要序列化的字段有所不同。

Encoding 指定序列化字符串时使用的文本编码。

设置好各种参数后,就可以Write/Read来序列化或反序列化对象了。安全起见,建议每个Binary只用一次,重复使用可能有意想不到的后果。

自定义扩展

Binary设计时使用Handler处理器架构,Write/Read内部实际上是逐个遍历Handler,直到找到能够处理的Handler为止。因此Handler也有优先级,其中基础数据类型BinaryGeneral处理器优先级最高。

BinaryGeneral 负责处理数字、布尔、时间日期、字符串等等基础数据类型。

BinaryNormal 负责处理字节数组、Guid、Packet等常见类型。

BinaryList 负责处理数组和列表。

BinaryDictionary 负责处理字典。

BinaryComposite 负责处理复杂对象,反射各成员,递归序列化。该处理器优先级最低。

来看看怎么样自定义一个处理器,以颜色处理器为例:

/// 颜色处理器。

publicclassBinaryColor:BinaryHandlerBase

{

/// 实例化

publicBinaryColor()

{

Priority=0x50;

}

/// 写入对象

/// 目标对象

/// 类型

///

publicoverrideBooleanWrite(Objectvalue,Typetype)

{

if(type!=typeof(Color))returnfalse;

varcolor=(Color)value;

WriteLog("WriteColor ",color);

Host.Write(color.A);

Host.Write(color.R);

Host.Write(color.G);

Host.Write(color.B);

returntrue;

}

/// 尝试读取指定类型对象

///

///

///

publicoverrideBooleanTryRead(Typetype,refObjectvalue)

{

if(type!=typeof(Color))returnfalse;

vara=Host.ReadByte();

varr=Host.ReadByte();

varg=Host.ReadByte();

varb=Host.ReadByte();

varcolor=Color.FromArgb(a,r,g,b);

WriteLog("ReadColor ",color);

value=color;

returntrue;

}

}

最后只需要挂载到Binary上即可序列化和反序列化带有Color类型的成员

varbn=newBinary();

bn.AddHandler();

总结

.NET内部自带二进制序列化BinaryFormatter,它会带上大量额外信息,导致体积很大,基本上很少用到。

Binary设计的初衷是序列化各种文件格式和通信协议,因此并没有过多考虑作为RPC通信格式。实际上NewLife组件自己的RPC框架ApiServer并没有使用Binary,而是选择了兼容性比较好的Json。

在中通的100亿Redis大数据中,尽管是二进制kv数据,同样没有用到Binary。因为它需要对字节数据进行极致控制,并且需要做多版本兼容。因此它实际上是直接读写二进制数据流,然后借用了Binary的一些辅助方法。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20210218A014YO00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券