高性能二进制序列化
定义
命名空间: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
默认情况下,支持的自定义类型必须具有公共无参构造函数。对于成员,仅支持公共的属性和字段。并且对于属性,要求必须是可读可写,对于只读属性,默认也是不做序列化和反序列化的。
例如:下列类型中,只有P1
和P3
成员将有效。
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; }
}
强制特性虽然可以将成员添加在操作行列,但是如果成员绝对不可写(或者不可读)时,执行相应操作则会抛出异常。
四、兼容类型
在序列化和反序列化时,并不要求类型一致,只要类型成员名称一致,且对应名称的基础类型一致,即可进行转换。
例如:下列MyClass2
与MyClass3
是两个不同类型
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
接口。
然后实现Read
和Write
方法。实现逻辑如下:
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倍。