跳到主要内容
版本:2.1

高性能二进制序列化

定义

命名空间:TouchSocket.Core
程序集:TouchSocket.Core.dll

一、说明

该序列化以二进制方式进行序列化,内存和性能都非常好。并且在序列化和反序列化时支持兼容类型,甚至可以像Json一样不同类型也可以。

目前支持的类型有:

  • 基础类型
  • 自定义实体类、结构体
  • 元组
  • 支持类型组成的数组、字典、List等。
提示

实际上经过自定义转化器,可以实现对任意类型的序列化和反序列化。

二、基本使用

2.1 简单使用

一般的,可以非常简单的对支持类型进行序列化和反序列化。

var bytes = FastBinaryFormatter.SerializeToBytes(10);
var newObj = FastBinaryFormatter.Deserialize<int>(bytes);

2.2 使用内存池块

在使用过程中,如果使用到频繁的序列化、反序列化,可以使用内存块,可以减少内存的申请和释放。

//申请内存块,并指定此次序列化可能使用到的最大尺寸。
//合理的尺寸设置可以避免内存块扩张。
using (var block = new ByteBlock(1024*64))
{
//将数据序列化到内存块
FastBinaryFormatter.Serialize(block, 10);

//在反序列化前,将内存块数据游标移动至正确位。
block.SeekToStart();

//反序列化
var newObj = FastBinaryFormatter.Deserialize<int>(block);
}

2.3 使用值类型内存池块

常规内存块是“类”,所以在使用时,自身对象会产生GC垃圾,如果追求极致序列化,则可以使用值类型内存池块,可以做到零GC分配。

//申请内存块,并指定此次序列化可能使用到的最大尺寸。
//合理的尺寸设置可以避免内存块扩张。
var block = new ValueByteBlock(1024 * 64);

try
{
//将数据序列化到内存块
FastBinaryFormatter.Serialize(ref block, 10);

//在反序列化前,将内存块数据游标移动至正确位。
block.SeekToStart();

//反序列化
var newObj = FastBinaryFormatter.Deserialize<int>(ref block);
}
finally
{
//因为使用了ref block,所以无法使用using,只能使用try-finally
block.Dispose();
}

三、常规配置

FastBinaryFormatter默认情况下,支持的自定义类型必须具有公共无参构造函数。对于成员,仅支持公共的属性和字段。并且对于属性,要求必须是可读可写,对于只读属性,默认也是不做序列化和反序列化的。

例如:下列类型中,只有P1P3成员将有效。

public class MyClass1
{
private int m_p5;

//公共属性,有效
public int P1 { get; set; }

//自动公共属性,即使包含set访问器,但private,无效
public int P2 { get; private set; }

//公共字段,有效
public int P3;

//私有字段,无效
private int P4;

//公共属性,不包含set访问器,无效
public int P5 => m_p5;

public void SetP4(int value)
{
this.P4 = value;
}

public void SetP2(int value)
{
this.P2 = value;
}
public void SetP5(int value)
{
this.m_p5 = value;
}
}

3.1 忽略成员

忽略成员,可以通过特性[FastNonSerialized]来忽略。

例如:

public class MyClass1
{
...

//公共属性,但忽略,无效
[FastNonSerialized]
public int P6 { get; set; }

}

3.2 强制成员

对于只读成员,有时候也需要序列化时,可以通过特性[FastSerialized]来强制。

例如:

public class MyClass1
{
...

//自动公共属性,包含set访问器,即使private,但因为FastSerialized后,有效
[FastSerialized]
public int P7 { get;private set; }
}
注意

强制特性虽然可以将成员添加在操作行列,但是如果成员绝对不可写(或者不可读)时,执行相应操作则会抛出异常。

四、兼容类型

在序列化和反序列化时,并不要求类型一致,只要类型成员名称一致,且对应名称的基础类型一致,即可进行转换。

例如:下列MyClass2MyClass3是两个不同类型

public class MyClass2
{
public int P1 { get; set; }
}

public class MyClass3
{
public int P1 { get; set; }
public string P2 { get; set; }
}

也可以互相序列化和反序列化。

var myClass2 = new MyClass2()
{
P1 = 10
};
var bytes = FastBinaryFormatter.SerializeToBytes(myClass2);

