74

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

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

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

8 回答 8

64

首先要检查:

输入数据是 PROTOBUF 数据吗?如果您尝试解析另一种格式(json、xml、csv、二进制格式化程序),或者只是破坏数据(例如,“内部服务器错误”html 占位符文本页面),那么它将无法正常工作


什么是线型?

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

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

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

  • 0:变长整数(最多 64 位)- base-128 编码,MSB 表示延续(用作整数类型的默认值,包括枚举)
  • 1:64 位 - 8 字节数据(用于double,或可选地用于long/ ulong
  • 2:length-prefixed——首先使用variant-length encoding读取一个整数;这告诉您后面有多少字节的数据(用于字符串、byte[]“打包”数组,并作为子对象属性/列表的默认值)
  • 3:“开始组”——一种使用开始/结束标签对子对象进行编码的替代机制——在很大程度上被谷歌弃用,跳过整个子对象字段会更昂贵,因为你不能只是“寻找”一个意想不到的结果目的
  • 4:“端组”——与 3 结对
  • 5:32 位 - 4 字节数据(用于float,或可选用于int/uint和其他小整数类型)

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

你是序列化到文件吗?最可能的原因(根据我的经验)是您覆盖了现有文件,但没有截断它;即它200字节;你已经重写了它,但只有 182 个字节。现在,流的末尾有 18 个字节的垃圾导致它出错。重写协议缓冲区时必须截断文件。你可以这样做FileMode

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

或者SetLength 写入数据之后:

file.SetLength(file.Position);

其他可能的原因

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

于 2010-01-28T07:38:48.513 回答
46

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

于 2013-06-15T21:39:37.093 回答
11

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


为什么会这样:

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

如果 data-type 和 wire-type 之间没有兼容的映射,则无法读取数据,并引发此错误。

现在让我们看看为什么会在此处的场景中发生这种情况:

[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。但是,这里我们附加了两种不同的类型。如果我们读回来,它不知道我们什么时候读完第一条消息,所以它一直在读。当它从第二条消息中获取数据时,我们发现自己正在读取“字符串”线型,但我们仍在尝试填充一个Data1实例,其中成员 1 是一个Int32. "string" 和 之间没有映射Int32,所以它会爆炸。

这些*WithLengthPrefix方法允许序列化程序知道每条消息在哪里结束;因此,如果我们序列化 aData1Data2使用*WithLengthPrefix,然后反序列化 aData1和 aData2使用*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
}
于 2012-09-13T09:16:39.960 回答
5

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

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.

我希望这有助于调试他们的应用程序的人。

于 2015-04-17T01:17:56.403 回答
1

还要检查所有子类都具有[ProtoContract]属性的明显情况。有时,当您拥有丰富的 DTO 时,您可能会错过它。

于 2014-06-16T10:59:43.677 回答
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)
}
于 2017-09-29T10:43:05.987 回答
1

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

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

using (var ms = new MemoryStream())
{
    Serializer.Serialize(ms, obj);
    var bytes = ms.ToArray();
    str = Encoding.Default.GetString(bytes);
}
于 2016-08-31T21:25:41.930 回答
1

这发生在我的案例中,因为我有这样的事情:

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);
于 2018-08-06T07:12:34.940 回答