我们使用protobuf-net v.2.3.2来序列化和反序列化我们项目中的一些复杂对象(包括列表,字典等) . 大多数情况下,一切都很好,但在极少数情况下,我们遇到了非常奇怪的行为:如果在第二个进程中调用序列化程序的 .FromProto<SomeComplexType>(bytes)
方法之前,在一个进程中序列化的对象会导致其他进程中的反序列化错误致电 .ToProto(someComplexObject)
.
这是一个例子:假设我们的流程1看起来像这样:
class Program1 {
public static void Main()
{
SomeComplexType complexObject = new SomeComplexType();
// Here goes some code filling complexObject with data
byte[] serialized = ToProto(complexObject);
File.WriteAllBytes("serialized.data", serialized);
}
public static byte[] ToProto(object value)
{
using (var stream = new MemoryStream())
{
ProtoBuf.Serializer.Serialize(stream, value);
return stream.ToArray();
}
}
public static T FromProto<T>(byte[] value)
{
using (var stream = new MemoryStream(value))
{
return ProtoBuf.Serializer.Deserialize<T>(stream);
}
}
}
现在,我们正在尝试在流程2中读取该对象:
class Program2 {
public static void Main()
{
byte[] serialized = File.ReadAllBytes("serialized.data");
SomeComplexType complexObject =
FromProto<SomeComplexType>(serialized);
}
public static byte[] ToProto(object value)
{
using (var stream = new MemoryStream())
{
ProtoBuf.Serializer.Serialize(stream, value);
return stream.ToArray();
}
}
public static T FromProto<T>(byte[] value)
{
using (var stream = new MemoryStream(value))
{
return ProtoBuf.Serializer.Deserialize<T>(stream);
}
}
}
我们看到的是,在极少数情况下,进程1生成的文件使进程2在调用FromProto时失败(我们观察到各种错误,从'缺少无参数构造函数'到StackOverflowException) .
但是,在调用FromProto之前的某处添加如下行: ToProto(new SomeComplexType());
会使错误消失,并且正在反序列化相同的字节集而没有任何障碍 . 没有其他方法(我们尝试过PrepareSerializer,GetSchema)似乎可以解决问题 .
看起来ToProto和FromProto如何解析对象模型存在一些细微差别 . 另一点是ProtoBuf在调用ToProto之后似乎“记住”了状态,这有助于它随后的反序列化 .
UPDATE: 以下是更多细节:我们看到的类结构与此类似(非常简化):
[ProtoContract(ImplicitFields = ImplicitFields.AllPublic)]
[ProtoInclude(1, typeof(A))]
[ProtoInclude(2, typeof(B))]
public interface IBase
{
[ProtoIgnore]
string Id { get; }
}
[ProtoContract(ImplicitFields=ImplicitFields.AllPublic, AsReferenceDefault=true)]
public class A : IBase
{
[ProtoIgnore]
public string Id { get; }
public string PropertyA { get; set; }
}
[ProtoContract(ImplicitFields=ImplicitFields.AllPublic, AsReferenceDefault=true)]
public class B : IBase
{
[ProtoIgnore]
public string Id { get; }
public string PropertyB { get; set; }
}
[ProtoContract(ImplicitFields=ImplicitFields.AllPublic, AsReferenceDefault=true)]
public class C
{
public List<IBase> ListOfBase = new List<IBase>();
}
[ProtoContract(ImplicitFields=ImplicitFields.AllPublic, AsReferenceDefault=true)]
public class D
{
public C PropertyC { get; set; }
public Dictionary<string, B> DictionaryOfBs { get; set; }
}
问题的根本原因似乎是Protobuf-net为类型准备序列化器的某种非确定性方式 . 这是我们观察到的 .
假设我们有两个程序: 生产环境 者和消费者 . Producer创建D的实例,添加一些数据并使用protobuf-net序列化该实例 . 消费者获取序列化数据并将其反序列化为D的实例 .
在 生产环境 者中,protobuf有时会在发现IBase之前发现B类,因此它会为B生成序列化器并将DictionaryOfBs中的值序列化为B的直接实例 .
在消费者中,protobuf-net可能首先发现IBase,因此当它为B准备(de)序列化器时,它将其视为IBase的子类 . 因此,在对DictionaryOfBs的值进行反序列化时,它会尝试将它们作为IBase的子类读取,期望字段编号区分A和B.流中的数据可能是IBase序列化程序决定它看到的是一个实例A,尝试将其转换为B(使用Merge方法)并进入无限递归,尝试将A转换为B转换为A转换为B等,从而导致最终的StackOverflowException .
在反序列化之前添加Serializer.Serialize(stream,new D())会改变创建序列化程序的顺序,因此在这种情况下没有错误,尽管这似乎是一个幸运的巧合 . 不幸的是,在我们的情况下,甚至不能将其用作令人满意的解决方法,因为这会导致反序列化时偶尔出现错误 .
1 回答
序列化代码使用通用API,但由于泛型类型推断而使用
<object>
. 这可能会让事情变得混乱 . 首先要尝试的是ToProto
方法使用Serializer.NonGeneric.Serialize
- 这将使用.GetType()
等,并希望减少它的混淆 .或者:使用
T value
制作ToProto
generic .注意:我没有测试过这个 - 但这是第一件要尝试的事情 .