Как проанализировать содержимое потока двоичного сериализации?

StackOverflow https://stackoverflow.com/questions/3052202

Вопрос

Я использую двоичную сериализацию (двоичный дискформатор) как временный механизм для хранения информации о состоянии в файле для относительно сложной (игровой) структуры объекта; Файлы выходят много Больше, чем я ожидаю, и моя структура данных включает рекурсивные ссылки - поэтому мне интересно, фактически хранит ли двоичный бинартерий, или моим основным «количеством объектов и значений», который я должен иметь «арифментич» база или где еще превышает чрезмерный размер.

Поиск по переполнению стека Я смог найти спецификацию для формата двоичного удаления Microsoft:http://msdn.microsoft.com/en-us/library/cc236844( lot.10).aspx.

То, что я не могу найти, это какой-либо существующий зритель, который позволяет вам «заглянуть» в содержимое выходного файла бинарформирования - получить счетчики объекта и общие байты для различных типов объектов в файле и т. Д.;

Я чувствую, что это должно быть моим "Google-Fu", проходящим мне (что мало у меня) - может кто-нибудь помочь? Этот должен были сделаны раньше, верно ??


ОБНОВИТЬ: Я не мог найти его и не получил ответов, поэтому я поставил что-то относительно быстрое вместе (ссылка на загружаемое проект ниже); Я могу подтвердить, что BinaryFormatter не храняет несколько копий одного и того же объекта, но он напечатает довольно много метаданных в поток. Если вам нужно эффективное хранение, создайте собственные пользовательские методы сериализации.

Это было полезно?

Решение

Потому что это может быть, предстоит интерес для кого-то, я решил сделать этот пост о Как выглядит двоичный формат сериализованных объектов .NET и как мы можем правильно интерпретировать его?

Я основал все мои исследования на .NET Remoting: Двоинарный формат Структура данных Технические характеристики.



Пример класса:

Чтобы иметь рабочий пример, я создал простой класс A который содержит 2 свойства, одну строку и одно целое значение, они называются SomeString и SomeValue.

Класс A Похоже:

[Serializable()]
public class A
{
    public string SomeString
    {
        get;
        set;
    }

    public int SomeValue
    {
        get;
        set;
    }
}

Для сериализации я использовал BinaryFormatter конечно:

BinaryFormatter bf = new BinaryFormatter();
StreamWriter sw = new StreamWriter("test.txt");
bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 });
sw.Close();

Как видно, я прошел новый экземпляр класса A содержащий abc и 123 в качестве ценностей.



Пример Результаты данных:

Если мы посмотрим на сериализованный результат в редакторе Hex, мы получаем что-то подобное:

Example result data



Давайте интерпретировать данные пример результатов:

Согласно вышеупомянутой спецификации (вот прямая ссылка на PDF: MS-NRBF] .pdf) каждая запись в потоке идентифицируется RecordTypeEnumeration. Отказ Раздел 2.1.2.1 RecordTypeNumeration состояния:

Это перечисление идентифицирует тип записи. Каждая запись (за исключением зависимости от MemberPrimityticpted) начинается с перечисления типа записи. Размер перечисления - один байт.



SerializateHeaderRecord:

Так что, если мы оглядываемся на данные, которые мы получили, мы можем начать интерпретацию первого байта:

SerializationHeaderRecord_RecordTypeEnumeration

Как указано в 2.1.2.1 RecordTypeEnumeration Значение 0 Определяет SerializationHeaderRecord который указан в 2.6.1 SerializationHeaderRecord:

Запись SerializateHeerRecord должна быть первой записью в двоичной сериализации. Эта запись имеет основную и второстепенную версию формата и идентификаторы верхнего объекта и заголовки.

Это состоит из:

  • RecordTypenum (1 байт)
  • PORTID (4 байта)
  • Headerid (4 байта)
  • Майорверсия (4 байта)
  • MinalVersion (4 байта)



С этим знанием мы можем интерпретировать запись, содержащую 17 байт:

SerializationHeaderRecord_Complete

00 представляет собой RecordTypeEnumeration который SerializationHeaderRecord в нашем случае.

01 00 00 00 представляет собой RootId

