Что делает сериализация данных?

мне трудно понять, что такое сериализация и что она делает.

Позволь мне упростить мою проблему. У меня есть struct info в моих программах на c / C++, и я могу сохранить это struct данные в файл save.bin или отправить его через сокет на другой компьютер.

struct info {
    std::string name;
    int age;
};

void write_to_file()
{
    info a = {"Steve", 10};
    ofstream ofs("save.bin", ofstream::binary);
    ofs.write((char *) &a, sizeof(a));   // am I doing it right?
    ofs.close();
}

void write_to_sock()
{
    // I don't know about socket api, but I assume write **a** to socket is similar to file, isn't it?
}

write_to_file просто сохранить struct info объект a на диск, делая эти данные настойчивый, верно? И записать его в сокет-это почти то же самое, верно?

в приведенном выше коде, я не думаю, я использовал сериализации данных, но данные a делается стойким в save.bin в любом случае, верно?

вопрос

  1. тогда в чем смысл сериализации? Нужно ли мне это здесь? Если да, то как мне его использовать?

  2. Я всегда думаю, что любые файлы .txt/.csv/.exe/..., биты 01 в памяти, что означает, что они имеют двоичное представление естественно, поэтому мы не можем просто отправить эти файлы через сокет напрямую?

пример кода высоко ценится.

6 ответов


но данные a становятся постоянными в save.в любом случае, бин, верно?

нет! Ваша структура содержит std::string. Точная реализация (и двоичные данные, которые вы получаете с приведением к char* не определяется стандартом, но фактические строковые данные всегда будут уходить в отставку где-то вне фрейма класса, выделенного кучей, поэтому вы не можете сохранить эти данные так легко. При правильной сериализации строковые данные записываются туда, где заканчивается остальная часть класса вверх, так что вы сможете прочитать его из файла. Для этого нужна сериализация.

как это сделать: вы должны каким-то образом кодировать строку, самый простой способ-сначала написать ее длину, а затем саму строку. При чтении файла сначала прочитайте длину, затем прочитайте это количество байтов в новый строковый объект.

Я всегда думаю, что любые файлы, .формат txt./csv/.исполняемый./.., биты 01 в

Да, но проблема в том, что не универсально определено, какой бит представляет какую часть структуры данных. В частности, есть мало-endian и big-endian, в архитектурах, они хранят биты "наоборот". Если вы наивно зачитаете файл, написанный в несовпадающей архитектуре, вы, очевидно, получите мусор.


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

1. Указатели

если данные содержат какой-либо указатель, конечно, вы не можете просто сбросить нагрузку позже, как адрес памяти, где указатели указывают на не будет иметь никакого значения, как только программа завершается и перезапускается. Многие объекты " скрыты" указатели... например, нет способа сбросить std::vector в памяти и перезагрузить его позже правильно... sizeof на std::vector явно не включает размер содержащихся элементов и, следовательно, любую структуру, содержащую std::vector нельзя просто сбросить и перезагрузить. То же самое касается std::string и другие std контейнеров.

2. Переносимость

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

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

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

3. Версионирование

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

если ваш формат - это только изображения памяти текущих структур данных, будет довольно сложно добавить позже например,


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

за кулисами, есть игровой объект, который выглядит как это:

class GameState
{
   int level;
}

и уровень 25.

вам действительно понравилась игра, но вы не хотите начинать все сначала, если последний босс убьет вас. Итак, интуитивно вы нажимаете Ctrl+S. Но подождите, вы получите сообщение об ошибке:

Sorry, saving is disabled.

что? Значит, мне придется начинать все сначала, если я умру? Как такое может быть?

барабанная дробь

разработчики, хотя и блестящие (им удалось удержать вас на крючке в течение 2 дней подряд, правильно?) не реализовал сериализация.

при перезапуске игры происходит очистка памяти. Что все важные GameState объект, тот, который вы потратили 2 дня, чтобы увеличить level члены 25, будет уничтожен.

как можно это исправить? Память восстанавливается ОС при закрытии игры. Где вы могли бы его хранить? На внешнем сервере? на диске? (запись в файл)

хорошо, почему не.

class GameState
{
    int level;
    void save(const std::string& fileName)
    { /* write level to file */ }
    void load(const std::string& fileName)
    { /* read game state from file */ }
};

при нажатии клавиши Ctrl+s на GameState объект сохраняется в файл.

и, чудесным образом, когда вы загружаете игру,


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

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

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


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

при этом важно понимать, что внутри программа, данные не просто последовательность байтов. Он имеет формат и структура: как double представлен отличается от одной машины к следующему, например; и более сложные объекты, такие как std::string, даже в смежной памяти. Поэтому первое, что вы должны сделать при сериализации определяется способ представления каждого типа в виде последовательности байтов. Если вы общаетесь с другой программой, обе программы должны согласиться с этим последовательным форматом; если это только для того, чтобы вы могли перечитывать данные сами, вы можете использовать любые формате вы хотите (но я бы рекомендовал использование предварительно определенного стандартного формата, например XDR, хотя бы для упрощения документация.)

то, что вы не можете сделать, это просто выгрузить изображение объекта в памяти. Сложные объекты, такие как std::string будет иметь указатели на них, и эти указатели будут бессмысленны в другом процессе. И даже представление простых типов, таких как double может меняться с течением времени. (Этот миграция с 32 бит на 64 привела к размеру long изменение на большинстве системный.) Необходимо определить формат, а затем создать его байт по байтам, из данных, которые у вас есть. Например, чтобы написать XDR, вы можете использовать что-то вроде этого:

typedef std::vector<char> Buffer;

void
writeUInt( Buffer& dest, unsigned value )
{
    dest.push_back( (value >> 24) & 0xFF );
    dest.push_back( (value >> 16) & 0xFF );
    dest.push_back( (value >>  8) & 0xFF );
    dest.push_back( (value      ) & 0xFF );
}

void
writeInt( Buffer& dest, int value )
{
    writeUInt( dest, static_cast<unsigned>( value ) );
}

void
writeString( Buffer& dest, std::string const& value)
{
    assert( value.size() <= 0xFFFFFFFF );
    writeInt( dest, value.size() )
    std::copy( value.begin(), value.end(), std::back_inserter( dest ) );
    while ( dest.size() % 4 != 0 ) {
        dest.push_back( '' );
    }
}

помимо большого эдиана или маленького эндиана существует проблема того, как данные упаковываются для данной структуры для этой программы с этим компилятором. Если вы хотите сохранить всю структуру, вы не можете использовать указатели, вам придется заменить ее буфером символов, достаточно большим для ваших нужд. Если другая машина будет иметь ту же архитектуру, то если вы используете #pragma pack (1), между полями вашей структуры не будет пробелов, и вы можете гарантировать, что данные появятся как если бы он был сериализован, но без префикса размера для вашей строки. Вы можете пропустить # pragma pack (1), если уверены, что другая программа, которая будет читать данные, имеет те же настройки для той же самой структуры. Кроме этого, данные не совпадают.

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

template<typename T>
buffer& operator<<(T data)
{
    *(T*)buf = data;
    buf += sizeof(T);
}

очевидно вам понадобятся специализированные для строк и больших типов данных. Вы можете использовать memcpy для больших структур и передавать указатели на данные. Для строк вы захотите префикс длины, как упоминалось ранее.

для серьезных потребностей сериализации, однако, есть намного больше, чтобы рассмотреть.