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

Mar*_*ell 60 c# protobuf-net

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

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

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

Mar*_*ell 56

首先要检查:

是输入数据PROTOBUF数据?如果您尝试解析另一种格式(json,xml,csv,binary-formatter),或者只是破坏数据(例如"内部服务器错误"html占位符文本页面),那么它将无法工作.


什么是线型?

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

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

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

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

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

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

using(var file = new FileStream(path, FileMode.Truncate)) {
    // write
}
Run Code Online (Sandbox Code Playgroud)

或者SetLength 写完你的数据之后:

file.SetLength(file.Position);
Run Code Online (Sandbox Code Playgroud)

其他可能的原因

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

  • +1 Nigel,领导protobuf-net的创建者到protobuf specs :),就像一个老板!我得到了例外,但它与文件无关.这是因为我在客户端更新了一个protobuffed对象,忘了发布到Web服务器.(不确定这是否值得添加到可能的原因) (17认同)
  • @MarcGravell只是跟进你的评论.当发送方使用类`class Foo {DateTime OccurredOn {get; 组; `和接收方有`class Foo {long OccurredOn {get; 组; }.注意对象类型之间的区别:`DateTime` vs`long`.不确定是否要解决这个问题,但这支持@Joe的发现.我使用protobuf-net 2.0.0.668.我通过在两端使用相同的类型来修复它(这是有意义的),但在这种情况下的例外与文件无关. (2认同)

Kir*_*oll 41

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

  • 公平点 - 可能 - 不,绝对 - 首先要检查. (9认同)
  • 是的,这就是我不小心做的 - 改变了现有领域的类型.嗯,这是一个非常明显的错误,但是我花了几分钟才意识到我做错了什么(需要检查一些原型文件和东西的差异).有没有办法获得完整的反序列化日志?它可以很快让我知道在哪里寻找破碎的场地. (3认同)

Chr*_*000 10

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


为什么会这样:

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

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

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

[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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在上面,两个消息直接写在彼此之后.复杂的是:protobuf是一种可附加格式,附加意思是"合并".protobuf消息不知道自己的长度,因此读取消息的默认方式是:读取直到EOF.但是,这里我们附加了两种不同的类型.如果我们回过头来看,它不知道我们何时读完第一条消息,所以它一直在阅读.当它从第二条消息中获取数据时,我们发现自己正在读取一个"字符串"有线类型,但我们仍在尝试填充一个Data1实例,其中成员1是一个实例Int32."string"之间没有映射Int32,因此它会爆炸.

这些*WithLengthPrefix方法允许序列化器知道每条消息的完成位置; 所以,如果我们序列化Data1Data2使用*WithLengthPrefix,然后使用方法反序列化a Data1和a ,那么它正确地在两个实例之间拆分传入数据,只将正确的值读入正确的对象.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
}
Run Code Online (Sandbox Code Playgroud)

  • 嗯....不,这不应该导致这个错误.protobuf规范*被设计为*可附加,因此写第二个对象将产生完全有效且合法的protobuf流,因此**不应该**导致此错误(指的是损坏的流).`*WithLengthPrefix`的意义是*没有*它,你绝对无法知道第一条消息的结束和第二条消息的开始.如果您有一个示例显示正在写入的多条消息导致此错误,我非常希望看到它. (2认同)

Tob*_*ias 5

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

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

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

public class DummyRequest
{
    [ProtoMember(1)]
    public int Foo{ get; set; }
}
Run Code Online (Sandbox Code Playgroud)

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

public class DummyRequest
{
    [ProtoMember(1)]
    public string Foo{ get; set; }
}
Run Code Online (Sandbox Code Playgroud)

那么这将导致这种情况下稍微误导性的错误消息

ProtoBuf.ProtoException:线类型无效;这通常意味着您在没有截断或设置长度的情况下覆盖了文件

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

public class DummyRequest
{
    [ProtoMember(1)]
    public int Bar{ get; set; }
}
Run Code Online (Sandbox Code Playgroud)

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

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