var newObj = FastBinaryFormatter.Deserialize<MyClass3>(bytes);

反序列化后的MyClass3

{"P1":10,"P2":null}

但如果是成员名称一致,但基础类型不一致的,则不会成功,且可能会抛出异常。

例如:

public class MyClass2
{
public int P1 { get; set; }
}

public class MyClass3
{
public string P1 { get; set; }
}
提示

兼容类型的使用,可以一定程度的解决一些兼容性问题,尤其是增加、或移除成员时都可以兼容。但是当修改成员类型时,可能会导致序列化数据丢失。

五、特性成员

默认情况下,确定成员的方式的是成员名称。当成员名称较长时(最大255字节),可能会大大增加序列化后的体积。

那这时候,就可以使用特性来确定成员。它使用的是一个byte值,来确定成员。

例如:

对于下列类,如果不使用特性,序列化体积可达40字节

public class MyClass4
{
public int MyProperty1 { get; set; }
public int MyProperty2 { get; set; }
}

使用特性后,体积可以减少到18字节

[FastSerialized(EnableIndex =true)]
public class MyClass4
{
[FastMember(1)]
public int MyProperty1 { get; set; }

[FastMember(2)]
public int MyProperty2 { get; set; }
}
信息

使用特性来确定成员唯一性时,使用的是byte类型的值,所以它只允许最多有255个成员(即属性和字段的总数量)。

六、自定义转换器

使用自定义转化器,可以解决所有类型的序列化与反序列化,并且可以对特定类型进行优化。

例如:

对于下列类,只有两个int类属性是有效值。

public class MyClass5
{
public int P1 { get; set; }
public int P2 { get; set; }
}

所以,我们需要自定义一个转换器。来将这2个int值,转换成有效数据。

首先,声明一个转换器类,继承FastBinaryConverter<T>,或者实现IFastBinaryConverter接口。

然后实现ReadWrite方法。实现逻辑如下:

public sealed class MyClass5FastBinaryConverter : FastBinaryConverter<MyClass5>
{
protected override MyClass5 Read<TByteBlock>(ref TByteBlock byteBlock, Type type)
{
//此处不用考虑为null的情况
//我们只需要把有效信息按写入的顺序,读取即可。

var myClass5 = new MyClass5();
myClass5.P1 = byteBlock.ReadInt32();
myClass5.P2 = byteBlock.ReadInt32();

return myClass5;
}

protected override void Write<TByteBlock>(ref TByteBlock byteBlock, in MyClass5 obj)
{
//此处不用考虑为null的情况
//我们只需要把有效信息写入即可。
//对于MyClass5类,只有两个属性是有效的。

//所以,依次写入属性值即可
byteBlock.WriteInt32(obj.P1);
byteBlock.WriteInt32(obj.P2);

}
}
信息

在转化器中,我们不需要考虑操作对象为null的情况。但是得考虑属性值为null的情况。

最后附加转换器即可

[FastConverter(typeof(MyClass5FastBinaryConverter))]
public class MyClass5
{
public int P1 { get; set; }
public int P2 { get; set; }
}

或者直接往FastBinaryFormatter中添加转换器。

FastBinaryFormatter.AddFastBinaryConverter(typeof(MyClass5),new MyClass5FastBinaryConverter());
注意

使用自定义转化器后,所有的类型兼容问题,都必须自己解决。如示例所示,我们是按顺序写入和读取的,所以一般来说,新增属性是可以的,但是移除属性时,可能得手动解决一些问题。

七、包模式序列化

FastBinaryFormatter支持一种特殊的序列化方式,包序列化模式 。可以解决一些特殊场景下的序列化,其性能更高。

并且,默认情况下,已经内置了转换器。

只需要对需要转换的对象实现IPackage接口(或继承PackageBase)即可。

例如:

下列类,实现了IPackage接口。在序列化时,会调用Package方法,反序列化时,会调用Unpackage方法。

