意外的 protobuf-net 序列化程序行为

Posted

技术标签:

【中文标题】意外的 protobuf-net 序列化程序行为【英文标题】:Unexpected protobuf-net serializer behavior 【发布时间】:2017-12-14 17:57:57 【问题描述】:

我们正在使用 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 时失败(我们观察到各种错误,从“缺少无参数构造函数”到 ***Exception)。

但是,在调用 FromProto 之前的某处添加这样的一行:ToProto(new SomeComplexType()); 会使错误消失,并且可以顺利反序列化同一组字节。似乎没有其他方法(我们尝试过 PrepareSerializer、GetSchema)可以解决问题。

看起来 ToProto 和 FromProto 解析对象模型的方式有一些细微的差别。另一点是,ProtoBuf 似乎“记住”了调用 ToProto 后的状态,这有助于后续的反序列化。

更新: 这里有更多细节: 我们的类结构看起来与此类似(非常简化):

[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 为类型准备序列化程序的方式有些不确定。这是我们观察到的。

假设我们有两个程序:生产者和消费者。生产者创建 D 的一个实例,添加一些数据并使用 protobuf-net 序列化该实例。消费者获取该序列化数据并将其反序列化为 D 的实例。

在生产者中,protobuf 有时会在发现 IBase 之前发现类型 B,因此它为 B 生成序列化程序并将 DictionaryOfBs 中的值序列化为 B 的直接实例。

在消费者中,protobuf-net可能会首先发现IBase,因此在为B准备(反)序列化器时,它会将其视为IBase的子类。因此,当涉及到 DictionaryOfBs 的反序列化值时,它试图将它们作为 IBase 的子类读取,期望字段编号能够区分 A 和 B。流中的数据可能是 IBase 序列化程序决定它看到的是一个实例的 A,尝试将其转换为 B(使用 Merge 方法)并进入无限递归,尝试将 A 转换为 B 转换为 A 转换为 B 等,从而最终导致 ***Exception。

在反序列化之前添加 Serializer.Serialize(stream, new D()) 会改变序列化器的创建顺序,因此在这种情况下没有错误,尽管这似乎是一个幸运的巧合.不幸的是,在我们的例子中,即使这样也不能用作令人满意的解决方法,因为这会导致偶尔出现“内部错误;发生键不匹配”反序列化错误。

【问题讨论】:

你有什么理由不使用像 xml,binary 这样的 .net 序列化?? @Pranay 有 很多 的理由不使用 BinaryFormatter,但是:protobuf is 二进制(至少在与 BinaryFormatter 相同的意义上) .至于原因:通常是生成输出的性能和大小。 @MarcGravell - 好的,这对我来说是新的,我会读出来而不是提供信息 @NikNik 我是 protobuf-net 的作者,我不记得 FromProto / ToProto 方法。现在,我完全有可能只是忘记了它们(我不在电脑前),但是:你确定它们不是你自己的方法吗?我在这里看不到它们:github.com/mgravell/protobuf-net/blob/master/src/protobuf-net/… 并且该类未标记为partial,因此我不需要查看任何其他文件... @Pranay 我忘记了最明显的原因:因为您想与正在使用 protobuf 的其他平台共享数据;几乎任何您可以命名的平台都有实现(因为它被许多 Google API 使用) 【参考方案1】:

序列化代码使用泛型 API,但由于泛型类型推断而使用 &lt;object&gt;。这可能会使事情变得混乱。首先要尝试的是 ToProto 方法使用 Serializer.NonGeneric.Serialize - 这将使用 .GetType() 等,希望能减少混淆。

或者:将ToProto 设为通用T value

注意:我尚未对此进行测试 - 但这是首先要尝试的。

【讨论】:

马克,谢谢您的回复。我将 ToProto 更改为通用版本,如下所示: public static byte[] ToProto(T value) using (var stream = new MemoryStream()) Serializer.Serialize(stream, value);返回流.ToArray(); 并再次运行我的测试。不幸的是,除非先调用相同类型的 Serialize,否则我仍然会收到 ***Exception。 @NikNik k;它可能与最近记录的一个错误有关,该错误也报告了堆栈溢出;你有没有机会尝试修复它? 抱歉,我无法共享该数据和代码。您能否发布您提到的那个错误的链接?

以上是关于意外的 protobuf-net 序列化程序行为的主要内容,如果未能解决你的问题,请参考以下文章

Protobuf-net:改变 System.Type 反序列化的方式

protobuf-net:反序列化 Guid 属性的错误线型异常

ProtoBuf-net 中集合 AsReference 的序列化

NEventStore 3+ 的 Protobuf-net 序列化程序

意外的子类型:MyNamespace.MyInheritedClass

为 protobuf-net 生成序列化程序集失败