二进制序列化

本文介绍了NewLife.Core中的二进制序列化框架Binary,它具有体积小、格式简单、支持广泛等特点,适用于多种文件格式和通信协议的序列化。Binary支持Handler处理器扩展,允许自定义数据类型处理,但不支持版本控制,可读性较差。文章通过示例展示了如何使用Binary进行快速序列化和反序列化,并提供了关键成员和自定义处理器的详细说明。
摘要由CSDN通过智能技术生成

在计算机世界,万物皆01二进制,包括各种各样的文件格式和网络协议,二进制格式最为常见!NewLife.Core 内置了完整的二进制序列化框架 Binary,经过十多年洗礼,发展到了第三代支持Handler处理器扩展。Binary的同类框架有 Protobuf、Thrift、MessagePack。

Nuget包:NewLife.Core

源码地址:https://github.com/NewLifeX/X/tree/master/NewLife.Core/Serialization/Binary

主要特性

Binary主要功能特性:

  1. 体积极小。Binary是Schemaless架构,不包含字段名和序号,用最少的字节去保存数据

  2. 压缩整数。大多数时候Int32字段保存的数字很小,采用七位压缩编码整数保存可以减少体积

  3. 格式简单。尽管Binary只有.NET版,但其格式非常简单,可以很容易在其它语言上实现

  4. 支持性很广。Binary设计初衷,就是用于实现各种已知文件格式和通信协议,例如ZipFile

  5. 支持动态特性。可根据某些字段值,生成不同消息类型,例如MQTT和DNS协议

  6. 可读性较差。二进制格式且没有Schema,可读性较差

  7. 无版本支持。需要读写双方约定好多版本格式的兼容

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的一些辅助方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值