[Из песочницы] Binary serialization in Unity3d or dot.net
Столкнулся с довольно-таки тривиальной проблемой. Сериализовать и десирилизовать данные.ЗадачаЕсть приложение, клиент-сервер. Клиент — Unity3d сервер PhotonServer. Есть модель, которая и на клиенте и на сервере должна быть эквивалентной. Требуется синхронизировать состояние модели и, возможно, дополнительные классы.Решение Protobuf Самое логичное решение — это использовать бинарный протокол. В этом явный фаворит — ptotobuf (использовал proto-net 668). Он не поддерживает веб-сборку, но это допустимая жертва. Разметил требуемые классы. Проверяю. Все работает, небольшой размер и быстрый в работе. Шикарно. Но! В один прекрасный момент Protobuf выплюнул екзепшен, мол, такой класс не найден. Как это? Баг подробно с примером кода.Начал различными способами решать эту проблему. Есть вариант скармливать Protobuf типы. Что уже не хорошо. Можно допустить достаточно много ошибок или забыть указать тот или иной тип. Более того, Protobuf не поддерживает многомерные массивы.
Как ни прискорбно, но Protobuf придется в сторону. К слову, однажды пытался использовать Protobuf в связке php и Unity. Со стороны php реализация Protobuf оказалась достаточно баганутой. В итоге в php и Unity использовал json. Это сработало, потому что между php и Unity ходили довольно-таки простые структуры данных.
Message pack Сещуствует еще один примечательный сериализатор. Есть реализации на огромное количество языков. Замечательно. Решил опробывать. Примитивный тип сериализовал нормально. Размер 18 байт против моего 41 байта, против 19 байтов protobuf и против 44 байтов json. Отличный результат. В чем же хитрость? На офицальном сайте есть пример, как он на самом деле все пакует. Вот ссылка.Пример [Serializable, ProtoContract ()] public class TTT { [TDataMember, ProtoMember (1)] public string s = «compact»; [TDataMember, ProtoMember (2)] public bool f = true; [TDataMember, ProtoMember (3)] public string s2 = «schema»; [TDataMember, ProtoMember (4)] public short i = 0;
} Но сложный пример, который будет, далее не осилил message pack, binary formatter и protobuf.Примеры ошибок.
BinaryFormatter SerializationException: Type TestS+TestC1 in assembly Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null is not marked as serializable. System.Runtime.Serialization.FormatterServices.GetSerializableMembers (System.Type type, StreamingContext context) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Serialization/FormatterServices.cs:101) System.Runtime.Serialization.Formatters.Binary.CodeGenerator.GenerateMetadataTypeInternal (System.Type type, StreamingContext context) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/CodeGenerator.cs:78) System.Runtime.Serialization.Formatters.Binary.CodeGenerator.GenerateMetadataType (System.Type type, StreamingContext context) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/CodeGenerator.cs:64) System.Runtime.Serialization.Formatters.Binary.ObjectWriter.CreateMemberTypeMetadata (System.Type type) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/ObjectWriter.cs:442) System.Runtime.Serialization.Formatters.Binary.ObjectWriter.GetObjectData (System.Object obj, System.Runtime.Serialization.Formatters.Binary.TypeMetadata& metadata, System.Object& data) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/ObjectWriter.cs:430) System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteObject (System.IO.BinaryWriter writer, Int64 id, System.Object obj) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/ObjectWriter.cs:306) System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteObjectInstance (System.IO.BinaryWriter writer, System.Object obj, Boolean isValueObject) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/ObjectWriter.cs:293) System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteQueuedObjects (System.IO.BinaryWriter writer) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/ObjectWriter.cs:271) System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteObjectGraph (System.IO.BinaryWriter writer, System.Object obj, System.Runtime.Remoting.Messaging.Header[] headers) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/ObjectWriter.cs:256) System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize (System.IO.Stream serializationStream, System.Object graph, System.Runtime.Remoting.Messaging.Header[] headers) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/BinaryFormatter.cs:232) System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize (System.IO.Stream serializationStream, System.Object graph) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/BinaryFormatter.cs:211) Message pack PlatformNotSupportedException: On-the-fly enum serializer generation is not supported in Unity iOS. Use pre-generated serializer instead. MsgPack.Serialization.ReflectionSerializers.ReflectionSerializerHelper.CreateReflectionEnuMessagePackSerializer[State] (MsgPack.Serialization.SerializationContext context) MsgPack.Serialization.MessagePackSerializer.CreateReflectionInternal[State] (MsgPack.Serialization.SerializationContext context) MsgPack.Serialization.MessagePackSerializer.CreateReflectionInternal (MsgPack.Serialization.SerializationContext context, System.Type targetType) MsgPack.Serialization.SerializationContext.GetSerializer (System.Type targetType, System.Object providerParameter) MsgPack.Serialization.ReflectionSerializers.ReflectionSerializerHelper.GetMetadata (MsgPack.Serialization.SerializationContext context, System.Type targetType, System.Func`2[]& getters, System.Action`2[]& setters, System.Reflection.MemberInfo[]& memberInfos, MsgPack.Serialization.DataMemberContract[]& contracts, MsgPack.Serialization.IMessagePackSerializer[]& serializers) MsgPack.Serialization.ReflectionSerializers.ReflectionObjectMessagePackSerializer`1[TestS+TestC]…ctor (MsgPack.Serialization.SerializationContext context) MsgPack.Serialization.MessagePackSerializer.CreateReflectionInternal[TestC] (MsgPack.Serialization.SerializationContext context) MsgPack.Serialization.SerializationContext.GetSerializer[TestC] (System.Object providerParameter) MsgPack.Serialization.SerializationContext.GetSerializer[TestC] ()
/// [,] string ArgumentException: 'System.String[,]' is not compatible for 'System.String[]'. Parameter name: objectTree MsgPack.Serialization.MessagePackSerializer`1[System.String[]].MsgPack.Serialization.IMessagePackSerializer.PackTo (MsgPack.Packer packer, System.Object objectTree) MsgPack.Serialization.ReflectionSerializers.ReflectionObjectMessagePackSerializer`1[TestS+TestC].PackMemberValue (MsgPack.Packer packer, .TestC objectTree, Int32 index) MsgPack.Serialization.ReflectionSerializers.ReflectionObjectMessagePackSerializer`1[TestS+TestC].PackToCore (MsgPack.Packer packer, .TestC objectTree) MsgPack.Serialization.MessagePackSerializer`1[TestS+TestC].PackTo (MsgPack.Packer packer, .TestC objectTree) MsgPack.Serialization.MessagePackSerializer`1[TestS+TestC].Pack (System.IO.Stream stream, .TestC objectTree) /// etc SerializationException: Non generic collection may contain only MessagePackObject type. MsgPack.Serialization.DefaultSerializers.NonGenericEnumerableSerializerBase`1[T].PackToCore (MsgPack.Packer packer, .T objectTree) MsgPack.Serialization.MessagePackSerializer`1[T].MsgPack.Serialization.IMessagePackSerializer.PackTo (MsgPack.Packer packer, System.Object objectTree) MsgPack.Serialization.ReflectionSerializers.ReflectionCollectionSerializer`1[System.Collections.ArrayList].PackToCore (MsgPack.Packer packer, System.Collections.ArrayList objectTree) MsgPack.Serialization.MessagePackSerializer`1[System.Collections.ArrayList].MsgPack.Serialization.IMessagePackSerializer.PackTo (MsgPack.Packer packer, System.Object objectTree) MsgPack.Serialization.ReflectionSerializers.ReflectionObjectMessagePackSerializer`1[TestS+TestC].PackMemberValue (MsgPack.Packer packer, .TestC objectTree, Int32 index) MsgPack.Serialization.ReflectionSerializers.ReflectionObjectMessagePackSerializer`1[TestS+TestC].PackToCore (MsgPack.Packer packer, .TestC objectTree) MsgPack.Serialization.MessagePackSerializer`1[TestS+TestC].PackTo (MsgPack.Packer packer, .TestC objectTree) MsgPack.Serialization.MessagePackSerializer`1[TestS+TestC].Pack (System.IO.Stream stream, .TestC objectTree) Protobuf-net NotSupportedException: Multi-dimension arrays are supported ProtoBuf.Meta.MetaType.ResolveListTypes (ProtoBuf.Meta.TypeModel model, System.Type type, System.Type& itemType, System.Type& defaultType) ProtoBuf.Meta.MetaType.ApplyDefaultBehaviour (Boolean isEnum, ProtoBuf.ProtoMemberAttribute normalizedAttribute) ProtoBuf.Meta.MetaType.ApplyDefaultBehaviour () ProtoBuf.Meta.RuntimeTypeModel.FindOrAddAuto (System.Type type, Boolean demand, Boolean addWithContractOnly, Boolean addEvenIfAutoDisabled) ProtoBuf.Meta.RuntimeTypeModel.GetKey (System.Type type, Boolean demand, Boolean getBaseKey) Json Итак, Protobuf не подходит. Что использовать? Json? Почему бы и нет. Тут вторая проблема: джейсон не умеет сериализовать поля типа интерфейс и абстрактные классы. Не беда, воспользовавшись гуглом нашел, как «научить» его делать это. В итоговом файле появились данные о типе поля и его данные (с указанием сборки, это важно; почему — написано далее). Но при десериализации это поле почему-то нул. Снова гуглю. Ведь если научил сериализовать, значит можно и десериализовать. Получается тот же костыль, что и с Protobuf. Такой вариант не подходит. Использовал сборку JSON .NET For Unity, который есть в ассетмаркете.Итог: Json хорош для не сложных структур. Но когда есть поля типа абстрактный класс или интерфейс, с ним возникают проблемы.
XML В любой вариации xml — достаточно громоздкий. Поэтому решил не рассматривать. Хоть и часть проекта на xml. Например, система локализации.BinaryFormatter Решил обратится к стандартным средствам. Разметил код, сериализуем. Success! Большой объем файла, правда, не есть хорошо. Не беда, пройдемся еще и компрессией. Использовал LZMA. Выиграл немного в размере, но проиграл по скорости работы. Допустимая жертва. Теперь сборки. Барабанная дробь. Веб не поддерживается, беда…Теперь устроим обмен между клиентом и сервером. И… Очередной FAIL. Дело в том, что сборки у классов разные, хоть классы одни и те же. В юнити своя сборка на фотоне своя. Можно решить через костыльный способ. Забиндить сборки и вручную их переименовать, но сборка попадает в бинарный файл. Зачем она там нужна?
Решил что к этому способу вернусь, просмотрел еще парочку сериализаторов. Один из них Шарп сериализатор. Смог сериализовать поля типа интерфейс, но тоже прописывает сборку и не поддерживается в вебе. Тогда я решил сначала сформировать требования к сериализатору.
Тест примитивного типа TTT c = new TTT (); TSerizalization serizalization = new TSerizalization (); bytes = serizalization.Serizalize (c, true); System.IO.File.WriteAllBytes («d:\\s.dat», bytes); Debug.LogError («T complete » + bytes.Length);
json = JsonConvert.SerializeObject©; System.IO.File.WriteAllText («d:\\s.json», json); Debug.LogError («J complete » + json.Length);
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formater = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter (); formater.AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Full; System.IO.MemoryStream mstream = new System.IO.MemoryStream (); formater.Serialize (mstream, c); Debug.LogError («B complete » + mstream.ToArray ().Length);
System.IO.File.WriteAllBytes («d:\\s2.dat», mstream.ToArray ());
mstream = new System.IO.MemoryStream ();
var serializer = MsgPack.Serialization.SerializationContext.Default.GetSerializer
mstream = new System.IO.MemoryStream ();
ProtoBuf.Serializer.Serialize
Как сохранить объекты и как их загрузить. В этом плане мне понравился подход protobuf-net 668. А именно — маркировать требуемые поля и свойства. Также маркировать и методы, которые будут вызваны до сериализации и после десериализации.
Карта
Для начала нужно сохранить карту. А именно — ключ и тип. Чтобы по этой карте можно было потом восстановить объект. Для стандартных типов значение 0. Размер ключа int16.Map
public class TMap
{
public Dictionary
Контейнер public class TContainer: TContainerBase { public int Size { get; protected set; } public List
Чтение запись
public abstract class TReaderBase
{
public abstract T Read
protected TWriterBase writer;
protected TReaderBase reader;
public TSerizalization ()
{
writer = new TBinaryWriter ();
reader = new TBinaryReader ();
}
public virtual byte[] Serialize (object target, bool callBeforeSerializationMethods = false);
public virtual T Deserialize
[To2dnd.TDataMember] public State state = State.Close;
[To2dnd.TDataMember] public DateTime dt = new DateTime ();
[To2dnd.TDataMember] public Type type = typeof (IClass);
[To2dnd.TDataMember] public string[,] arr = new string[,] { {»1111»,»2222»,»3333»,»4444» }, {«aaaa», «bbbb», «cccc», «dddd» }, {»321»,»32»,»2qfs»,»12f» } };
[To2dnd.TDataMember] public object classD = new TestC2();
[To2dnd.TDataMember] public TestC1[] array1 = new TestC1[] { new TestC1(), new TestC2(), new TestC2() };
[To2dnd.TDataMember] public ArrayList arr2 = new ArrayList (new string[]{ «list1», «list2» });
[To2dnd.TDataMember]
public List
[To2dnd.TDataMember]
public Dictionary
[To2dnd.TDataMember] public Hashtable ht = new Hashtable () { {«H one», 1}, {«H two», 2}, {«H three», 3}, {«H four», 4} };
[To2dnd.TDataMember]
public SortedList
[To2dnd.TDataMember]
public Dictionary> l = new List
>()
{
new List
[ProtoMember (16)]
public Dictionary
[ProtoMember (17)]
public Dictionary
[ProtoMember (18)]
public Dictionary
public TestC ()
{
Dic4 = new Dictionary
} [Serializable] public class TestC1: IClass { [To2dnd.TDataMember] public float value1 = 10; [To2dnd.TDataMember] public float value2 = 12; }
[Serializable ] public class TestC2: TestC1 { [To2dnd.TDataMember] public float a1 = 10; [To2dnd.TDataMember] public float b2 = 12; [To2dnd.TDataMember] public string str = «Class 1»;
[To2dnd.TDataMember] public State state = State.Close;
public TestC2() { }
[TAfterDeserialization] public void After () { }
[TBeforeSerialization] public void Before () { } }
public class TestC33 { [To2dnd.TDataMember] public float b2 = 12;
[To2dnd.TDataMember] public TestC2 tt = new TestC2();
[ To2dnd.TDataMember] public TestC1[] array1 = new TestC1[] { new TestC1(), new TestC2(), new TestC2() };
[To2dnd.TDataMember] public object classD = new TestC2();
[To2dnd.TDataMember] public Type type = typeof (IClass); } Видео теста:
[embedded content]
Итог По объему файла, конечно, проигрывает Protobuf и messagepack. Ведь я сохраняю карту типов и не использую хитрые махинации с смещением битов или конвертации строк «byte[] bytes = Encoding.UTF7.GetBytes ((string)data.value)». Это дополнительная нагрузка, возможно, потом расширю в виде вариативности. Протестировал обмен данными между фотоном и юнити. Работает как и ожидалось. Ведь я создаю тип относительно сборки, которая является параметром в методе Deserialize.Готовые решения, которых так много в интернете, не подошли по требованиям. Поэтому пришлось изобрести велосипед. Который оправдал затраченное на него время. Его можно расширять улучшать.
Заключение Если вы используете примитивные типы, то вам подойдет любой из рассмотренных сериализаторов. Для примитивов я все же предпочел бы Protobuf. Но для сложных типов данных готовые решения подойдут не всегда.Ссылки ProtobufUnity3d JsonMSDN binary formatterMessage packБечмарки