Используя Protobuf-net, я внезапно получил исключение из-за неизвестного типа провода

(это повторная запись вопроса, который я видел в своем RSS, но который был удален OP. Я добавил его повторно, потому что я видел этот вопрос несколько раз в разных местах; для "хорошей формы" )

Неожиданно я получаю ProtoException при десериализации и сообщение: неизвестный тип проводки 6

  • Что такое проводной тип?
  • Каковы различные значения проводного типа и их описание?
  • Я подозреваю, что поле вызывает проблему, как отладить это?

Ответ 1

Первое, что нужно проверить:

ЯВЛЯЮТСЯ ДАННЫМИ ПРОТОБУЛА ВХОДНЫХ ДАННЫХ? Если вы попытаетесь разобрать другой формат (json, xml, csv, binary-formatter) или просто сломанные данные (например, текстовая страница с заголовком html "внутренняя ошибка сервера" ), то он не будет работать.


Что такое проводной тип?

Это 3-битный флаг, который сообщает ему (в широком смысле, всего 3 бита), как выглядят следующие данные.

Каждое поле в буферах протоколов имеет префикс заголовком, который сообщает ему, какое поле (номер) оно представляет, и какой тип данных будет дальше; это "какой тип данных" имеет важное значение для поддержки случая, когда непредвиденные данные находятся в потоке (например, вы добавили поля к типу данных на одном конце), так как он позволяет сериализатору знать, как читать прошлые данные (или хранить его для туда и обратно в случае необходимости).

Каковы различные значения типа проводника и их описание?

  • 0: целочисленное целое число (до 64 бит) - base-128, закодированное с MSB, указывающее продолжение (используемое по умолчанию для целочисленных типов, включая перечисления)
  • 1: 64-бит - 8 байтов данных (используется для double или по выбору для long/ulong)
  • 2: length-prefixed - сначала считывает целое число с использованием кодирования с расширением длины; это говорит вам, сколько байтов данных используется (используется для строк, byte[], "упакованных" массивов и по умолчанию для свойств/списков дочерних объектов)
  • 3: "start group" - альтернативный механизм для кодирования дочерних объектов, который использует теги start/end, во многом устаревшие от Google, более дорого пропустить целое поле дочернего объекта, поскольку вы не можете просто "искать", мимо неожиданного объекта
  • 4: "конечная группа" - побратимая с 3
  • 5: 32-бит - 4 байта данных (используется для float или по выбору для int/uint и других небольших целых типов)

Я подозреваю, что поле вызывает проблему, как отладить это?

Вы сериализуетесь в файл? Наиболее вероятной причиной (по моему опыту) является то, что вы перезаписали существующий файл, но не усекали его; то есть 200 байтов; вы переписали его, но только с 182 байтами. В конце вашего потока есть 18 байт мусора, которые его отключают. Файлы должны быть усечены при перезаписывании буферов протокола. Вы можете сделать это с помощью FileMode:

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

или, альтернативно, SetLength после записи ваших данных:

file.SetLength(file.Position);

Другая возможная причина

Вы (случайно) десериализируете поток в другой тип, чем сериализованный. Стоит дважды проверить обе стороны разговора, чтобы этого не происходило.

Ответ 2

Так как трассировка стека ссылается на этот вопрос StackOverflow, я подумал, что хочу указать, что вы также можете получить это исключение, если вы (случайно) десериализуете поток в другой тип, чем тот, который был сериализован. Поэтому стоит дважды проверить обе стороны разговора, чтобы этого не происходило.

Ответ 3

Это также может быть вызвано попыткой записать более одного сообщения protobuf в один поток. Решение состоит в использовании SerializeWithLengthPrefix и DeserializeWithLengthPrefix.


Почему это происходит:

Спецификация protobuf поддерживает довольно небольшое количество проводных типов (двоичные форматы хранения) и типы данных (типы данных .NET и т.д.). Кроме того, это не 1:1, равно 1: много или много: 1 - для разных типов данных может использоваться один проводной тип, а один тип данных может быть закодирован с помощью любого из нескольких типов проводов, Как следствие, вы не можете полностью понять фрагмент 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);
    }
}

В приведенном выше сообщении два сообщения записываются непосредственно друг за другом. Усложнение: protobuf - это добавочный формат с добавлением значения "merge". Сообщение protobuf не знает его собственной длины, поэтому способ чтения сообщения по умолчанию: читать до EOF. Однако здесь мы добавили два разных. Если мы прочтем это, он не знает, когда мы закончим читать первое сообщение, чтобы он продолжал читать. Когда он получает данные из второго сообщения, мы обнаруживаем, что читаем "строковый" тип проводника, но мы все еще пытаемся заполнить экземпляр Data1, для которого элемент 1 является Int32. Между "строкой" и Int32 нет карты, поэтому она взрывается.

Методы *WithLengthPrefix позволяют сериализатору знать, где заканчивается каждое сообщение; поэтому, если мы сериализуем Data1 и Data2 с помощью *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
}

Ответ 4

Предыдущие ответы уже объясняют проблему лучше, чем я могу. Я просто хочу добавить еще более простой способ воспроизвести исключение.

Эта ошибка также возникает, если тип сериализованного 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.

Надеюсь, это поможет кому-то отладить свое приложение.

Ответ 5

Также убедитесь, что все ваши подклассы имеют атрибут [ProtoContract]. Иногда вы можете пропустить это, когда у вас есть богатый DTO.

Ответ 6

Если вы используете SerializeWithLengthPrefix, помните, что экземпляр casting для типа 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)
}

Ответ 7

Я видел эту проблему при использовании неправильного типа Encoding для преобразования байтов в строки и из строк.

Необходимо использовать Encoding.Default, а не Encoding.UTF8.

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