web-dev-qa-db-ja.com

Protobuf-netを使用して、未知のワイヤタイプに関する例外が突然発生しました

(これはRSSで見た質問の再投稿ですが、OPによって削除されました。この質問がさまざまな場所で何度も尋ねられたのを見て、それを再追加しました。形")

突然、逆シリアル化するときにProtoExceptionを受け取り、メッセージは次のとおりです:unknown wire-type 6

  • ワイヤータイプとは何ですか?
  • 異なるワイヤタイプの値とその説明は何ですか?
  • フィールドが問題を引き起こしていると思われますが、これをデバッグする方法は?
57
Marc Gravell

最初に確認すること:

入力データはプロトバッファデータですか?別の形式(json、xml、csv、binary-formatter)、または単に破損したデータ(「内部サーバーエラー」などのhtmlプレースホルダーテキストページ)を解析しようとすると、動作しません


ワイヤータイプとは何ですか?

これは、次のデータがどのようなものかを(大まかに言えば3ビットだけです)伝える3ビットのフラグです。

プロトコルバッファの各フィールドの先頭にはヘッダーが付いており、ヘッダーが表すフィールド(番号)と次に来るデータのタイプを示します。この「どのタイプのデータ」は、予期しないデータがストリームにある場合をサポートするために不可欠です(たとえば、一方の端でデータタイプにフィールドを追加しました) 、シリアライザーがそのデータを過ぎて読み込む方法を知ることができるようにするため(または必要に応じてラウンドトリップのために保存するため)。

異なるワイヤタイプの値とその説明は何ですか?

  • 0:可変長整数(最大64ビット)-継続を示すMSBでエンコードされたbase-128(enumを含む整数型のデフォルトとして使用)
  • 1:64ビット-8バイトのデータ(doubleに使用、または選択的にlong/ulongに使用)
  • 2:length-prefixed-最初に可変長エンコードを使用して整数を読み取ります。これは、続くデータのバイト数を示します(文字列、byte[]、「パック」配列、および子オブジェクトのプロパティ/リストのデフォルトとして)
  • 3:「開始グループ」-開始/終了タグを使用する子オブジェクトをエンコードするための代替メカニズム-主にGoogleによって非推奨。予期しないものを「シーク」することができないため、子オブジェクトフィールド全体をスキップする方が費用がかかります。対象
  • 4:「グループ終了」-3とツイン
  • 5:32ビット-4バイトのデータ(floatに使用、またはelectivelyint/uintおよびその他の小さな整数型に使用)

フィールドが問題を引き起こしていると思われますが、これをデバッグする方法は?

ファイルにシリアル化していますか? 最も可能性の高い原因(私の経験では)は、既存のファイルを上書きしたが、切り捨てていないことです。つまり、itwas200バイト;書き直しましたが、182バイトしかありません。これで、ストリームを終了するストリームの最後に18バイトのガベージがあります。プロトコルバッファを書き換えるときは、ファイルを切り捨てる必要があります。 FileModeでこれを行うことができます:

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

または、SetLengthafterデータの書き込み後:

file.SetLength(file.Position);

その他の考えられる原因

(誤って)ストリームをシリアル化されたものとは異なるタイプに逆シリアル化しています。これが行われていないことを確認するために、会話の両側を再確認する価値があります。

55
Marc Gravell

スタックトレースはこのStackOverflowの質問を参照するため、ストリームを(誤って)シリアル化されたものとは異なる型に逆シリアル化した場合にも、この例外を受け取ることができると指摘したいと思います。そのため、会話の両側を再確認して、これが発生していないことを確認する価値があります。

39
Kirk Woll

これは、1つのストリームに複数のprotobufメッセージを書き込もうとした場合にも発生する可能性があります。解決策は、SerializeWithLengthPrefixとDeserializeWithLengthPrefixを使用することです。


これが起こる理由:

Protobuf仕様は、かなり少数のワイヤタイプ(バイナリストレージ形式)とデータタイプ(.NETなどのデータタイプ)をサポートしています。さらに、これは1:1ではなく、1対多または多対1でもありません-単一のワイヤタイプを複数のデータタイプに使用でき、単一のデータタイプを複数のワイヤタイプのいずれかでエンコードできます。結果として、あなたはcannotスキーマをすでに知っていない限りprotobufフラグメントを完全に理解しているので、各値の解釈方法を知っています。たとえば、Int32データ型を読み取る場合、サポートされるワイヤー型は「varint」、「fixed32」、および「fixed64」であり、Stringデータ型を読み取る場合、サポートされる唯一のワイヤタイプは「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);
    }
}

上記では、2つのメッセージがそれぞれの直後に書き込まれます。複雑な点は次のとおりです。protobufは追加可能な形式で、追加は「マージ」を意味します。 protobufメッセージ独自の長さを知らないであるため、メッセージのデフォルトの読み取り方法は、EOFまで読み取ります。ただし、ここでは2つのdifferentタイプを追加しています。これを読み返すと、最初のメッセージを読み終えたときにdoes not knowなので、読み続けます。 2番目のメッセージのデータに到達すると、「文字列」ワイヤータイプを読み取っていますが、Data1インスタンスを作成しようとしています。このインスタンスのメンバー1はInt32です。 「string」とInt32の間にマップがないため、爆発します。

*WithLengthPrefixメソッドallow各メッセージの終了位置を知るためのシリアライザー。したがって、Data1を使用してData2*WithLengthPrefixをシリアル化し、次にData1メソッドを使用してData2*WithLengthPrefixをシリアル化解除してから、 it 正しく 2つのインスタンス間で受信データを分割し、適切な値を適切なオブジェクトにのみ読み込みます。

さらに、このような異種データを保存する場合、mightを追加して(*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
}
9
Chriseyre2000

以前の回答はすでに私ができる以上に問題を説明しています。例外を再現するためのさらに簡単な方法を追加したいだけです。

このエラーは、シリアル化された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; }
}

これにより、サーバーはintBarstringFooに逆シリアル化し、同じProtoBuf.ProtoException

これがアプリケーションのデバッグに役立つことを願っています。

4
Tobias

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)
}
1
Chris Xue

また、すべてのサブクラスに[ProtoContract]属性があることを確認してください。リッチDTOがある場合、見逃すことがあります。

1
Tomasito

私の場合、これは次のようなものだったために起こりました。

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);
0
kamil-mrzyglod

不適切なEncoding型を使用してバイトを文字列に変換したり、文字列から変換したりするときにこの問題が発生しました。

Encoding.DefaultではなくEncoding.UTF8を使用する必要があります。

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