Al usar Protobuf-net, de repente recibí una excepción sobre un tipo de cable desconocido

Resuelto asked hace 14 años • 10 respuestas

(Esta es una nueva publicación de una pregunta que vi en mi RSS, pero que el OP eliminó. La volví a agregar porque vi esta pregunta varias veces en diferentes lugares; wiki para "bueno forma")

De repente, recibo un mensaje ProtoExceptional deserializar y el mensaje es: tipo de cable desconocido 6

  • ¿Qué es un tipo de cable?
  • ¿Cuáles son los diferentes valores de tipo de cable y su descripción?
  • Sospecho que un campo está causando el problema, ¿cómo depurarlo?
 avatar Jan 28 '10 14:01
Aceptado

Lo primero que hay que comprobar:

¿LOS DATOS DE ENTRADA SON DATOS PROTOBUF? Si intenta analizar otro formato (json, xml, csv, formateador binario), o simplemente datos rotos (una página de texto de marcador de posición html de "error interno del servidor", por ejemplo), entonces no funcionará .


¿Qué es un tipo de cable?

Es una bandera de 3 bits que le indica (en términos generales; después de todo, solo son 3 bits) cómo se verán los siguientes datos.

Cada campo en los buffers de protocolo tiene como prefijo un encabezado que le indica qué campo (número) representa y qué tipo de datos vendrán a continuación; Este "qué tipo de datos" es esencial para respaldar el caso en el que hay datos inesperados en la secuencia (por ejemplo, ha agregado campos al tipo de datos en un extremo), ya que le permite al serializador saber cómo leer más allá de eso. datos (o almacenarlos para el viaje de ida y vuelta si es necesario).

¿Cuáles son los diferentes valores de tipo de cable y su descripción?

  • 0: entero de longitud variante (hasta 64 bits): codificado en base 128 con el MSB que indica continuación (se utiliza como valor predeterminado para tipos de enteros, incluidas enumeraciones)
  • 1: 64 bits: 8 bytes de datos (utilizados para doubleo opcionalmente para long/ ulong)
  • 2: longitud con prefijo: primero lea un número entero usando codificación de longitud variante; esto le indica cuántos bytes de datos siguen (usados ​​para cadenas, byte[]matrices "empaquetadas" y como valor predeterminado para propiedades/listas de objetos secundarios)
  • 3: "grupo de inicio": un mecanismo alternativo para codificar objetos secundarios que utiliza etiquetas de inicio/fin. Google lo ha desaprobado en gran medida; es más costoso omitir un campo de objeto secundario completo, ya que no se puede simplemente "buscar" más allá de lo inesperado. objeto
  • 4: "grupo final" - hermanado con 3
  • 5: 32 bits: 4 bytes de datos (usados ​​para float, o opcionalmente para int/ uinty otros tipos de enteros pequeños)

Sospecho que un campo está causando el problema, ¿cómo depurarlo?

¿Estás serializando en un archivo? La causa más probable (en mi experiencia) es que haya sobrescrito un archivo existente, pero no lo haya truncado; es decir, eran 200 bytes; lo ha reescrito, pero con sólo 182 bytes. Ahora hay 18 bytes de basura al final de tu flujo que lo están haciendo tropezar. Los archivos deben truncarse al reescribir los buffers de protocolo. Puedes hacer esto con FileMode:

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

o alternativamente mediante SetLength después de escribir tus datos:

file.SetLength(file.Position);

Otra posible causa

Está (accidentalmente) deserializando una secuencia en un tipo diferente al que se serializó. Vale la pena volver a verificar ambos lados de la conversación para asegurarse de que esto no suceda.

Marc Gravell avatar Jan 28 '2010 07:01 Marc Gravell

Dado que el seguimiento de la pila hace referencia a esta pregunta de StackOverflow, pensé en señalar que también puede recibir esta excepción si (accidentalmente) deserializa una secuencia en un tipo diferente al que se serializó. Por lo tanto, vale la pena volver a verificar ambos lados de la conversación para asegurarse de que esto no esté sucediendo.

 avatar Jun 15 '2013 21:06

Esto también puede deberse a un intento de escribir más de un mensaje protobuf en una única secuencia. La solución es utilizar SerializeWithLengthPrefix y DeserializeWithLengthPrefix.


Por qué sucede esto:

La especificación protobuf admite una cantidad bastante pequeña de tipos de cables (los formatos de almacenamiento binario) y tipos de datos (los tipos de datos .NET, etc.). Además, esto no es 1:1, ni es 1:muchos o muchos:1: se puede usar un solo tipo de cable para múltiples tipos de datos, y un solo tipo de datos se puede codificar a través de cualquiera de los múltiples tipos de cables. . Como consecuencia, no puedes comprender completamente un fragmento de protobuf a menos que ya conozcas el scema, por lo que sabes cómo interpretar cada valor. Cuando, por ejemplo, lee un Int32tipo de datos, los tipos de cable admitidos pueden ser "varint", "fixed32" y "fixed64", mientras que cuando lee un Stringtipo de datos, el único tipo de cable admitido es "string ".

Si no hay un mapa compatible entre el tipo de datos y el tipo de cable, entonces los datos no se pueden leer y se genera este error.

Ahora veamos por qué ocurre esto en el siguiente escenario:

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

En lo anterior, se escriben dos mensajes uno detrás del otro. La complicación es: protobuf es un formato que se puede agregar, y agregar significa "fusionar". Un mensaje protobuf no conoce su propia longitud , por lo que la forma predeterminada de leer un mensaje es: leer hasta EOF. Sin embargo, aquí hemos añadido dos tipos diferentes . Si volvemos a leer esto, no sabe cuándo hemos terminado de leer el primer mensaje, por lo que continúa leyendo. Cuando llegamos a los datos del segundo mensaje, nos encontramos leyendo un cable de tipo "cadena", pero todavía estamos intentando completar una Data1instancia, para la cual el miembro 1 es un archivo Int32. No hay ningún mapa entre "cadena" y Int32, por lo que explota.

Los *WithLengthPrefixmétodos permiten al serializador saber dónde termina cada mensaje; entonces, si serializamos a Data1y Data2usando *WithLengthPrefix, luego deserializamos a Data1y a Data2usando los *WithLengthPrefixmétodos, entonces divide correctamente los datos entrantes entre las dos instancias, leyendo solo el valor correcto en el objeto correcto.

Además, al almacenar datos heterogéneos como este, es posible que desee asignar adicionalmente (a través de *WithLengthPrefix) un número de campo diferente a cada clase; esto proporciona una mayor visibilidad de qué tipo se está deserializando. También existe un método Serializer.NonGenericque luego se puede utilizar para deserializar los datos sin necesidad de saber de antemano qué estamos deserializando :

// 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
}
Marc Gravell avatar Sep 13 '2012 09:09 Marc Gravell