двоичная сериализация
Ниже приведен пример того, как вы можете приблизиться к сериализации для низкоуровневого формата файла. Обратите внимание, что мы знаем только, как сериализовать два типа полей (uint16_t
и std::string
). Вы можете специализировать дополнительные типы по мере необходимости.
Формат данных
Первое, что мы хотим сделать, это выбрать формат данных. Ниже я использовал простой блочный подход, где у нас зарезервировано два байта для типа и длины каждого поля. Всякий раз, когда вы хотите сериализовать поле, вы сначала пишете число, представляющее тип, а затем число, указывающее, сколько байтов следует за заголовком.
Это известно как кодирование переменной длины.
Пример кода
#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <cstdint>
#include <iomanip>
using namespace std;
enum class SerializedType {
Unknown,
UnsignedShortInteger,
String
};
struct mapInt {
public:
const uint8_t first;
const uint8_t second;
mapInt(uint16_t value)
: first((value & 0xff00)>>8),
second(value & 0xff) {
}
};
std::vector<uint8_t> toWireFormat(const uint16_t& value) {
const auto mapped = mapInt(value);
const std::vector<uint8_t> data = {
static_cast<uint8_t>(SerializedType::UnsignedShortInteger),
2,
mapped.first,
mapped.second
};
return data;
}
std::vector<uint8_t> toWireFormat(const std::string& value) {
std::vector<uint8_t> data;
data.resize(value.size() + 2);
data[0] = static_cast<uint8_t>(SerializedType::String);
data[1] = static_cast<uint8_t>(value.size());
for (auto i = 0; i < value.size(); ++i) {
data[i+2] = value.at(i);
}
return data;
}
std::string serializedTypeAsString(SerializedType type) {
switch (type) {
case SerializedType::Unknown:
return " Unknown";
case SerializedType::UnsignedShortInteger:
return "uint16_t";
case SerializedType::String:
return " string";
}
}
template<class T>
void appendData(std::vector<uint8_t>& data, const T& value) {
const auto buffer = toWireFormat(value);
data.insert(data.end(), buffer.begin(), buffer.end());
}
class PlayerOne {
public:
const string name;
uint16_t hp;
uint16_t shield;
string favFood = "soup";
PlayerOne() = default;
~PlayerOne() = default;
void serialize(std::vector<uint8_t>& data) const {
appendData(data, name);
appendData(data, hp);
appendData(data, shield);
appendData(data, favFood);
};
};
int main()
{
PlayerOne ready = { "JOHN!", 100, 55, "Soup" };
PlayerOne two = { "Sarah", 250, 0, "Cake" };
vector<uint8_t> output;
ready.serialize(output);
two.serialize(output);
cout << "Serialized two objects into " << output.size() << " bytes." << endl;
size_t i = 0;
while (i < output.size()) {
const auto typeString = serializedTypeAsString(
static_cast<SerializedType>(output.at(i)));
const int length = output.at(i+1);
cout << "\nType: " << typeString << ", Length: " << length << ", Data: ";
i += 2;
if ((i+length) > output.size()) {
break;
}
for (size_t j = i; j < (i+length); ++j) {
cout << hex << setfill('0') << setw(2) << static_cast<int>(output.at(j));
}
i += length;
}
cout << endl;
return 0;
}
Пример вывода
Примечание: строки ниже кодируются в шестнадцатеричном формате. Строки не заканчиваются нулем для экономии места в двоичном формате. При десериализации вам придется повторно применять нулевые байтовые терминаторы.
Serialized two objects into 42 bytes.
Type: string, Length: 5, Data: 4a4f484e21
Type: uint16_t, Length: 2, Data: 0064
Type: uint16_t, Length: 2, Data: 0037
Type: string, Length: 4, Data: 536f7570
Type: string, Length: 5, Data: 5361726168
Type: uint16_t, Length: 2, Data: 00fa
Type: uint16_t, Length: 2, Data: 0000
Type: string, Length: 4, Data: 43616b65