1. 程式人生 > >Protobuf簡單型別直接反序列化方法

Protobuf簡單型別直接反序列化方法

我有一個想法,有一個能夠進行跨平臺的高效能資料協議規範,能夠讓資料在兩個不同的程式之間進行讀取,最好能夠支援直接將object序列化,那就完美了。 ## 目標 1. 支援任意Object序列化 2. 支援從類似*System.String*的字串中獲取類的資訊並進行反序列化 3. 支援簡單物件的直接序列化與反序列化 ## 方案 ### Xml序列化 說到序列化,.NET自帶的XML序列化就很好用了,無奈有很多型別不支援,典型的比如`Dictionary<>`,而且這個東西雖然強大,但是xml的標籤機制導致多餘的內容比較多,空間佔用會比較大。 ### Binary序列化 支援任意object序列化,.NET還提供了`BinaryFormatter`。 ```C# // 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的狀態,操作非常便捷,我認為非常合適。 ### Protobuf 這個東西就是grpc中的資料格式,可以跨平臺,支援多種語言,資料是二進位制的,壓縮率也很高。好吧,就是它了。 如果要在.NET中使用Protobuf協議,經常用的兩個類庫,一個是`Google.Protobuf`,另外一個是`protobuf.net`。詳細的區別我就不贅述了,有一篇[文章](https://www.shisujie.com/blog/Google-ProtoBuf-vs-ProtoBuf-Net)有多個對比。由於我比較喜歡直接使用C#的型別系統,所以我還是聽從文章建議,直接使用protobuf.net了。 ## protobuf-net 對於通訊雙方都是.NET程式的情況下,使用protobuf不需要直接編寫proto檔案,可以直接共享資料類的引用。如果是需要與非.NET程式進行通訊的話,也可以通過工具生成,直接從proto中讀取資訊並生成類。回顧一下目標,一條條處理。 1. 支援任意物件的序列化 protobuf通過定義實體類來進行序列化,所以也是支援任意物件的。這裡我就不再詳細說明了,可以在[官網](https://github.com/protobuf-net/protobuf-net)檢視詳細使用方法。 2. 支援從類似*System.String*的字串中獲取類的資訊並進行反序列化 一直有一個痛點,能否從序列化後的內容中還原一般物件,就是物件型別在編譯的時候未知的那種。通過儲存型別的string名稱,在需要反序列化的時候,通過型別名稱載入型別,將內容反序列化為指定型別。這個多多少少要用到反射了吧。 ```c# static void Main(string[] args) { var ps = new List { "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物件,不過返回的型別還是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型別下手。 ```c# 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名稱的程式碼。 我按照這個思路,完整的程式碼如下: ```c# static void Main(string[] args) { var ps = new List { "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]); } } ``` 3. 支援簡單物件的直接序列化與反序列化 我說的簡單物件,就是系統定義的泛型集合與直接有`TypeCode`,並且不是object的物件。補充一下,我常用的幾種。 ### 內建型別 定義在System名稱空間下的型別,包括DateTime,Int32之類的,都是直接**System.型別名稱**的形式。 > 注意,int和float這種是不行的,需要使用Int32和Single。 ### 泛型集合+內建型別 泛型集合定義在System.Collections.Generic這個名稱空間,所以組合起來為**System.Collections.Generic.泛型名稱`引數數量[System.型別名稱]**。舉兩個例子: `List`的是**System.Collections.Generic.List`1[System.String]**。 `Dictionary`的是**System.Collections.Generic.Dictionary`2[[System.Int32],[System.String]]**。 ### 內建型別陣列 直接在名稱後新增*[]*即可,形式為**System.型別名稱[]**。 ## 補充 序列化操作不要求序列化的型別和反序列化的型別完全一致,比如說Array可以與List,IEnumerable進行互換。因此,一些單獨定義的、結構比較簡單的型別,可以通過內建型別進行反序列化,就沒有必要在反序列化的時候載入原始的類了,簡化了操作。 ```c# [ProtoContract] public class Message { [ProtoMember(1)] public List values { get; set; } } static void Main(string[] args) { var ps = new Message { values = new List { "1346dfg", "31461sfghj", "24576sth" } }; using (FileStream ms = new FileStream("d:\\a.txt", FileMode.Create)) { Serializer.Serialize(ms, ps); } using (FileStream ms = new FileStream("d:\\a.txt", FileMode.Open)) { //List反序列化,而無需使用Message類。這裡Message的FullName是"ConsoleApp6.Program+Message" dynamic data = Serializer.Deserialize(Type.GetType("System.Collections.Generic.List`1[System.String]"), ms); Console.WriteLine(data[1]); } } ``` 另外,對於上面的dynamic,由於編譯的時候不檢查,怕操作錯誤的同學可以進行型別轉換。分享一個程式碼段,可能能有點幫助。 ```c# //轉換物件為某一種型別 public static T ConvertTo(object value) { return (T)Convert.ChangeType(value, typeof(T)); } ``` 如果是限定的幾種型別,可以使用switch語句進行判斷,並將物件轉成T,以進行型別安全的操作。如果不是的話,推薦使用介面來定義類的通用行為,這個[回答](https://stackoverflow.com/questions/972636/casting-a-variable-using-a-type-variable/1145562#1145562)中提供了一些建議,推薦看看。 ## 參考資料 * [內建型別](https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/builtin-types/built-in-types) * [https://stackoverflow.com/questions/7442164/c-sharp-and-net-how-to-serialize-a-structure-into-a-byte-array-using-binary#](https://stackoverflow.com/questions/7442164/c-sharp-and-net-how-to-serialize-a-structure-into-a-byte-array-using-