web-dev-qa-db-fra.com

En utilisant Protobuf-net, j'ai soudainement eu une exception concernant un type de fil inconnu

(Ceci est une nouvelle publication d'une question que j'ai vue dans mon RSS, mais qui a été supprimée par le PO. Je l'ai ajoutée de nouveau parce que j'ai vu cette question posée plusieurs fois à différents endroits. forme")

Soudainement, je reçois une ProtoException lors de la désérialisation et le message est le suivant: type de fil inconnu 6

  • Qu'est-ce qu'un type de fil?
  • Quelles sont les différentes valeurs de type de fil et leur description?
  • Je soupçonne qu'un champ est à l'origine du problème, comment le déboguer?
52
Marc Gravell

Première chose à vérifier:

EST-CE QUE LES DONNÉES D'ENTRÉE PROTOBUF? Si vous essayez d'analyser un autre format (json, xml, csv, formateur binaire) ou simplement des données cassées (une page de texte "erreur de serveur interne" html, par exemple), alors cela ne fonctionnera pas .


Qu'est-ce qu'un type de fil?

C'est un drapeau à 3 bits qui indique (en gros, ce n'est que 3 bits après tout) à quoi ressemblent les données suivantes.

Chaque champ dans les tampons de protocole est préfixé par un en-tête qui lui indique quel champ (nombre) il représente, .__ et quel type de données est à venir; ce "quel type de données" est essentiel pour prendre en charge le cas où les données non anticipées sont dans le flux (par exemple, vous avez ajouté des champs au type de données à une extrémité), commeit indique au sérialiseur comment lire au-delà de ces données (ou les stocker pour un aller-retour, si nécessaire).

Quelles sont les différentes valeurs de type de fil et leur description?

  • 0: entier de longueur variable (jusqu'à 64 bits) - base 128 codé avec le bit de poids fort indiquant la continuation (utilisé comme valeur par défaut pour les types de nombre entier, y compris les enums)
  • 1: 64 bits - 8 octets de données (utilisés pour double ou électivement pour long/ulong)
  • 2: préfixe de longueur - commence par lire un entier en utilisant un codage à longueur variable; cela vous indique combien d'octets de données suivent (utilisés pour les chaînes, les tableaux byte[], "emballés" et par défaut pour les propriétés/listes d'objets enfants)
  • 3: "groupe de démarrage" - un mécanisme alternatif de codage des objets enfants qui utilise des balises de début/fin - largement déconseillé par Google, il est plus coûteux d'ignorer tout un champ d'objet enfant, car il est impossible de "rechercher" une passé objet
  • 4: "groupe d'extrémité" - jumelé avec 3
  • 5: 32 bits - 4 octets de données (utilisés pour float ou électivement pour int/uint et d'autres types de petit entier)

Je soupçonne qu'un champ est à l'origine du problème, comment le déboguer?

Êtes-vous en train de sérialiser dans un fichier? La cause la plus probable (selon mon expérience) est que vous avez écrasé un fichier existant, mais que vous ne l'avez pas tronqué. c'est-à-dire qu'il était 200 octets; vous l'avez réécrit, mais avec seulement 182 octets. Il y a maintenant 18 octets d'ordures à la fin de votre flux qui le déclenchent. Les fichiers doivent être tronqués lors de la réécriture des tampons de protocole. Vous pouvez le faire avec FileMode:

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

ou alternativement par SetLengthaprès en écrivant vos données:

file.SetLength(file.Position);

Autre cause possible

Vous êtes (accidentellement) en train de désérialiser un flux dans un type différent de celui qui a été sérialisé. Il est utile de vérifier les deux côtés de la conversation pour vous assurer que cela ne se produit pas.

49
Marc Gravell

Comme la trace de pile fait référence à cette question StackOverflow, je pensais préciser que vous pouvez également recevoir cette exception si vous désérialisez (accidentellement) un flux dans un type différent de celui qui a été sérialisé. Il est donc intéressant de vérifier les deux côtés de la conversation pour vous assurer que cela ne se produit pas.

38
Kirk Woll

Cela peut aussi être causé par une tentative d'écriture de plusieurs messages protobuf dans un seul flux. La solution consiste à utiliser SerializeWithLengthPrefix et DeserializeWithLengthPrefix.


Pourquoi cela se produit:

La spécification protobuf prend en charge un nombre relativement restreint de types de fil (les formats de stockage binaires) et de types de données (les types de données .NET, etc.). De plus, il ne s'agit pas de 1: 1, ni de 1: plusieurs ou plusieurs: 1 - un seul type de fil peut être utilisé pour plusieurs types de données, et un seul type de données peut être codé via plusieurs types de fil. . En conséquence, vous ne pouvez pas pleinement comprendre un fragment de protobuf si vous ne connaissez pas déjà le scema, vous savez donc comment interpréter chaque valeur. Lorsque vous lisez, par exemple, un type de données Int32, les types de fil pris en charge peuvent être "varint", "fixed32" et "fixed64", où, lors de la lecture d'un type de données String, le seul type de fil pris en charge est "chaîne".

S'il n'y a pas de mappe compatible entre le type de données et le type de fil, les données ne peuvent pas être lues et cette erreur est générée.

Voyons maintenant pourquoi cela se produit dans le scénario suivant:

[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);
    }
}

