Pull to refresh

Comments 33

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

Посмотрел Вашу старую статью только что и расстроился — она осталась без внимания, хотя тоже написана очень интересно.

В общем, если есть что рассказать — пишите, у вас отлично получается.
Спасибо! За ссылкой на github приглашаю в личку (это же не «Я пиарюсь», в самом деле!). Выслушаю все замечания к статье, починю всё, что можно, и ссылку всем отдам.
Сходу вопрос: почему решили пилить свой сериализатор и чем не устроил тот же, скажем, Google Protobuf? Последний, кстати говоря, используется в diablo 3, и вроде всё ок + куча клиентских библиотек и тд
То есть первая картинка в посте не даёт ответ на этот вопрос? :)

Шутки шутками, но чисто для уточнения: хотелось сделать сериализатор только на C++ и шаблонах (шаблоны — не C++ :-D) без использования внешних утилит (компилятора протобуфа)?


Потому как у протобуфа, как минимум, следующие профиты:


  • генерация для разных языков из промежуточного представления, т.е. не нужно писать парсер для всех поддерживаемых языков
  • переменная длинна полей: то есть маленькие значения меньше занимают места (https://developers.google.com/protocol-buffers/docs/encoding — первый пример, где для 150 будет потрачено 3 байта, вместо 4х)
Хотелось минимальный оверхед. Насколько я понял, протобуф кучу всего делает динамически. А мы здесь фактически генерируем последовательность cout << something (или, в случае десериализатора, который я опустил, in >> something; if(in.rdstate()) return false;).

Собственно, TerraDon ниже уже это отметил.

Гм, у него только один комментарий и тот к статье в черновиках.

Упс, забыл одобрить. ¯\_(ツ)_/¯ Теперь он есть.
В данном случаи больше подходит flatbuffers от того же гугла. Не такой мощный, но быстрее. В случаи с реалтайм играми «рефлексия» в protobuf зло.
Можно подробнее для тупых и tl;dr:
почему MyBigPacket::serialize(std::cout, 11, 22, 'a', 33, 44, 55, 'b');
возвращает 1122a334455b
а не hex(0000000b00000016...)?
Я, конечно. понимаю. что для простоты у вас простейшая реализация stream << value; но эта вишенка портит ваш роскошный торт. Хотя бы тем, что люди, читающие первый и последний параграф (или сразу с примеров кода), возмущённо воскликнут: «А как же десериализация?!» Лично я так и сделал.
Достаточно написать stream << value << " ";, чтобы всё починить. На деле такой способ не очень хорош (зачем совать пробел в двоичный буфер?), поэтому я делаю так:
// манипуляторы!11
template< class CharT, class Traits >
std::basic_ostream<CharT, Traits>& delimiter( std::basic_ostream<CharT, Traits>& os ) {
    return os << CharT(' ');
}

template< class CharT, class Traits >
std::basic_istream<CharT, Traits>& delimiter( std::basic_istream<CharT, Traits>& is ) {
    return is; // при чтении париться о пробелах уже не надо
}

BitStream& delimiter(BitStream& bs) {
    return bs; // ничего не надо делать -- ни при чтении, ни при записи
}

И, соответственно, в коде будет вроде
serialize(...) {
    F::serialize(out, ...);
    out << delimiter;
    NextSerializer::serialize(out, ...);
}
deserialize(...) {
    if(!F::deserialize(in, ...)) return false;
    in >> delimiter; // вдруг будет вид потока, требующий считать разделитель?
    return !in.rdstate() // ничего не сломалось при считывании разделителя
    && NextDeserializer::deserialize(in, ...); // ничего не сломалось в рекурсии
}

Возможно, этот кусок кода действительно не следовало опускать.
БЭЭЭЭЭЭП!!! Неправильный ответ, у вас выпадает сериализация строк.
Давайте проще, вам нужно в разделе «Интерфейс сериализатора» упомянуть, что потоки могут иметь различный формат вывода, а потому для разного вида потоков может потребоваться написать дополнительный код. И, соответственно, добавить такое же описание в примерах или использовать двоичный вывод. Увы, из-за некоторой углублённости в Qt я оторвался от плюсовой жизни и стандартов, потому не помню, есть ли такие же пары, как QTextStream\QDataStream. Потому конкретые правильные формулировки остаются на вашей совести.
Нет, просто для строк я напишу специальный класс StringField, а не буду использовать подход в лоб. Буду в base64 их конвертить, например.
1) Я был немного не прав, строки будут нормально писаться.
2) Имеет смысл сделать RAW- и String- сериализаторы, которые будут подставляться в зависимости от типа потока и\или требования пользователя.
Но это всё хотелки, летать может и так.
успешно выводит
7776666a