Если ни в потоке сериализаций не присутствует ни одно двоичная запись, ни двоичная передача, значение этого поля должно содержать объект записи класса, массива или бинализированной застройки, содержащуюся в потоке сериализации.

Так что в нашем случае это должно быть ObjectId со значением 1 (Потому что данные сериализуются с использованием маленького эндапана), которые мы надеемся увидеть снова ;-)

FF FF FF FF представляет собой HeaderId

01 00 00 00 представляет собой MajorVersion

00 00 00 00 представляет собой MinorVersion



BinaryLibrary:

Как указано, каждая запись должна начать с RecordTypeEnumeration. Отказ Поскольку последняя запись завершена, мы должны предположить, что начинается новый.

Давайте интерпретировать следующий байт:

BinaryLibraryRecord_RecordTypeEnumeration

Как мы видим, в нашем примере SerializationHeaderRecord следует BinaryLibrary записывать:

Бинализируемая запись связывает ID32 ID (как указано в разделе [MS-DTYP] 2.2.22) с именем библиотеки. Это позволяет другим записям ссылаться на имя библиотеки с помощью идентификатора. Этот подход уменьшает размер провода, когда есть несколько записей, которые ссылаются на то же имя библиотеки.

Это состоит из:

  • RecordTypenum (1 байт)
  • Библиотека (4 байта)
  • Liblicname (переменное количество байтов (что является LengthPrefixedString))



Как указано в 2.1.1.6 LengthPrefixedString...

ProductPrefixedString представляет собой строковое значение. Строка префиксирована по длине кодированной строки UTF-8 в байтах. Длина кодируется в поле переменной длины с минимумом 1 байта и максимум 5 байтов. Чтобы минимизировать размер провода, длина кодируется как поле переменной длины.

В нашем простом примере длина всегда кодируется с использованием 1 byte. Отказ С этим знанием мы можем продолжить интерпретацию байтов в потоке:

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId

0C представляет собой RecordTypeEnumeration который идентифицирует BinaryLibrary записывать.

02 00 00 00 представляет собой LibraryId который 2 в нашем случае.



Теперь LengthPrefixedString следует:

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId_LibraryName

42 представляет информацию о длине LengthPrefixedString который содержит LibraryName.

В нашем случае информация о длине 42 (Десятичная 66) Расскажите нам, что нам нужно прочитать следующие 66 байтов и интерпретировать их как LibraryName.

Как уже говорилось, строка UTF-8 закодирован, поэтому результат вышеперечисленных байтов будет что-то вроде: _WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null



ClasswithMembersandTypes:

Опять же, запись завершена, поэтому мы интерпретируем RecordTypeEnumeration следующего:

ClassWithMembersAndTypesRecord_RecordTypeEnumeration

05 идентифицирует а ClassWithMembersAndTypes записывать. Раздел 2.3.2.1 ClassWithMembersAndTypes состояния:

Регистрация ClassWithMembersandTypes является самым словам в классных записях. Содержит метаданные о участниках, включая имена и отсоединения типов членов. Он также содержит идентификатор библиотеки, который ссылается на название библиотеки класса.

Это состоит из:

  • RecordTypenum (1 байт)
  • ClassInfo (переменное количество байтов)
  • Membertypeinfo (переменное количество байтов)
  • Библиотека (4 байта)



ClassInfo:

Как указано в 2.3.1.1 ClassInfo Рекорд состоит из:

  • ObjectID (4 байта)
  • Имя (переменное количество байтов (что еще раз LengthPrefixedString))
  • MemberCount (4 байта)
  • MemonseNames (что является последовательностью LengthPrefixedStringгде количество предметов должно быть равно значением, указанному в MemberCount поле.)



Вернуться к необработанным данным, шаг за шагом:

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId

01 00 00 00 представляет собой ObjectId. Отказ Мы уже видели это, это было указано как RootId в SerializationHeaderRecord.

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name

0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41 представляет собой Name класса, который представлен с помощью LengthPrefixedString. Отказ Как уже упоминалось, в нашем примере длина строки определяется 1 байтом, поэтому первый байт 0F Указывает, что 15 байтов должны быть прочитаны и декодированы с использованием UTF-8. Результат выглядит что-то подобное: StackOverFlow.A - так очевидно, я использовал StackOverFlow как название пространства имен.

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name_MemberCount

