使用Protobuf-net,我突然得到一个关于一个未知的线型的例外

(这是我在RSS中看到的一个问题的转贴,但被OP删除了,我又重新添加了,因为我在不同的地方看过这个问题几次,形成”)

突然间,我反序列ProtoException时收到一个ProtoException ,消息是:未知的线型6

  • 什么是导线型?
  • 什么是不同的线型值及其描述?
  • 我怀疑一个字段是造成这个问题,怎么debugging呢?

首先要检查:

input数据PROTOBUF数据? 如果你尝试和parsing另一种格式(json,xml,csv,binary-formatter),或者只是简单的数据(例如“内部服务器错误”html占位符文本页面),那么它将无法工作


什么是导线型?

这是一个3比特的标志(广义上讲,它只有3比特)告诉它下一个数据是什么样的。

协议缓冲区中的每个字段都由一个头部作为前缀,告诉它它代表哪个字段(数字),接下来是什么types的数据; 这个“什么types的数据”对于支持stream中未预料到的数据(例如,在一端添加了字段到数据types)的情况是非常重要的,因为它让序列化程序知道如何读取数据(或根据需要将其存储起来)。

什么是不同的线型值及其描述?

  • 0:变长整数(最多64位) – base-128编码,MSB指示连续(用作整数types的默认值,包括枚举)
  • 1:64位 – 8个字节的数据(用于double ulong ,或select long / long
  • 2:长度前缀 – 首先使用变长编码读取整数; 这告诉你有多less个字节的数据(用于string, byte[] ,“packed”数组,以及作为子对象属性/列表的默认值)
  • 3:“开始组” – 用于编码使用开始/结束标签的子对象的替代机制 – Google大部分已弃用,因此跳过整个子对象字段会更加昂贵,因为您不能仅仅“寻找”目的
  • 4:“结束小组” – 与3结对
  • 5:32位 – 4字节的数据(用于float ,或者可选地用于int / uint和其他小整数types)

我怀疑一个字段是导致这个问题,如何debugging呢?

你是序列化到一个文件? 最可能的原因(以我的经验)是,你覆盖了一个现有的文件,但没有截断它; 即它 200字节; 你已经重写了它,但只有182个字节。 现在,stream的末尾有18个字节的垃圾被绊倒了。 重写协议缓冲区时,文件必须被截断。 你可以用FileMode来做到这一点:

 using(var file = new FileStream(path, FileMode.Truncate)) { // write } 

或者写入数据之后通过SetLength

 file.SetLength(file.Position); 

其他可能的原因

您(意外)将stream反序列化为与序列化不同的types。 值得仔细检查双方的谈话,以确保不会发生。

由于堆栈跟踪引用这个StackOverflow的问题,我想我会指出,如果你(意外)将stream反序列化为不同于序列化的types,你也可以接收到这个exception。 所以值得仔细检查双方的谈话,以确保这不会发生。

这也可能是由于尝试将多个protobuf消息写入单个stream而造成的。 解决scheme是使用SerializeWithLengthPrefix和DeserializeWithLengthPrefix。


为何发生这种情况

Protobuf规范支持相当less量的线型(二进制存储格式)和数据types(.NET等数据types)。 此外,这不是1:1,也不是1:多个或多个:1 – 单个线型可用于多种数据types,而单个数据types可通过多种线型中的任何一种进行编码。 因此,除非你已经知道scema,否则你不能完全理解protobuf片段,所以你知道如何解释每个值。 当你读取一个Int32数据types时,支持的线types可能是“varint”,“fixed32”和“fixed64”,在读取String数据types时,唯一支持的线types是“串”。

如果数据types和导线types之间没有兼容的映射,那么数据将无法读取,并且会引发此错误。

现在让我们来看看为什么在这里出现这种情况:

 [ProtoContract] public class Data1 { [ProtoMember(1, IsRequired=true)] public int A { get; set; } } [ProtoContract] public class Data2 { [ProtoMember(1, IsRequired = true)] public string B { get; set; } } class Program { static void Main(string[] args) { var d1 = new Data1 { A = 1}; var d2 = new Data2 { B = "Hello" }; var ms = new MemoryStream(); Serializer.Serialize(ms, d1); Serializer.Serialize(ms, d2); ms.Position = 0; var d3 = Serializer.Deserialize<Data1>(ms); // This will fail var d4 = Serializer.Deserialize<Data2>(ms); Console.WriteLine("{0} {1}", d3, d4); } } 

在上面,两条消息直接写在对方之后。 并发症是:protobuf是一个可附加格式,附加意思是“合并”。 protobuf消息不知道自己的长度 ,所以读取消息的默认方式是:读取直到EOF。 但是,这里我们附加了两种不同的types。 如果我们读回来,它不知道我们什么时候读完第一条消息,所以它一直在阅读。 当它从第二条消息得到数据时,我们发现自己正在读取一个“string”线型,但是我们仍然试图填充一个Data1实例,其中的成员1是Int32 。 “string”和Int32之间没有映射,所以爆炸了。

*WithLengthPrefix方法允许序列化器知道每个消息完成的地方; 因此,如果我们使用*WithLengthPrefix序列化Data1Data2 ,然后使用*WithLengthPrefix方法对Data1Data2进行反序列化,则它将在两个实例之间正确地分割传入数据,只将正确的值读入正确的对象。

此外,当存储这样的异构数据时,您可能需要为每个类另外分配一个不同的字段编号(通过*WithLengthPrefix ) 这提供了更大的可见性,哪些types正在被反序列化。 Serializer.NonGeneric还有一个方法可以用来反序列化数据, 而不需要事先知道我们正在反序列化什么

 // Data1 is "1", Data2 is "2" Serializer.SerializeWithLengthPrefix(ms, d1, PrefixStyle.Base128, 1); Serializer.SerializeWithLengthPrefix(ms, d2, PrefixStyle.Base128, 2); ms.Position = 0; var lookup = new Dictionary<int,Type> { {1, typeof(Data1)}, {2,typeof(Data2)}}; object obj; while (Serializer.NonGeneric.TryDeserializeWithLengthPrefix(ms, PrefixStyle.Base128, fieldNum => lookup[fieldNum], out obj)) { Console.WriteLine(obj); // writes Data1 on the first iteration, // and Data2 on the second iteration } 

以前的答案已经比我更好地解释了这个问题。 我只是想添加一个更简单的方法来重现exception。

如果序列化ProtoMember的types与反序列化期间的预期types不同,也会发生此错误。

例如,如果客户端发送以下消息:

 public class DummyRequest { [ProtoMember(1)] public int Foo{ get; set; } } 

但是服务器将消息反序列化成以下类:

 public class DummyRequest { [ProtoMember(1)] public string Foo{ get; set; } } 

那么这将导致这种情况稍有误导性的错误信息

ProtoBuf.ProtoException:线型无效; 这通常意味着你已经覆盖了一个文件而不截断或设置长度

如果属性名称改变,甚至会发生。 假设客户端发送了以下内容:

 public class DummyRequest { [ProtoMember(1)] public int Bar{ get; set; } } 

这仍然会导致服务器将int Bar反序列化为string Foo ,这会导致相同的ProtoBuf.ProtoException

我希望这有助于debugging他们的应用程序。

另外检查一下,你所有的子类都有[ProtoContract]属性。 有时,如果你有丰富的DTO,你可能会错过它。

如果您使用的是SerializeWithLengthPrefix,请注意,将objecttypes的投射实例打破反序列化代码并导致ProtoBuf.ProtoException : Invalid wire-type

 using (var ms = new MemoryStream()) { var msg = new Message(); Serializer.SerializeWithLengthPrefix(ms, (object)msg, PrefixStyle.Base128); // Casting msg to object breaks the deserialization code. ms.Position = 0; Serializer.DeserializeWithLengthPrefix<Message>(ms, PrefixStyle.Base128) } 

使用不正确的Encodingtypes将字节转换为string时,我遇到过这个问题。

需要使用Encoding.Default而不是Encoding.UTF8

 using (var ms = new MemoryStream()) { Serializer.Serialize(ms, obj); var bytes = ms.ToArray(); str = Encoding.Default.GetString(bytes); }