Как проанализировать содержимое потока двоичного сериализации?
-
27-09-2019 - |
Вопрос
Я использую двоичную сериализацию (двоичный дискформатор) как временный механизм для хранения информации о состоянии в файле для относительно сложной (игровой) структуры объекта; Файлы выходят много Больше, чем я ожидаю, и моя структура данных включает рекурсивные ссылки - поэтому мне интересно, фактически хранит ли двоичный бинартерий, или моим основным «количеством объектов и значений», который я должен иметь «арифментич» база или где еще превышает чрезмерный размер.
Поиск по переполнению стека Я смог найти спецификацию для формата двоичного удаления 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, мы получаем что-то подобное:
Давайте интерпретировать данные пример результатов:
Согласно вышеупомянутой спецификации (вот прямая ссылка на PDF: MS-NRBF] .pdf) каждая запись в потоке идентифицируется RecordTypeEnumeration
. Отказ Раздел 2.1.2.1 RecordTypeNumeration
состояния:
Это перечисление идентифицирует тип записи. Каждая запись (за исключением зависимости от MemberPrimityticpted) начинается с перечисления типа записи. Размер перечисления - один байт.
SerializateHeaderRecord:
Так что, если мы оглядываемся на данные, которые мы получили, мы можем начать интерпретацию первого байта:
Как указано в 2.1.2.1 RecordTypeEnumeration
Значение 0
Определяет SerializationHeaderRecord
который указан в 2.6.1 SerializationHeaderRecord
:
Запись SerializateHeerRecord должна быть первой записью в двоичной сериализации. Эта запись имеет основную и второстепенную версию формата и идентификаторы верхнего объекта и заголовки.
Это состоит из:
- RecordTypenum (1 байт)
- PORTID (4 байта)
- Headerid (4 байта)
- Майорверсия (4 байта)
- MinalVersion (4 байта)
С этим знанием мы можем интерпретировать запись, содержащую 17 байт:
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
. Отказ Поскольку последняя запись завершена, мы должны предположить, что начинается новый.
Давайте интерпретировать следующий байт:
Как мы видим, в нашем примере 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
. Отказ С этим знанием мы можем продолжить интерпретацию байтов в потоке:
0C
представляет собой RecordTypeEnumeration
который идентифицирует BinaryLibrary
записывать.
02 00 00 00
представляет собой LibraryId
который 2
в нашем случае.
Теперь LengthPrefixedString
следует:
42
представляет информацию о длине LengthPrefixedString
который содержит LibraryName
.
В нашем случае информация о длине 42
(Десятичная 66) Расскажите нам, что нам нужно прочитать следующие 66 байтов и интерпретировать их как LibraryName
.
Как уже говорилось, строка UTF-8
закодирован, поэтому результат вышеперечисленных байтов будет что-то вроде: _WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ClasswithMembersandTypes:
Опять же, запись завершена, поэтому мы интерпретируем 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
поле.)
Вернуться к необработанным данным, шаг за шагом:
01 00 00 00
представляет собой ObjectId
. Отказ Мы уже видели это, это было указано как RootId
в SerializationHeaderRecord
.
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
как название пространства имен.
02 00 00 00
представляет собой MemberCount
, это говорит нам, что 2 члена, оба представлены с LengthPrefixedString
будет следовать.
Имя первого участника:
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
.
Имя второго участника:
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
записывать:
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 байтами:
02 00 00 00
представляет собой LibraryId
который 2.
Ценности:
Как указано в 2.3 Class Records
:
Значения членов класса должны быть сериализованы в качестве записей, которые следуют этой записи, как указано в разделе 2.7. Порядок записей должен соответствовать порядку пользовательских данных, как указано в структуре ClassInfo (раздел 2.3.1.1).
Вот почему теперь мы можем ожидать ценностей членов.
Давайте посмотрим на последние несколько байтов:
06
Определяет АН BinaryObjectString
. Отказ Это представляет ценность нашего SomeString
Собственность (то <SomeString>k__BackingField
если быть точным).
В соответствии с 2.5.7 BinaryObjectString
это содержит:
- RecordTypenum (1 байт)
- ObjectID (4 байта)
- Значение (переменная длина, представленная как
LengthPrefixedString
)
Так что зная, что мы можем четко идентифицировать, что
03 00 00 00
представляет собой ObjectId
.
03 61 62 63
представляет собой Value
куда 03
это длина сама струны и 61 62 63
являются байтами содержания, которые переводят на abc
.
Надеюсь, вы можете вспомнить, что был второй член, Int32
. Отказ Зная, что то Int32
представлено, используя 4 байта, мы можем заключить, что
Должно быть Value
нашего второго участника. 7B
шестнадцатеричные равны 123
Десятичный, который, похоже, подходит для нашего примера кода.
Так вот полный ClassWithMembersAndTypes
записывать:
Сообщение:
Наконец последний байт 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» из примера кода, вы увидите, что у него есть много метаданных. Вот почему я рекомендовал вам создать свой собственный механизм сериализации.
Извините за то, что быть побочным.
Может быть, вы можете запустить вашу программу в режиме отладки и попробуйте добавить контрольную точку.
Если это невозможно из-за размера игр или других зависимостей, вы всегда можете погладить простое / небольшое приложение, которое включает в себя код десериализации и заглянуть из режима отладки.