public class MyClass6:PackageBase
{
public int P1 { get; set; }
public int P2 { get; set; }

public override void Package<TByteBlock>(ref TByteBlock byteBlock)
{
byteBlock.WriteInt32(this.P1);
byteBlock.WriteInt32(this.P2);
}

public override void Unpackage<TByteBlock>(ref TByteBlock byteBlock)
{
this.P1 = byteBlock.ReadInt32();
this.P2 = byteBlock.ReadInt32();
}
}

当然,在包模式的源生成可用时,也可以直接用源生成的方式实现更多细节。

[GeneratorPackage]
public partial class MyClass6:PackageBase
{
public int P1 { get; set; }
public int P2 { get; set; }
}
由源生成的代码
/*
此代码由SourceGenerator工具直接生成,非必要请不要修改此处代码
*/
#pragma warning disable
using System;
using System.Diagnostics;
using TouchSocket.Core;
using System.Threading.Tasks;

namespace FastBinaryFormatterConsoleApp
{
[global::System.CodeDom.Compiler.GeneratedCode("TouchSocket.SourceGenerator", "2.1.0.0")]
partial class MyClass6
{
public override void Package<TByteBlock>(ref TByteBlock byteBlock)
{
byteBlock.WriteInt32(P1);
byteBlock.WriteInt32(P2);
}

public override void Unpackage<TByteBlock>(ref TByteBlock byteBlock)
{
P1 = byteBlock.ReadInt32();
P2 = byteBlock.ReadInt32();
}
}
}
注意

当使用包模式序列化时,类型兼容性将跟随包类型一致。一般来说,新增属性是允许的,但是修改或移除属性是不允许的。

八、性能测试

8.1 简单测试

待测试类型

[Serializable]
public class MyPackPerson
{
public int Age { get; set; }
public string Name { get; set; }
}

结果

以下测试是执行10000次序列化和反序列的结果。

8.2 复杂类型测试

待测试类

 [Serializable]
public class Student
{
public int P1 { get; set; }
public string P2 { get; set; }
public long P3 { get; set; }
public byte P4 { get; set; }
public DateTime P5 { get; set; }
public double P6 { get; set; }
public byte[] P7 { get; set; }

public List<int> List1 { get; set; }
public List<string> List2 { get; set; }
public List<byte[]> List3 { get; set; }

public Dictionary<int, int> Dic1 { get; set; }
public Dictionary<int, string> Dic2 { get; set; }
public Dictionary<string, string> Dic3 { get; set; }
public Dictionary<int, Arg> Dic4 { get; set; }
}

[Serializable]
public class Arg
{
public Arg(int myProperty)
{
this.MyProperty = myProperty;
}

public Arg()
{
Person person = new Person();
person.Name = "张三";
person.Age = 18;
}

public int MyProperty { get; set; }
}
[Serializable]
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

赋值

Student student = new Student();
student.P1 = 10;
student.P2 = "若汝棋茗";
student.P3 = 100;
student.P4 = 0;
student.P5 = DateTime.Now;
student.P6 = 10;
student.P7 = new byte[1024 * 64];

Random random = new Random();
random.NextBytes(student.P7);

student.List1 = new List<int>();
student.List1.Add(1);
student.List1.Add(2);
student.List1.Add(3);

student.List2 = new List<string>();
student.List2.Add("1");
student.List2.Add("2");
student.List2.Add("3");

student.List3 = new List<byte[]>();
student.List3.Add(new byte[1024]);
student.List3.Add(new byte[1024]);
student.List3.Add(new byte[1024]);

student.Dic1 = new Dictionary<int, int>();
student.Dic1.Add(1, 1);
student.Dic1.Add(2, 2);
student.Dic1.Add(3, 3);

student.Dic2 = new Dictionary<int, string>();
student.Dic2.Add(1, "1");
student.Dic2.Add(2, "2");
student.Dic2.Add(3, "3");

student.Dic3 = new Dictionary<string, string>();
student.Dic3.Add("1", "1");
student.Dic3.Add("2", "2");
student.Dic3.Add("3", "3");

student.Dic4 = new Dictionary<int, Arg>();
student.Dic4.Add(1, new Arg(1));
student.Dic4.Add(2, new Arg(2));
student.Dic4.Add(3, new Arg(3));

结果

Fast的效率比System自带的,快了近7倍,比System.Text.Json快了4倍多,比NewtonsoftJson快了近30倍。