Понимание сериализации

38

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

Проблема, которая у меня есть: не все ли переменные (например, примитивы intили составные объекты) уже представлены последовательностью байтов? (Конечно, потому что они хранятся в регистрах, памяти, на диске и т. Д.)

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

ddcz
источник
21
Сериализация может быть тривиальной для смежных объектов. Когда значение объекта представляется в виде графа указателя , все становится намного сложнее, особенно если в указанном графе есть циклы.
Чи
1
@chi: Ваше первое предложение немного вводит в заблуждение, учитывая, что смежность не имеет значения. У вас может быть график, который оказывается непрерывным в памяти и который все равно не поможет вам с его сериализацией, так как вам все равно придется (а) обнаружить, что он действительно является смежным, и (б) исправить указатели внутри. Я бы просто сказал вторую часть того, что вы сказали.
Мердад
@ Mehrdad Я согласен, что мой комментарий не совсем точен по причинам, которые вы упомянули. Возможно, лучше не использовать указатели / использовать указатели (даже если они не совсем точны)
Чи
7
Вы также должны беспокоиться о представлении на оборудовании. Если я сериализую int 4 bytesна моем PDP-11, а затем попытаюсь прочитать те же четыре байта в памяти на моем macbook, они не будут одинаковыми (из-за Endianes). Таким образом, вы должны нормализовать данные для представления, которое можно расшифровать (это сериализация). То, как вы сериализуете данные, также имеет компромисс между скоростью и гибкостью, понятными человеку / машине.
Мартин Йорк,
Что если вы используете Entity Framework со многими глубоко связанными навигационными свойствами? В одном случае вы можете захотеть сериализовать свойство навигации, но в другом оставить его пустым (потому что вы повторно загрузите этот фактический объект из базы данных на основе идентификатора, который находится в вашем сериализованном родительском объекте). Это только один пример. Здесь очень много.
ErikE

Ответы:

40

Если у вас сложная структура данных, ее представление в памяти обычно может быть разбросано по всей памяти. (Например, подумайте о бинарном дереве.)

Напротив, когда вы хотите записать его на диск, вы, вероятно, захотите иметь представление в виде (возможно, короткой) последовательности непрерывных байтов. Это то, что сериализация делает для вас.

DW
источник
27

Проблема, которую я имею: не все ли переменные (будь то примитивы типа int или составные объекты) уже представлены последовательностью байтов? (Конечно, потому что они хранятся в регистрах, памяти, на диске и т. Д.)

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

Рассмотрим граф объектов в C с узлами, определенными так:

struct Node {
    struct Node* parent;
    struct Node* someChild;
    struct Node* anotherLink;

    int value;
    char* label;
};

//

struct Node nodes[10] = {0};
nodes[5].parent = nodes[0];
nodes[0].someChild = calloc( 1, sizeof(struct Node) );
nodes[5].anotherLink = nodes[3];
for( size_t i = 3; i < 7; i++ ) {
    nodes[i].anotherLink = calloc( 1, sizeof(struct Node) );
}

Во время выполнения весь Nodeграф объектов будет разбросан по пространству памяти, и на один и тот же узел можно будет указывать множество разных узлов.

Вы не можете просто вывести память в файл / поток / диск и назвать ее сериализованной, потому что значения указателя (которые являются адресами памяти) не могут быть десериализованы (потому что эти области памяти могут быть уже заняты, когда вы загружаете дамп обратно в память). Другая проблема с простым дампом памяти заключается в том, что вы в конечном итоге будете хранить все виды неактуальных данных и неиспользуемого пространства - на x86 процесс имеет до 4 ГБ памяти, а ОС или MMU имеют только общее представление о том, какая память на самом деле имеет смысл или нет (в зависимости от страниц памяти, назначенных процессу), поэтому Notepad.exeвыгрузка 4 ГБ необработанных байтов на мой диск всякий раз, когда я хочу сохранить текстовый файл, кажется немного расточительной.