Dans ce qui précède, deux messages sont écrits directement l'un après l'autre. La complication est la suivante: protobuf est un format annexable, avec append signifiant "fusion". Un message protobuf ne connaît pas sa longueur, le mode de lecture par défaut du message est le suivant: lire jusqu'à EOF. Cependant, nous avons ajouté ici deuxdifférentstypes. Si nous relisons ceci, ne sait pas quand nous avons fini de lire le premier message, il continue donc à lire. Quand il arrive aux données du deuxième message, nous nous trouvons en train de lire un type de fil "chaîne", mais nous essayons toujours de renseigner une instance Data1, pour laquelle le membre 1 est un Int32. Il n'y a pas de carte entre "string" et Int32, donc ça explose.

Les méthodes *WithLengthPrefixpermettent au sérialiseur de savoir où se termine chaque message; donc, si nous sérialisons un Data1 et un Data2 à l'aide du *WithLengthPrefix, puis désérialisons un Data1 et un Data2 à l'aide des méthodes *WithLengthPrefix, il correctement divise les données entrantes entre les deux instances, lisant uniquement la valeur correcte dans l'objet de droite .

De plus, lorsque vous stockez des données hétérogènes telles que celle-ci, vous pourriez vouloir également attribuer (via *WithLengthPrefix) un numéro de champ différent à chaque classe; cela donne une plus grande visibilité du type en cours de désérialisation. Il existe également une méthode dans Serializer.NonGeneric qui peut ensuite être utilisée pour désérialiser les données sans avoir besoin de savoir à l'avance ce que nous désérialisons:

// 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

Les réponses précédentes expliquent déjà le problème mieux que moi. Je veux juste ajouter un moyen encore plus simple de reproduire l'exception.

Cette erreur se produira également simplement si le type d'une ProtoMember sérialisée est différent du type attendu lors de la désérialisation.

Par exemple, si le client envoie le message suivant:

public class DummyRequest
{
    [ProtoMember(1)]
    public int Foo{ get; set; }
}

Mais le serveur désérialise le message dans la classe suivante:

public class DummyRequest
{
    [ProtoMember(1)]
    public string Foo{ get; set; }
}

Dans ce cas, le message d'erreur légèrement trompeur apparaît dans ce cas. 

ProtoBuf.ProtoException: type de fil invalide; cela signifie généralement que vous avez écrasé un fichier sans tronquer ni définir la longueur

Cela se produira même si le nom de la propriété a changé. Disons que le client a plutôt envoyé ce qui suit:

public class DummyRequest
{
    [ProtoMember(1)]
    public int Bar{ get; set; }
}

Cela entraînera toujours le serveur à désérialiser la intBar en stringFoo, ce qui entraînerait le même ProtoBuf.ProtoException.

J'espère que cela aide quelqu'un à déboguer son application.

4
Tobias

Si vous utilisez SerializeWithLengthPrefix, n'oubliez pas que le transfert de l'instance en type object rompt le code de désérialisation et provoque 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

Vérifiez également que toutes vos sous-classes ont l'attribut [ProtoContract]. Parfois, vous pouvez le manquer lorsque vous avez un DTO riche.

1
Tomasito

C'est arrivé dans mon cas parce que j'avais quelque chose comme ça:

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

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

Donc, fondamentalement, je mettais une base64 dans une file d’attente, puis, côté consommateur, j’avais:

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

Donc, bien que le type de chaque myQueueItem soit correct, j'ai oublié de convertir une chaîne. La solution était de le convertir une fois de plus:

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

J'ai constaté ce problème lors de l'utilisation du type incorrect Encoding pour convertir les octets entrant et sortant des chaînes. 

Besoin d'utiliser Encoding.Default et non pas Encoding.UTF8.

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