02 00 00 00 представляет собой MemberCount, это говорит нам, что 2 члена, оба представлены с LengthPrefixedStringбудет следовать.

Имя первого участника:ClassWithMembersAndTypesRecord_MemberNameOne

1B 3C 53 6F 6D 65 53 74 72 69 6E 67 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 представляет первый MemberName, 1B Опять же, длина строки, которая составляет 27 байтов в длину результатов в таком случае: <SomeString>k__BackingField.

Имя второго участника:ClassWithMembersAndTypesRecord_MemberNameTwo

1A 3C 53 6F 6D 65 56 61 6C 75 65 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 представляет вторую MemberName, 1A Указывает, что строка длиной 26 байтов. Это приводит к тому, что это так: <SomeValue>k__BackingField.



Membertypeinfo:

После ClassInfo то MemberTypeInfo следует.

Раздел 2.3.1.2 - MemberTypeInfo Государства, что структура содержит:

  • Бинартипиники (переменная по длине)

Последовательность нученных значений, представляющих типы участников, которые передаются. Массив должен:

  • Имейте одинаковое количество предметов, что и поле «Член» структуры ClassInfo.

  • Заказаны таким образом, чтобы двоичное число соответствовало имени участника в поле члена структуры ClassInfo.

  • Дополнительные данные (переменная по длине), в зависимости от BinaryTpeEnum Дополнительная информация может или не может присутствовать.

| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |

Поэтому принимая во внимание, что мы почти там ... мы ожидаем 2 BinaryTypeEnumeration значения (потому что у нас было 2 члена в MemberNames).



Снова, обратно в необработанные данные полного MemberTypeInfo записывать:

ClassWithMembersAndTypesRecord_MemberTypeInfo

01 представляет собой BinaryTypeEnumeration первого участника, согласно 2.1.2.2 BinaryTypeEnumeration мы можем ожидать String и он представлен с помощью LengthPrefixedString.

00 представляет собой BinaryTypeEnumeration второго участника, и снова, согласно спецификации, это Primitive. Отказ Как указано выше, Primitiveсопровождаются дополнительной информацией, в данном случае PrimitiveTypeEnumeration. Отказ Вот почему нам нужно прочитать следующий байт, который 08, соответствует этому с таблицей, указанной в 2.1.2.3 PrimitiveTypeEnumeration и быть удивленным, чтобы заметить, что мы можем ожидать Int32 который представлен 4 байтами, как указано в каком-то другом документе о базовых данных.



Библиотека:

После MemerTypeInfo то LibraryId Следует, он представлен 4 байтами:

ClassWithMembersAndTypesRecord_LibraryId

02 00 00 00 представляет собой LibraryId который 2.



Ценности:

Как указано в 2.3 Class Records:

Значения членов класса должны быть сериализованы в качестве записей, которые следуют этой записи, как указано в разделе 2.7. Порядок записей должен соответствовать порядку пользовательских данных, как указано в структуре ClassInfo (раздел 2.3.1.1).

Вот почему теперь мы можем ожидать ценностей членов.

Давайте посмотрим на последние несколько байтов:

BinaryObjectStringRecord_RecordTypeEnumeration

06 Определяет АН BinaryObjectString. Отказ Это представляет ценность нашего SomeString Собственность (то <SomeString>k__BackingField если быть точным).

В соответствии с 2.5.7 BinaryObjectString это содержит:

  • RecordTypenum (1 байт)
  • ObjectID (4 байта)
  • Значение (переменная длина, представленная как LengthPrefixedString)



Так что зная, что мы можем четко идентифицировать, что

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue

03 00 00 00 представляет собой ObjectId.

03 61 62 63 представляет собой Value куда 03 это длина сама струны и 61 62 63 являются байтами содержания, которые переводят на abc.

Надеюсь, вы можете вспомнить, что был второй член, Int32. Отказ Зная, что то Int32 представлено, используя 4 байта, мы можем заключить, что

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue_MemberTwoValue

Должно быть Value нашего второго участника. 7B шестнадцатеричные равны 123 Десятичный, который, похоже, подходит для нашего примера кода.