Другая проблема связана с версионированием: что происходит, если вы сериализуете свой Nodeграфик в 1-й день, затем в 2-й день вы добавляете другое поле Node(например, другое значение указателя или примитивное значение), затем в 3-й день вы десериализуете свой файл из 1 день?

Вы также должны учитывать другие вещи, такие как порядок байтов. Одна из основных причин, по которой файлы MacOS и IBM / Windows / PC были несовместимы друг с другом в 1980-х и 1990-х годах, несмотря на то, что якобы создавались одними и теми же программами (Word, Photoshop и т. Д.), Заключалась в том, что на x86 / PC многобайтовые целочисленные значения были сохранены в порядке с прямым порядком байтов, но с порядком байтов на Mac - и программное обеспечение не было создано с учетом межплатформенной переносимости. В настоящее время дела обстоят лучше благодаря лучшему обучению разработчиков и нашему все более разнородному миру вычислений.

Dai
источник
2
Сброс всего в пространство памяти процесса также будет ужасным по соображениям безопасности. Ночь программы имеет в памяти как 1) некоторые общедоступные данные и 2) пароль, секретный одноразовый номер или закрытый ключ. При сериализации первого не требуется раскрывать какую-либо информацию о последнем.
Чи
8
Очень интересная заметка на эту тему: почему форматы файлов Microsoft Office такие сложные?
нанес удар
15

Хитрость есть на самом деле уже было описаны в самом слове: « серийной лизация».

Вопрос в основном: как я могу представить произвольно сложный взаимосвязанный циклически ориентированный граф произвольно сложных объектов в виде линейной последовательности байтов?

Подумайте об этом: линейная последовательность подобна вырожденному ориентированному графу, где каждая вершина имеет ровно одно входящее и исходящее ребро (кроме «первой вершины», у которой нет входящего ребра, и «последней вершины», у которой нет исходящего ребра) , И байт явно менее сложен, чем объект .

Таким образом, кажется разумным, что при переходе от произвольно сложного графа к гораздо более ограниченному «графу» (фактически просто к списку) и от произвольно сложных объектов к простым байтам информация будет потеряна, если мы сделаем это наивно и не будем t кодировать «постороннюю» информацию каким-либо образом. И это именно то, что делает сериализация: кодировать сложную информацию в простой линейный формат.

Если вы знакомы с YAML , вы можете взглянуть на функции привязки и псевдонима, которые позволяют вам представить идею, что «один и тот же объект может появляться в разных местах» в сериализации.

Например, если у вас есть следующий график:

A → B → D
↓       ↑
C ––––––+

Вы можете представить это как список линейных путей в YAML, например:

- [&A A, B, &D D]
- [*A, C, *D]

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

(Что, кстати, означает, что вышеуказанный текстовый файл YAML также должен быть «сериализован», для этого предназначены различные кодировки символов и форматы передачи Unicode… это не строго «сериализация», просто кодировка, потому что текстовый файл уже является последовательным). / линейный список кодовых точек, но вы можете увидеть некоторые сходства.)

Йорг Миттаг
источник
13

Другие ответы уже касаются сложных графов объектов, но стоит отметить, что сериализация примитивов также нетривиальна.

Используя имена примитивных типов C для конкретности, рассмотрим:

  1. Я сериализую long. Некоторое время спустя я десериализовал его, но ... на другой платформе, а теперьlong это int64_tне то, что int32_tя хранил. Поэтому мне нужно либо очень внимательно следить за точным размером каждого типа, который я храню, либо хранить метаданные, описывающие тип и размер каждого поля.

    Обратите внимание, что это другая платформа может быть той же самой платформой после будущей перекомпиляции.

  2. Я сериализирую int32_t. Некоторое время спустя я десериализовал его, но ... на другой платформе, и теперь значение повреждено. К сожалению, я сохранил значение на платформе с прямым порядком байтов и загрузил ее на плате с прямым порядком байтов. Теперь мне нужно установить соглашение для моего формата, или добавить больше метаданных, описывающих постоянство каждого файла / потока / чего угодно. И, конечно же, фактически выполнять соответствующие преобразования.

  3. Я сериализую строку. На этот раз одна платформа использует charи UTF-8, а другаяwchar_t и UTF-16.

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

