Я использую Protobuf для связи между моим веб-клиентом и сервером (С#), используя WebSocket. На клиенте де-сериализация выполняется через Protobuf.js, а на сервере - протокол protobuf-net.
Проблема заключается в том, что при использовании агрегации с абстрактными классами protobuf-net не может десериализовать данные, отправленные Protobuf.js.
Это трассировка стека:
ProtoException: No parameterless constructor found for Base.
at ProtoBuf.Meta.TypeModel.ThrowCannotCreateInstance(Type type) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 1397
at proto_6(Object , ProtoReader )
at ProtoBuf.Serializers.CompiledSerializer.ProtoBuf.Serializers.IProtoSerializer.Read(Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Serializers\CompiledSerializer.cs:line 57
at ProtoBuf.Meta.RuntimeTypeModel.Deserialize(Int32 key, Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Meta\RuntimeTypeModel.cs:line 775
at ProtoBuf.ProtoReader.ReadTypedObject(Object value, Int32 key, ProtoReader reader, Type type) na c:\Dev\protobuf-net\protobuf-net\ProtoReader.cs:line 579
at ProtoBuf.ProtoReader.ReadObject(Object value, Int32 key, ProtoReader reader) na c:\Dev\protobuf-net\protobuf-net\ProtoReader.cs:line 566
at proto_2(Object , ProtoReader )
at ProtoBuf.Serializers.CompiledSerializer.ProtoBuf.Serializers.IProtoSerializer.Read(Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Serializers\CompiledSerializer.cs:line 57
at ProtoBuf.Meta.RuntimeTypeModel.Deserialize(Int32 key, Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Meta\RuntimeTypeModel.cs:line 775
at ProtoBuf.Meta.TypeModel.DeserializeCore(ProtoReader reader, Type type, Object value, Boolean noAutoCreate) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 700
at ProtoBuf.Meta.TypeModel.Deserialize(Stream source, Object value, Type type, SerializationContext context) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 589
at ProtoBuf.Meta.TypeModel.Deserialize(Stream source, Object value, Type type) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 566
at ProtoBuf.Serializer.Deserialize[T](Stream source) na c:\Dev\protobuf-net\protobuf-net\Serializer.cs:line 77
at ProtobufPolymorphismTest.Program.Main(String[] args) na c:\Desenvolvimento\Testes\ProtobufPolymorphismTest\ProtobufPolymorphismTest\Program.cs:line 30
at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()
Это контракт:
[ProtoContract]
[ProtoInclude(100, typeof(Child))]
abstract class Base
{
[ProtoMember(1)]
public int BaseProperty { get; set; }
}
[ProtoContract]
class Child : Base
{
[ProtoMember(1)]
public float ChildProperty { get; set; }
}
[ProtoContract]
class Request
{
[ProtoMember(1)]
public Base Aggregate { get; set; }
}
И это код для воспроизведения ошибки. Когда сериализация работает, я предоставляю результат только в виде байтового массива. Если это поможет, я могу предоставить шаги, которые я предпринял для получения сериализованных значений.
// This is the object serialized
Child child = new Child() { ChildProperty = 0.5f, BaseProperty = 10 };
Request request = new Request() { Aggregate = child };
// This is the byte representation generated by protobuf-net and Protobuf.js
byte[] protoNet = new byte[] { 10, 10, 162, 6, 5, 13, 0, 0, 0, 63, 8, 10 };
byte[] protoJS = new byte[] { 10, 10, 8, 10, 162, 6, 5, 13, 0, 0, 0, 63 };
// Try to deserialize the protobuf-net data
using (System.IO.MemoryStream ms = new System.IO.MemoryStream(protoNet))
{
request = Serializer.Deserialize<Request>(ms); // Success
}
// Try to deserialize the Protobuf.js data
using (System.IO.MemoryStream ms = new System.IO.MemoryStream(protoJS))
{
request = Serializer.Deserialize<Request>(ms); // ProtoException: No parameterless constructor found for Base.
}
Если я добавлю SkipConstructor = true в определение базового класса, ошибка изменится на "MemberAccessException: не может создать абстрактный класс" со следующей трассировкой стека. Если я удалю абстрактный из определения базового класса, он работает как ожидалось.
System.MemberAccessException: Cannot create an abstract class.
at System.Runtime.Serialization.FormatterServices.nativeGetUninitializedObject(RuntimeType type)
at ProtoBuf.BclHelpers.GetUninitializedObject(Type type) na c:\Dev\protobuf-net\protobuf-net\BclHelpers.cs:line 38
at proto_6(Object , ProtoReader )
at ProtoBuf.Serializers.CompiledSerializer.ProtoBuf.Serializers.IProtoSerializer.Read(Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Serializers\CompiledSerializer.cs:line 57
at ProtoBuf.Meta.RuntimeTypeModel.Deserialize(Int32 key, Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Meta\RuntimeTypeModel.cs:line 775
at ProtoBuf.ProtoReader.ReadTypedObject(Object value, Int32 key, ProtoReader reader, Type type) na c:\Dev\protobuf-net\protobuf-net\ProtoReader.cs:line 579
at ProtoBuf.ProtoReader.ReadObject(Object value, Int32 key, ProtoReader reader) na c:\Dev\protobuf-net\protobuf-net\ProtoReader.cs:line 566
at proto_2(Object , ProtoReader )
at ProtoBuf.Serializers.CompiledSerializer.ProtoBuf.Serializers.IProtoSerializer.Read(Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Serializers\CompiledSerializer.cs:line 57
at ProtoBuf.Meta.RuntimeTypeModel.Deserialize(Int32 key, Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Meta\RuntimeTypeModel.cs:line 775
at ProtoBuf.Meta.TypeModel.DeserializeCore(ProtoReader reader, Type type, Object value, Boolean noAutoCreate) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 700
at ProtoBuf.Meta.TypeModel.Deserialize(Stream source, Object value, Type type, SerializationContext context) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 589
at ProtoBuf.Meta.TypeModel.Deserialize(Stream source, Object value, Type type) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 566
at ProtoBuf.Serializer.Deserialize[T](Stream source) na c:\Dev\protobuf-net\protobuf-net\Serializer.cs:line 77
at ProtobufPolymorphismTest.Program.Main(String[] args) na c:\Desenvolvimento\Testes\ProtobufPolymorphismTest\ProtobufPolymorphismTest\Program.cs:line 30
at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()
Я не уверен, почему двоичное представление, сгенерированное через protobuf-net и Protobuf.js, отличается, но оба они кажутся действительными, поскольку они работают, если базовый класс не является абстрактным.
Любые идеи о том, почему это происходит, или способ обойтись без удаления абстрактного из базового класса?
Спасибо заранее!
UPDATE
Это код, который я использовал для генерации байтовой сериализации через Protobuf.js:
<script src="/ByteBufferAB.min.js"></script>
<script src="/ProtoBuf.js"></script>
<script type="text/javascript">
// Proto file
var proto = "";
proto += "package ProtobufPolymorphismTest;\r\n\r\n";
proto += "message Base {\r\n";
proto += " optional int32 BaseProperty = 1 [default = 0];\r\n";
proto += " // the following represent sub-types; at most 1 should have a value\r\n";
proto += " optional Child Child = 100;\r\n";
proto += "}\r\n\r\n";
proto += "message Child {\r\n";
proto += " optional float ChildProperty = 1 [default = 0];\r\n";
proto += "}\r\n\r\n";
proto += "message Request {\r\n";
proto += " optional Base Aggregate = 1;\r\n";
proto += "}";
// Build the entities
var protoFile = dcodeIO.ProtoBuf.loadProto(proto);
var requestClass = protoFile.build("ProtobufPolymorphismTest.Request");
var baseClass = protoFile.build("ProtobufPolymorphismTest.Base");
var childClass = protoFile.build("ProtobufPolymorphismTest.Child");
// Build the request
var base = new baseClass();
base.BaseProperty = 10;
base.Child = new childClass();
base.Child.ChildProperty = 0.5;
var request = new requestClass();
request.Aggregate = base;
// Serialize
var bytes = new Uint8Array(request.toArrayBuffer());
var str = "new byte[] { " + bytes.join(", ") + " };";
console.log(str);
</script>
РЕШЕНИЕ
Как пояснил Марк, protobuf-net не поддерживает полиморфизм, когда порядок поля инвертирован. В качестве обходного пути, специфичного для Protobuf.js, вы можете изменить порядок полей в файле .proto для его сериализации в правильном порядке.
В моем случае изменение файла .proto на следующее решение проблемы:
package ProtobufPolymorphismTest;
message Base {
// the following represent sub-types; at most 1 should have a value
optional Child Child = 100;
optional int32 BaseProperty = 1 [default = 0];
}
message Child {
optional float ChildProperty = 1 [default = 0];
}
message Request {
optional Base Aggregate = 1;
}
(Обратите внимание на optional Child Child = 100;
перед BaseProperty
)
Долгое и короткое из них заключается в том, что поддержка protobuf-net polyorpism предполагает, что подтип будет первым в сообщении (или, более конкретно: для любого объекта тип будет исправлен до данных предоставлен). В выводе js данные поля для BaseProperty
приходят сначала - вполне разумно, возможно. Но так как не существует перечеркнутого определения протокола о том, как должно вести себя наследование, реализация protobuf-net была действительно действительно предназначена для работы с самим собой. В терминах байтов это фактически сводится к тому, где появляется маркер поля "162, 6" (и связанная длина/данные, "5, 13, 0, 0, 0, 63" ).
Библиотека может быть переработана, чтобы разрешить любой порядок полей для полиморфизма, но: это потребует определенных усилий. Я знаю, что обычно предполагается обрабатывать поля в любом порядке, но поскольку это уже вне спецификации, я не фокусировался на этом. Все остальные поля данных принимаются в любом порядке - только полиморфизм работает таким образом.
В общем случае: поскольку полиморфизм не является частью спецификации, я настоятельно рекомендую избегать полиморфизма при работе между библиотеками.
Примечание. Вероятно, вы можете заставить это работать, гарантируя, что поля полиморфизма ниже (численно), чем поля данных.
optional Child Child = 100;
к началу сообщения, чтобы он генерировал такое же представление байтов, как и protobuf-net. Я обновлю вопрос, чтобы добавить эту заметку. Большое спасибо!