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

时间:2010-01-28 07:38:31

标签: c# protobuf-net

(这是我在RSS中看到的一个问题的重新发布,但被OP删除了。我重新添加了它,因为我在不同的地方已经多次看过这个问题; wiki为“好形式”)

突然,我在反序列化时收到ProtoException,消息是:unknown wire-type 6

  • 什么是线型?
  • 有哪些不同的线型值及其描述?
  • 我怀疑某个字段导致了问题,如何调试?

8 个答案:

答案 0 :(得分:52)

首先要检查:

是输入数据的原始数据吗?如果你尝试解析另一种格式(json,xml,csv,binary-formatter),或者只是破坏数据(例如“内部服务器错误”html占位符文本页面),那么它将不起作用


  

什么是线型?

它是一个3位标志,告诉它(广义上说,它毕竟只有3位)下一个数据是什么样的。

协议缓冲区中的每个字段都以一个标头为前缀,告诉它它代表哪个字段(数字), 接下来会有什么类型的数据;这种“什么类型的数据”对于支持这种情况至关重要 未预料到的数据位于流中(例如,您在一端向数据类型添加了字段),如 它让序列化器知道如何读取该数据(或者如果需要,将其存储为往返)。

  

有哪些不同的线型值及其描述?

  • 0:变量长度整数(最多64位) - 使用MSB指示连续的base-128编码(用作整数类型的默认值,包括枚举)
  • 1:64位 - 8个字节的数据(用于double,或选择用于long / ulong
  • 2:length-prefixed - 首先使用变长编码读取整数;这告诉你跟随多少字节数据(用于字符串,byte[],“打包”数组,以及作为子对象属性/列表的默认值)
  • 3:“开始组” - 一种用于编码使用开始/结束标记的子对象的替代机制 - 谷歌基本上不赞成,跳过整个子对象字段更加昂贵,因为你不能只是“寻找”过去一个意外的对象
  • 4:“结束组” - 与3
  • 结对
  • 5:32位 - 4个字节的数据(用于float,或选择用于int / uint和其他小整数类型< / LI>
  

我怀疑某个字段导致了问题,如何调试?

您是否序列化为文件? 最可能的原因(根据我的经验)是你已经覆盖了现有文件,但没有截断它;即它 200字节;你重写了它,但只有182个字节。现在流的末尾有18个字节的垃圾正在绊倒它。重写协议缓冲区时必须截断文件。您可以使用FileMode

执行此操作
using(var file = new FileStream(path, FileMode.Truncate)) {
    // write
}
在编写数据之后

SetLength

file.SetLength(file.Position);
  

其他可能的原因

您(意外地)将流反序列化为与序列化不同的类型。值得仔细检查对话的双方,以确保不会发生这种情况。

答案 1 :(得分:38)

由于堆栈跟踪引用了这个StackOverflow问题,我想我会指出,如果您(意外地)将流反序列化为与序列化不同的类型,您也可以收到此异常。所以值得仔细检查对话的双方,以确保不会发生这种情况。

答案 2 :(得分:9)

这也可能是由于尝试将多个protobuf消息写入单个流。解决方案是使用SerializeWithLengthPrefix和DeserializeWithLengthPrefix。


为什么会这样:

protobuf规范支持相当少量的线类型(二进制存储格式)和数据类型(.NET等数据类型)。此外,这不是1:1,也不是1:很多或很多:1 - 单个线型可用于多种数据类型,单个数据类型可通过多种线型中的任何一种进行编码。因此,除非您已经知道了scema,否则不能完全理解protobuf片段,因此您知道如何解释每个值。当你读取Int32数据类型时,支持的线型可能是&#34; varint&#34;,&#34; fixed32&#34;和&#34; fixed64&#34;,其中 - 当读取String数据类型时,唯一支持的线型是&#34; string&#34;。

如果数据类型和有线类型之间没有兼容的映射,则无法读取数据,并引发此错误。

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

[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是一种可附加的格式,附加含义&#34; merge&#34;。 protobuf消息不知道自己的长度,因此读取消息的默认方式是:读取直到EOF。但是,这里我们添加了两个 不同的 类型。如果我们回过头来看,当我们读完第一条消息后,就不知道,所以它一直在阅读。当它从第二条消息中获取数据时,我们发现自己正在读取&#34;字符串&#34;有线类型,但我们仍在尝试填充Data1实例,其成员1为Int32。 &#34; string&#34;之间没有地图。和Int32,所以爆炸。

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

此外,当存储这样的异构数据时,可能想要另外为每个类分配(通过*WithLengthPrefix)不同的字段编号;这样可以更好地了解正在反序列化的类型。 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
}

答案 3 :(得分:4)

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

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

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

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

我希望这可以帮助调试他们的应用程序。

答案 4 :(得分:1)

同时检查所有子类是否具有[ProtoContract]属性。有时候,当你有丰富的DTO时,你会错过它。

答案 5 :(得分:1)

如果您使用的是SerializeWithLengthPrefix,请注意,将实例转换为object类型会破坏反序列化代码并导致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)
}

答案 6 :(得分:0)

在使用不正确的Encoding类型转换字符串输入和输出字节时,我已经看到了这个问题。

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

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

答案 7 :(得分:0)

发生这种情况是因为我有这样的事情:

var ms = new MemoryStream();
Serializer.Serialize(ms, batch);

_queue.Add(Convert.ToBase64String(ms.ToArray()));

所以基本上我是将base64放入队列中,然后在用户端我有:

var stream = new MemoryStream(Encoding.UTF8.GetBytes(myQueueItem));
var batch = Serializer.Deserialize<List<EventData>>(stream);

因此,尽管每个 myQueueItem 的类型都是正确的,但我忘记了我转换了字符串。解决方案是再次将其转换:

var bytes = Convert.FromBase64String(myQueueItem);
var stream = new MemoryStream(bytes);
var batch = Serializer.Deserialize<List<EventData>>(stream);