Так вот полный ClassWithMembersAndTypes записывать:ClassWithMembersAndTypesRecord_Complete



Сообщение:

MessageEnd_RecordTypeEnumeration

Наконец последний байт 0B представляет собой MessageEnd записывать.

Другие советы

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

Однако я хотел понять, что происходило в потоке, поэтому я записал (относительно) быстрый класс, который делает то, что я хотел:

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

Для меня недостаточно полезно, чтобы поставить его где-то видимым, как кодпроект, поэтому я просто бросил проект в ZIP-файле на моем сайте: http://www.architectshack.com/binaryserializationanalysis.ashx.

В моем конкретном случае получается, что проблема была в два года:

  • BinaryFormatter - очень многословна (это известно, я просто не осознавал степень)
  • У меня были проблемы в моем классе, оказалось, что я хранил объекты, которые я не хотел

Надеюсь, это поможет кому-то в какой-то момент!


Обновление: Ян Райт связался со мной с проблемой с исходным кодом, где он разбился, когда исходный объект (ы) содержал «десятичные» значения. Теперь это исправлено, и я использовал повод для перемещения кода в Github и предоставить ему (Permissive, BSD) лицензию.

Наша приложение работает массивными данными. Это может занять до 1-2 ГБ ОЗУ, как ваша игра. Мы встречаем о том же «хранилище нескольких копий тех же объектов». Также двоичная сериализация хранит слишком много метаданных. Когда он был впервые реализован, сериализованный файл занял около 1-2 ГБ. В настоящее время мне удалось уменьшить ценность - 50-100 МБ. Что мы сделали.

Краткий ответ - не используйте двоичную сериализацию .NET, создайте свой собственный механизм двоичного сериализации. У нас есть собственный класс BinaryFormatter, и Iserializable интерфейс (с двумя методами сериализации, десериализации).

Тот же объект не должен быть сериализован более одного раза. Мы сохраняем его уникальный идентификатор и восстановить объект из кэша.

Я могу поделиться некоторым кодом, если вы спросите.

РЕДАКТИРОВАТЬ: Кажется, вы правы. Смотрите следующий код - оно доказывает, что я ошибался.

[Serializable]
public class Item
{
    public string Data { get; set; }
}

[Serializable]
public class ItemHolder
{
    public Item Item1 { get; set; }

    public Item Item2 { get; set; }
}

public class Program
{
    public static void Main(params string[] args)
    {
        {
            Item item0 = new Item() { Data = "0000000000" };
            ItemHolder holderOneInstance = new ItemHolder() { Item1 = item0, Item2 = item0 };

            var fs0 = File.Create("temp-file0.txt");
            var formatter0 = new BinaryFormatter();
            formatter0.Serialize(fs0, holderOneInstance);
            fs0.Close();
            Console.WriteLine("One instance: " + new FileInfo(fs0.Name).Length); // 335
            //File.Delete(fs0.Name);
        }

        {
            Item item1 = new Item() { Data = "1111111111" };
            Item item2 = new Item() { Data = "2222222222" };
            ItemHolder holderTwoInstances = new ItemHolder() { Item1 = item1, Item2 = item2 };

            var fs1 = File.Create("temp-file1.txt");
            var formatter1 = new BinaryFormatter();
            formatter1.Serialize(fs1, holderTwoInstances);
            fs1.Close();
            Console.WriteLine("Two instances: " + new FileInfo(fs1.Name).Length); // 360
            //File.Delete(fs1.Name);
        }
    }
}

Выглядит как BinaryFormatter использует Object.equals, чтобы найти такие же объекты.

Вы когда-нибудь просматривали сгенерированные файлы? Если вы откроете «Temp-file0.txt» и «Temp-file1.txt» из примера кода, вы увидите, что у него есть много метаданных. Вот почему я рекомендовал вам создать свой собственный механизм сериализации.

Извините за то, что быть побочным.

Может быть, вы можете запустить вашу программу в режиме отладки и попробуйте добавить контрольную точку.

Если это невозможно из-за размера игр или других зависимостей, вы всегда можете погладить простое / небольшое приложение, которое включает в себя код десериализации и заглянуть из режима отладки.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top