Графы объектов добавляют еще один уровень сложности поверх этого.

Бесполезный
источник
6

Есть несколько аспектов:

Читаемость по той же программе

Ваша программа как-то хранит ваши данные в виде байтов в памяти. Но он может быть произвольно разбросан по разным регистрам, с указателями, перемещающимися назад и вперед между его меньшими частями. [Править: Как уже отмечалось, физически данные более вероятны в основной памяти, чем регистр данных, но это не снимает проблему с указателем] , Просто подумайте о связанном списке целых чисел. Каждый элемент списка может храниться в совершенно другом месте, и все, что содержит список, - это указатели от одного элемента к другому. Если бы вы взяли эти данные как есть и попытались скопировать их на другой компьютер с той же программой, вы столкнулись бы с проблемами:

  1. Прежде всего, регистр адреса, в котором ваши данные хранятся на одном компьютере, может уже использоваться для чего-то совершенно другого на другом компьютере (кто-то просматривает обмен стеками, и браузер уже съел всю эту память). Так что, если вы просто переопределите эти регистры, прощайте браузер. Таким образом, вам нужно будет переставить указатели в структуре, чтобы они соответствовали свободным адресам на втором компьютере. Та же проблема возникает при попытке перезагрузить данные на том же компьютере в более позднее время.
  2. Что, если какой-либо внешний компонент указывает на вашу структуру или ваша структура имеет указатели на внешние данные, вы не передали? Сегфолты везде! Это станет кошмаром отладки.

Читаемость другой программой

Допустим, вам удается выделить только правильные адреса на другом компьютере, чтобы ваши данные вписывались. Если ваши данные обрабатываются отдельной программой на этом компьютере (на другом языке), эта программа может иметь совершенно другое базовое понимание данных. Скажем, у вас есть объекты C ++ с указателями, но ваш целевой язык даже не поддерживает указатели на этом уровне. Опять же, у вас нет чистого способа обработки этих данных во второй программе. В результате вы получаете в памяти некоторые двоичные данные, но затем вам нужно написать дополнительный код, который оборачивает данные и каким-то образом преобразует их во что-то, с чем может работать ваш целевой язык. Похоже на десериализацию, просто ваша отправная точка теперь странный объект, разбросанный по вашей основной памяти, который отличается для разных исходных языков, вместо файла с четко определенной структурой. Конечно, то же самое, если вы попытаетесь напрямую интерпретировать двоичный файл, содержащий указатели, - вам нужно писать синтаксические анализаторы для каждого возможного способа, которым другой язык может представлять данные в памяти.

Читаемость человеком

Два самых известных современных языка сериализации для веб-сериализации (xml, json) легко понятны человеку. Вместо двоичной кучи фактическая структура и содержание данных понятны даже без программы для чтения данных. Это имеет несколько преимуществ:

  • более простая отладка -> если есть проблема в вашем сервисном конвейере, вы просто смотрите на данные, которые приходят из одного сервиса, и проверяете, имеет ли это смысл (в качестве первого шага); вы также непосредственно видите, выглядят ли данные так, как вы думаете, когда вы пишете свой интерфейс экспорта.
  • возможность архивирования: если у вас есть данные в виде чистого двоичного кода, и вы теряете программу, предназначенную для их интерпретации, вы теряете данные (или вам придется потратить довольно много времени, чтобы что-то там найти); если ваши сериализованные данные читаются человеком, вы можете легко использовать их в качестве архива или запрограммировать собственного импортера для новой программы
  • декларативный характер данных, сериализованных таким образом, также означает, что они полностью независимы от компьютерной системы и ее аппаратных средств; Вы можете загрузить его в совершенно другой построенный квантовый компьютер или заразить инопланетного ИИ альтернативными фактами, чтобы он случайно вылетел на следующее солнце (Эммерих, если вы прочитаете это, было бы неплохо, если бы вы использовали эту идею для следующего 4 июля фильм)
