我有一个想法,有一个能够进行跨平台的高性能数据协议规范,能够让数据在两个不同的程序之间进行读取,最好能够支持直接将object序列化,那就完美了。
目标支持任意Object序列化
支持从类似System.String的字符串中获取类的信息并进行反序列化
支持简单对象的直接序列化与反序列化
方案 Xml序列化说到序列化,.NET自带的XML序列化就很好用了,无奈有很多类型不支持,典型的比如Dictionary<>,而且这个东西虽然强大,但是xml的标签机制导致多余的内容比较多,空间占用会比较大。
Binary序列化支持任意object序列化,.NET还提供了BinaryFormatter。
// code from https://stackoverflow.com/questions/7442164/c-sharp-and-net-how-to-serialize-a-structure-into-a-byte-array-using-binary MyObject obj = new MyObject(); byte[] bytes; IFormatter formatter = new BinaryFormatter(); using (MemoryStream stream = new MemoryStream()) { formatter.Serialize(stream, obj); bytes = stream.ToArray(); }这种方式支持任意的object进行序列化,不过有一个问题,它和Type模型严格绑定,只支持同一个程序集版本的消息交互,也不支持其他语言编写的程序。
我之前用过这种方式,用于单个程序内的数据快速保存与读取。这种情况下,只是单纯在为了保存object的状态,操作非常便捷,我认为非常合适。
这个东西就是grpc中的数据格式,可以跨平台,支持多种语言,数据是二进制的,压缩率也很高。好吧,就是它了。
如果要在.NET中使用Protobuf协议,经常用的两个类库,一个是Google.Protobuf,另外一个是protobuf.net。详细的区别我就不赘述了,有一篇文章有多个对比。由于我比较喜欢直接使用C#的类型系统,所以我还是听从文章建议,直接使用protobuf.net了。
protobuf-net对于通信双方都是.NET程序的情况下,使用protobuf不需要直接编写proto文件,可以直接共享数据类的引用。如果是需要与非.NET程序进行通信的话,也可以通过工具生成,直接从proto中读取信息并生成类。回顾一下目标,一条条处理。
支持任意对象的序列化
protobuf通过定义实体类来进行序列化,所以也是支持任意对象的。这里我就不再详细说明了,可以在官网查看详细使用方法。
支持从类似System.String的字符串中获取类的信息并进行反序列化
一直有一个痛点,能否从序列化后的内容中还原一般对象,就是对象类型在编译的时候未知的那种。通过保存类型的string名称,在需要反序列化的时候,通过类型名称加载类型,将内容反序列化为指定类型。这个多多少少要用到反射了吧。
static void Main(string[] args) { var ps = new List<string> { "1346dfg" , "31461sfghj", "24576sth"} ; var name = ps.GetType().FullName; using (FileStream ms = new FileStream("d:\\a.txt", FileMode.Create)) { Serializer.Serialize(ms, ps); } using (FileStream ms = new FileStream("d:\\a.txt", FileMode.Open)) { //data已经转换为List<string>对象,不过返回的类型还是object,可以强制转换。 dynamic data = Serializer.Deserialize(Type.GetType(name), ms); Console.WriteLine(data[1]); } }这里使用到了一个Type类型的FullName属性,对于内置类型对象,假设ps的类型是String的话,那个FullName为System.String,返回的内容很简单。但在这个例子中,FullName为System.Collections.Generic.List`1[[System.String, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]],感觉一下子复杂了很多,而且要命的是,这里明确指明了CoreLib的引用,还有版本声明。如果需要在.NET Core3.1中反序列化,肯定是无法实现。
尝试解决一下,这个System.String不光在.NET 5中有,在其他.NET平台应该都可以支持,所以得想办法去掉System.String的尾巴。
可以试着对FullName下手,但是这个东西有点太长了,而且直接处理字符串我不是很喜欢;试着从Type类型下手。
var ty = Type.GetType(name); Console.WriteLine(ty.Name); Console.WriteLine(ty.Namespace); Console.WriteLine(ty.GenericTypeArguments[0].Name); Console.WriteLine(ty.GenericTypeArguments[0].Namespace); //组合相关的代码 dynamic data = Serializer.Deserialize(Type.GetType($"{ty.Namespace}.{ty.Name}" + $"[{ty.GenericTypeArguments[0].Namespace}.{ty.GenericTypeArguments[0].Name}]"), ms); Console.WriteLine(data[1]);稍微修改一下,通过手动连接Namespace与Name属性就可以达到我们的目的了。
List`1这个代表这个泛型里面只有一个参数,我这边就硬编码了,对于其他泛型,可能有多个参数,需要进行鉴别,并调整构造Type名称的代码。
我按照这个思路,完整的代码如下:
static void Main(string[] args) { var ps = new List<string> { "1346dfg", "31461sfghj", "24576sth" }; var ty = ps.GetType(); //保存Type名称 var name = $"{ty.Namespace}.{ty.Name}" + $"[{ty.GenericTypeArguments[0].Namespace}.{ty.GenericTypeArguments[0].Name}]"; //实际的程序不涉及文件操作,这里展示MemoryStream的用法。 using (MemoryStream ms = new MemoryStream()) { Serializer.Serialize(ms, ps); //重置指针,从头开始读 ms.Position = 0; //使用Type名称反序列化 dynamic data = Serializer.Deserialize(Type.GetType(name), ms); Console.WriteLine(data[1]); } }