А как это сможет десерелиазоваться? Как я понимаю: процесс должен быть обратимым.
Раздел: «Интерфейс сериализатора»
Строка: «out << t; // просто скормить сериализуемый объект в поток!»
Смысл: поведение сериализатора очень зависит от класса потока.
Суть: автор путёвой либы сделал путёвую статью с плохим примером. Конкретнее, скармливал в него cin\cout. Гениально!
Вы правы, не подумал. Добавил в статью эту деталь реализации. :)
Возможно, статья сможет освободиться от них и даже стать приличным учебным материалом!


Можно сразу начинать делать в своё удовольствие потихоньку сборник статей, потихоньку удаляя ошибки из уже написанного (ну вот как здесь было сделано — https://habrahabr.ru/post/248153/) (только лучше сразу примерный план сборника составить и выложить на обсуждение).
Каковы дополнительные расходы на сериализацию классов — сколько дополнительного кода надо добавить в каждый из них?
Как будет выглядеть серилизатор для чего-нибудь простенького, вроде:
class Point {int x, y};
class Player {
Point a, b;
vector c;};
?
struct PointField {
    template<typename OS> serialize(OS& out, const Point& p) {
        Schema<IntegerField, IntegerField>::serialize(out, p.x, p.y);
    }
};
struct PlayerField {
    template<typename OS> serialize(OS& out, const Player& pl) {
        Schema<PointField, PointField, VectorField>::serialize(out, pl.a, pl.b, pl.c);
    }
};

(это если не нужно нигде никаких дополнительных проверок и нужно прям все данные в лоб передать)
С точки зрения использования — несколько недостатков:
1) сериализуемое поле и его схема разделены (то есть, например, VectorField и pl.c не стоят рядом с друг другом) — в два счёта перепутать что к чему относится.
2) что вообще надо указывать эту схему вручную, а не доверить это всё компилятору — при изменении будут вылезать проблемы
3) что разделены serialize и deserialize, т.е. проблема «Повторяющийся код» не решена.
1,2) На Хабре есть статья про генерацию простых сериализаторов для классов, которую вполне можно здесь привлечь. Пара дополнительных шаблонов, пара макросов — и генерировать сериализаторы для классов станет легко и удобно. Статья всё-таки чуть-чуть не об этом, тут мы занимаемся сериализацией пакетов.
3) На каком-то уровне они всё-таки должны быть разделены. Но я предлагаю разделять на самом базовом (числа, строки, ещё какие-то элементарные значения, массивы, деревья); на уровне больших классов всё-таки тактика «шаблоны+макросы» должна всё решить. (Хотя хороша ли идея пересылать большие объекты целиком в traffic-intensive сетевой игре?)
Чем вас не устраивает Boost.Serialize? Функционал похож. Можете пояснить, чем ваша реализация лучше?
Не могу. Я не утверждал, что она лучше.
Я с большим уважением отношусь к таким статьям, однако проблемы заявленные во вступлении не решены, кроме как использования возможностей обновленного стандарта.
Ну и имитация конца света с отключенными поисковыми системами и доступа к готовым библиотекам. (при отключенном интернете нам бы пришлось изобрести новый способ связи)
1) Например, причем тут безопасность? — Кроме использования шифрования в связке со сжатием (независимо от языка) далеко не уедешь.
В коде нет ничего об этом.

2) То что делает ваш сериализатор похоже на вывод CSV без запятых. А как быть с древовидными структурами?
Например в .net (да других платформах тоже) есть сериализатор работающий отлично от xml/json и т.п, все что нужно, так это добавить атрибут Serializable у своего класса (в java подобное делается через интрфейс). И никакого дублированного кода при высокой производительности. Причем сериализатор сразу позволяющий модифицировать удаленный объект (устанавливает удаленное подключение на заданный адрес при необходимости) ну или запишет в файл.

Даже если опустить прелюдию с готовыми решениями есть готовые паттерны (это про дублирующийся код) Например в книге GOF чтение дерева описывают в паттерне Visitor. И даже обыденное использование интерфейсов (абстрактных классов в c++ без реализации методов) лишит необходимости переопределять метод сериализации + полный контроль над тем что именно сериализуется для конкретного класса.

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