Фрэнк Хопкинс
источник
Мои данные, вероятно, в основном в основной памяти, а не в регистрах. Если мои данные помещаются в регистры, сериализация едва ли является проблемой. Я думаю, вы неправильно поняли, что такое регистр.
Дэвид Ричерби
Действительно, я использовал термин «регистр» здесь слишком свободно. Но главное в том, что ваши данные могут содержать указатели на адресное пространство, чтобы идентифицировать его собственные компоненты или ссылаться на другие данные. Не имеет значения, является ли это физическим регистром или виртуальным адресом в основной памяти.
Фрэнк Хопкинс
Нет, вы использовали термин «зарегистрироваться» совершенно неправильно. То, что вы называете регистрами, находится в совершенно иной части иерархии памяти, чем действительные регистры.
Дэвид Ричерби
6

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

Иногда вы хотите сериализовать вещи, которые не являются чистыми данными.

Например, подумайте о дескрипторе файла или соединении с сервером. Даже если дескриптор файла или сокета является int, это число не имеет смысла при следующем запуске программы. Чтобы правильно воссоздать объекты, которые содержат дескрипторы таких вещей, вам нужно заново открыть файлы и воссоздать соединения, и решить, что делать, если это не удастся.

Многие языки в наши дни поддерживают хранение анонимных функций внутри объектов, например, onBlah()обработчик в Javascript. Это сложно, потому что такой код может содержать ссылки на дополнительные фрагменты данных, которые, в свою очередь, должны быть сериализованы. (И еще есть проблема сериализации кода кросс-платформенным способом, который, очевидно, проще для интерпретируемых языков.) Тем не менее, даже если поддерживается только подмножество языка, он все равно может оказаться весьма полезным. Не многие механизмы сериализации пытаются сериализовать код, но смотрите serialize-javascript .

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

Часто вы хотите, чтобы сериализованные данные были краткими.

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

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

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

Иногда пространство не важно, но удобочитаемость - в этом случае вы можете использовать формат ASCII (возможно, JSON или XML).

Artelius
источник
3

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

Многие из объектов, с которыми вы работаете в типичной программе, не в этой форме, потому что объекты на самом деле состоят из множества различных выделений памяти, которые находятся в разных местах в ОЗУ, и могут быть отделены друг от друга миллионами байтов материала, который вы не волнует Подумайте только о базовом связанном списке: каждый узел в списке представляет собой последовательность байтов, да, но узлы находятся в разных местах памяти вашего компьютера, и они связаны с указателями. Или просто подумайте о простой структуре, которая имеет указатель на строку переменной длины.

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

Дэвид Грейсон
источник
1
Я не уверен, что это отличное определение последовательности. Большинство людей определило бы последовательность, ну, в общем, последовательность: последовательность вещей одна за другой. По вашему определению, int seq(int i) { if (0 <= i < length) return i+1; else return -1;}это последовательность. Так как я собираюсь сохранить это на диске?
Дэвид Ричерби
1
Если длина равна 4, я сохраняю четырехбайтовый файл с содержимым: 1, 2, 3, 4.
Дэвид Грейсон,
1
@DavidRicherby Его определение эквивалентно «линии вещей одна за другой», это просто более математическое и точное определение, чем ваше интуитивное определение. Обратите внимание, что ваша функция не является последовательностью, потому что для того, чтобы иметь последовательность, вам нужна эта функция и другое целое число, которое называется длиной.
user253751
1
@FreshAir Моя точка зрения такова, что последовательность составляет 1, 2, 3, 4, 5. Я записал функцию . Функция не является последовательностью.
Дэвид Ричерби
1
Я уже предложил простой способ записи функции на диск: для каждого возможного ввода сохраняйте вывод. Я думаю, может быть, вы все еще не понимаете, но я не уверен, что сказать. Знаете ли вы, что во встроенных системах принято преобразовывать дорогие функции, например, sinв таблицу поиска, которая представляет собой последовательность чисел? Знаете ли вы, что ваша функция такая же, как эта для входов, которые нас интересуют? int seq(n) { int a[] = [1, 2, 3, 4]; return a[n]; } Почему именно вы говорите, что мой четырехбайтовый файл является неадекватным представлением?
Дэвид Грейсон,
2

