Пре-сериализация объектов сообщения - реализация? - PullRequest
0 голосов
/ 15 декабря 2011

У меня есть TCP-клиент-сервер, где мне нужно иметь возможность передавать сообщения разных форматов в разное время, используя одну и ту же инфраструктуру передачи / приема.

Два разных типа сообщений, отправляемых от клиента ксервер может быть:

  • TIME_SYNC_REQUEST: запрос игрового времени сервера.Не содержит никакой информации, кроме типа сообщения.

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

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

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

В C ++: я не хочу использовать dynamic_cast для отражения подхода, описанного выше, из соображений производительности.Должен ли я использовать композиционный подход с использованием фиктивных членов для любых возможных данных и char messageType?Я предполагаю, что другая возможность - хранить разные типы сообщений в списках разного типа.Это единственный выбор?В противном случае, что еще я могу сделать, чтобы сохранить информацию о сообщении, пока не пришло время его сериализовать?

Ответы [ 6 ]

1 голос
/ 15 декабря 2011

Может быть, вы можете позволить классу сообщений выполнять сериализацию. Определите интерфейс сериализации, и каждое сообщение реализует этот интерфейс.Поэтому во время сериализации и отправки вы вызываете AbstractMessage :: Serialize () для получения сериализованных данных.

1 голос
/ 15 декабря 2011

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

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

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

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

EDIT2: я не могу предоставить полную реализацию, но вот набросок.

Сначала позвольте мне определить тип значения - это типичный тип значений, которые могут существовать для поля:

typedef boost::variant<int32, int64, double, std::string> value_type;

Теперь я опишу поле

struct field
{
  int field_key;
  value_type field_value;    
};

Теперь вот мой контейнер сообщений

struct Message
{
  field type;
  field size;

  container<field> fields; // I use a generic "container", you can use whatever you want (map/vector etc. depending on how you want to handle repeating fields etc.)
};

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

boost::unique_ptr<Message> getTimeSyncMessage()
{
  boost::unique_ptr<Message> msg(new Message);
  msg->type = { dict::field_type, TIME_SYNC }; // set the type

  // set other default attributes for this message type

  return msg;
}

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

namespace dict
{
  static const int field_type = 1; // message type field id

  // fields that you want
  static const int field_time = 2;
  :
}

Так что теперь я могу сказать,

boost::unique_ptr<Message> msg = getTimeSyncMessage();

msg->setField(field_time, some_value);
msg->setField(field_other, some_other_value);
: // etc.

Теперь сериализация этого сообщения, когда вы готовы к отправке, просто проходит через контейнер и добавляется в BLOB-объект. Вы можете использовать кодировку ASCII или двоичную кодировку (я бы сначала начал с первого, а затем перешел на второе - в зависимости от требований). Таким образом, версия выше в кодировке ASCII может выглядеть примерно так:

1=1|2=10:00:00.000|3=foo 

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

Десериализация будет проходить через большой двоичный объект, извлекать каждое поле соответствующим образом (например, путем разделения на |), использовать фабричные методы для создания скелета (как только вы получите тип - поле 1), затем заполните все атрибуты в контейнере. Позже, когда вы хотите получить определенный атрибут - вы можете сделать что-то вроде:

msg->getField(field_time); // this will return the variant - and you can use boost::get for the specific type.

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

0 голосов
/ 15 декабря 2011

На отправляющем конце соединения, чтобы построить наше сообщение, мы держим идентификатор сообщения и заголовок отдельно от данных сообщения:

  • Message - это тип, который содержит толькоmessageCategory и messageID.
  • Каждый такой Message помещается в единый messageQueue.
  • Отдельные хеши хранятся для данных, относящихся к каждому из messageCategory с.В них есть запись данных для каждого сообщения этого типа с ключом messageID.Тип значения зависит от категории сообщения, поэтому для сообщения TIME_SYNC у нас будет, например, struct TimeSyncMessageData.

Сериализация:

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

Преимущества:

  • Нет потенциально неиспользуемых элементов данных в одном универсальном Message объекте.
  • Интуитивно понятная настройка для извлечения данных, когда наступает время сериализации.
0 голосов
/ 15 декабря 2011

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

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

0 голосов
/ 15 декабря 2011

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

0 голосов
/ 15 декабря 2011

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

struct header
{
  int msgid;
  int len;
};

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

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

В псевдокоде принимающая сторона сообщения выглядит так:

  read_header( header );
  switch( header.msgid )
  {
     case TIME_SYNC:
       read_time_sync( ts );
       process_time_sync( ts );
       break;

     case UPDATE:
       read_update( up );
       process_update( up );
       break;

     default:
       emit error
       skip header.len;
       break;
  }

То, на что похожи функции чтения, зависит от вашей сериализации.Google protobuf довольно приличный, если у вас есть базовые структуры данных и вам нужно работать на разных языках.Повышенная сериализация хороша, если вы используете только C ++ и весь код может использовать одни и те же заголовки структуры данных.

...