Большое спасибо за статью, однако она должна как минимум иметь название по-скромнее. «Необычная Сериализация в C++11». И не нужно говорить в начале что все на свете проблемы тут порешаны на 20 строках кода:)
1) Наверное, мне стоило больше цитировать статью Gaffer on Games, чтобы дать понять, причём тут безопасность. Безопасный код, парсящий пакет — это прежде всего код, в котором после каждого считывания стоит проверка, и могут стоять ещё всяческие дополнительные проверки (длина массива меньше мегабайта, число чего-то там больше нуля). Все эти проверки по отдельности могут быть вынесены в отдельные поля (ShortArrayField, RangeIntegerField, etc.), а приведённый шаблонный код успешно склеивает их на этапе компиляции в большую уродливую функцию с if(error) return false; в каждой строке, писать которую вручную нам было бы тошно.

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

Ну и да, библиотеки, читающие структуры из внешних файлов — это прекрасно, но на шаблонах оверхед всё-таки меньше. И проблемы порешаны, конечно же, не в 20 строках кода. Проблемы порешаны на идейном уровне. (И только поставленные, а не все, про какие только можно подумать.)
Не ожидал ответа, поэтому благодарен.

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

Возможно в статье, на которую Вы ссылаетесь, есть много полезных и стоящих идей, но эту статью пишете Вы. Техника с применением шаблонов (Variadic Templates) имеет законное место быть — не зря же ее добавили в стандарт. И поэтому ваша статья была похоже больше на очередной пример использования, нежели на решение проблемы сериализатора. Поскольку тут нет жюри и присяжных, не принимайте близко к сердцу.

Я бы назвал эту технику как способ выхода из сложной ситуации. Создатели решили просто обойти ограничения c++ еще одним способом. Для пообного в других языкаъ давно существуют динамические типы и различные функции-делегаты (еще замыкания — Closures как JavaScript или Groovy).

И как я писал выше, старое доброе ООП решение тут бы подошло. Реализовать единый интерфейс и наследовать его теми классами которые сериализуются. И точно указывая какие поля сериализовать. А так получается, что сериализуемые объекты нужно выделять в отдельный класс т.к сериализатор берет объект целиком. В 99% случаев у ваших классов будут поля которые не нужно передавать при сериализации (в данном случае по сети), для той же безопасности или экономии трафика.

Я лично под сериализатором понимаю некий класс превращающий объекты в поток байт и обратно. Но если говорить про игру, любой читающий хотел бы увидеть нюансы конкретно для игр. Вот их в статье и не было.
В начале статьи стояла задача безопасности: «не доверяй данным присланным клиентом» — а хеширование куда пропало? Каким образом написание вроде-как-быстрого кода через использование Variadic Templates делает сообщение безопасным?
— Изначально, правильно писать игру так, чтобы клиент просто выводил данные и передавал команды игрока не имея возможности влиять на саму игру (кроме как на своем экране, если удастся изменить память игры или входящий трафик)
— Трафик должно быть непросто видоизменить (именно на это будут смотреть те кто будет давать вашей игре статус «безопасной») поэтому любой простейший метод шифрования необходим — для той же уверенности в достоверности сообщений. Ну и почему бы не сжать данные? это сэкономит трафик. (сжимают обычно до шифрования т.к сжатие зашифрованного файла ничего не сэкономит)

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

Всего хорошего и удачи в написании самых читаемых статей
Вы понимаете, что в статье не идет речи «решении задачи сериализации» в вакууме? Это статья навеяна циклом статей о написании сетевого протокола сетевой игры. Это кардинальным образом сужает то, что требуется от сериализатора пакетов. Вложенность далеко не всем нужна и в данном случае пишется сериализатор, который этого не реализует. То что вы ожидали не имеет никакого отношения к тому, что должно быть в статье. Поэтому «вам надо, то напишите» является единственным разумным ответом.

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

Так что, давайте как-то разделять свои ожидания и хотелки от реальных недостатков статьи. Статья поставила задачи, она их решила. Вам недостаточно — ищите другие статьи.

А есть тесты насколько именно ваш сериализатор круче того же json'a? =)

Автору респект! Получилось идеоматично и компактно. Надеюсь статья позволит плодить меньше костылей в C++ проектах (ведь почти все изобретают свой сериализатор).


P.S. Окромя flatbuffers можно обратить внимание на Cap'n'Proto и Microsoft Bond. Возможно они решают задачу лучше.

Sign up to leave a comment.

Articles