Сложности отражают сложности данных и самих объектов. Эти объекты могут быть объектами реального мира или объектами только компьютера. Ответ в названии. Сериализация - это линейное представление многомерных объектов. Есть много проблем, кроме фрагментированной оперативной памяти.

Если вы можете сгладить 12 пятимерных массивов и некоторый программный код, сериализация также позволяет переносить всю компьютерную программу (и данные) между компьютерами. Протоколы распределенных вычислений, такие как RMI / CORBA, широко используют сериализацию для передачи данных и программ.

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

Некоторые части структуры могут даже не быть в памяти вообще. Если вы используете отложенное кэширование, некоторые части объекта могут ссылаться только на файл диска и загружаться только при обращении к этой части этого конкретного объекта. Это часто встречается в серьезных системах персистентности. BLOB являются хорошим примером. Getty Images может хранить огромную мегабайтную фотографию Фиделя Кастро и некоторые метаданные, такие как имя изображения, стоимость аренды и само изображение. Возможно, вы не захотите каждый раз загружать 200 МБ изображение в память, если только вы не посмотрите на него. Сериализованный, весь файл потребует более 200 МБ памяти.

Некоторые объекты вообще не могут быть сериализованы. В области программирования Java вы можете иметь программный объект, представляющий графический экран или физический последовательный порт. Там нет реальной концепции сериализации любой из них. Как бы вы отправили свой порт кому-то еще по сети?

Некоторые вещи, такие как пароли / ключи шифрования, не должны храниться или передаваться. Они могут быть помечены как таковые (volatile / transient и т. Д.), И процесс сериализации пропустит их, но они могут жить в оперативной памяти. Пропуск этих тегов - это то, как ключи шифрования непреднамеренно отправляются / сохраняются в простом ASCII.

Этот и другие ответы, почему это сложно.

Пол Ушак
источник
2

Проблема, которую я имею: не все ли переменные (будь то примитивы типа int или составные объекты) уже представлены последовательностью байтов?

Да, они. Проблема здесь заключается в расположении этих байтов. Простойint может быть длиной 2, 4 или 8 бит. Это может быть большой или маленький порядок байтов. Он может быть без знака, подписан с дополнением 1 или даже в каком-нибудь супер экзотическом битовом кодировании, таком как negabinary.

Если вы просто intвыгружаете двоичный файл из памяти и называете его «сериализованным», вам необходимо подключить практически весь компьютер, операционную систему и вашу программу, чтобы она была десериализуемой. Или, по крайней мере, точное описание их.

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

Сериализация простого объекта в значительной степени записывает его в соответствии с некоторыми правилами. Этих правил много, и они не всегда очевидны. Например, xs:integerв XML написано в Base-10. Не Base-16, не Base-9, но 10. Это не скрытое предположение, это фактическое правило. И такие правила делают сериализацию сериализацией. Потому что, по большому счету, нет правил размещения битов вашей программы в памяти .

Это была лишь верхушка айсберга. Давайте рассмотрим пример последовательности этих простейших примитивов: а C struct. Вы могли бы подумать, что

struct {
short width;
short height;
long count;
}

имеет определенную структуру памяти на данном компьютере + ОС? Ну, это не так. В зависимости от текущей #pragma packнастройки компилятор будет заполнять поля. При настройках по умолчанию для 32-битной компиляции оба shortsбудут заполнены до 4 байтов, поэтому structв памяти будет фактически 3 поля по 4 байта. Так что теперь вам нужно не только указать, что shortэто 16-битная длина, это целое число, записанное в отрицательном дополнении 1 с большим или маленьким порядком байтов. Вы также должны записать настройку упаковки структуры, с которой была скомпилирована ваша программа.

В этом и заключается суть сериализации: создание набора правил и их соблюдение.

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

Agent_L
источник