(这是我在 RSS 中看到的一个问题的重新发布,但被 OP 删除了。我重新添加了它,因为我在不同的地方看到这个问题被问了好几次;wiki for "good形式”)
突然,我在反序列化时收到一条ProtoException
消息:unknown wire-type 6
- 什么是线型?
- 有哪些不同的线型值及其描述?
- 我怀疑是某个字段导致了问题,如何调试呢?
(这是我在 RSS 中看到的一个问题的重新发布,但被 OP 删除了。我重新添加了它,因为我在不同的地方看到这个问题被问了好几次;wiki for "good形式”)
突然,我在反序列化时收到一条ProtoException
消息:unknown wire-type 6
首先要检查:
输入数据是 PROTOBUF 数据吗?如果您尝试解析另一种格式(json、xml、csv、二进制格式化程序),或者只是破坏数据(例如,“内部服务器错误”html 占位符文本页面),那么它将无法正常工作。
什么是线型?
它是一个 3 位标志,它告诉它(从广义上讲;它毕竟只有 3 位)下一个数据是什么样的。
协议缓冲区中的每个字段都以一个标头为前缀,该标头告诉它它代表哪个字段(数字),以及接下来会出现什么类型的数据;这个“什么类型的数据”对于支持流中存在 意外数据的情况至关重要(例如,您在一端向数据类型添加了字段),因为它让序列化程序知道如何读取过去的数据数据(或根据需要将其存储为往返)。
有哪些不同的线型值及其描述?
double
,或可选地用于long
/ ulong
)byte[]
“打包”数组,并作为子对象属性/列表的默认值)float
,或可选用于int
/uint
和其他小整数类型)我怀疑是某个字段导致了问题,如何调试呢?
你是序列化到文件吗?最可能的原因(根据我的经验)是您覆盖了现有文件,但没有截断它;即它是200字节;你已经重写了它,但只有 182 个字节。现在,流的末尾有 18 个字节的垃圾导致它出错。重写协议缓冲区时必须截断文件。你可以这样做FileMode
:
using(var file = new FileStream(path, FileMode.Truncate)) {
// write
}
或者SetLength
在写入数据之后:
file.SetLength(file.Position);
其他可能的原因
您(意外地)将流反序列化为与序列化不同的类型。值得仔细检查对话的双方以确保不会发生这种情况。
由于堆栈跟踪引用了这个 StackOverflow 问题,我想我会指出,如果您(意外)将流反序列化为与序列化不同的类型,您也会收到此异常。因此,值得仔细检查对话的双方以确保不会发生这种情况。
这也可能是由于尝试将多个 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
方法允许序列化程序知道每条消息在哪里结束;因此,如果我们序列化 aData1
和Data2
使用*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
}
以前的答案已经比我更好地解释了这个问题。我只想添加一种更简单的方法来重现异常。
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
.
我希望这有助于调试他们的应用程序的人。
还要检查所有子类都具有[ProtoContract]
属性的明显情况。有时,当您拥有丰富的 DTO 时,您可能会错过它。
如果您使用的是 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)
}
我在使用不正确的Encoding
类型将字节转换为字符串输入和输出时看到了这个问题。
需要用Encoding.Default
而不用Encoding.UTF8
。
using (var ms = new MemoryStream())
{
Serializer.Serialize(ms, obj);
var bytes = ms.ToArray();
str = Encoding.Default.GetString(bytes);
}
这发生在我的案例中,因为我有这样的事情:
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);