C ++ дизайн - сетевые пакеты и сериализация - PullRequest
4 голосов
/ 01 декабря 2008

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

Затем я хотел бы иметь классы, производные от Packet, например: StatePacket, PauseRequestPacket и т. Д. Каждый из этих подклассов будет реализовывать виртуальные функции, Handle (), которые будут вызываться сетевым механизмом, когда один из этих пакетов принимается, чтобы он мог выполнять свою работу, несколько функций get / set, которые считывали и устанавливали поля в массиве данных.

Итак, у меня две проблемы:

  1. (Абстрактный) класс Packet должен быть копируемым и назначаемым, но без разделения, сохраняя все поля производного класса. Возможно даже, что производный класс не будет иметь никаких дополнительных полей, только функцию, которая будет работать с массивом в базовом классе. Как мне этого добиться?
  2. При сериализации я присваивал каждому подклассу уникальный числовой идентификатор, а затем записывал его в поток перед собственной сериализацией подкласса. Но для десериализации, как мне сопоставить идентификатор чтения с соответствующим подклассом, чтобы создать его экземпляр?

Если кому-то нужны какие-либо разъяснения, просто спросите.

- Спасибо


Редактировать: Я не совсем доволен этим, но вот что мне удалось:

Packet.h: http://pastebin.com/f512e52f1
Packet.cpp: http://pastebin.com/f5d535d19
PacketFactory.h: http://pastebin.com/f29b7d637
PacketFactory.cpp: http://pastebin.com/f689edd9b
PacketAcknowledge.h: http://pastebin.com/f50f13d6f
PacketAcknowledge.cpp: http://pastebin.com/f62d34eef

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


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

Ответы [ 4 ]

8 голосов
/ 01 декабря 2008

Для копирования вам нужно написать функцию клона, так как конструктор не может быть виртуальным:

virtual Packet * clone() const = 0;

Какую реализацию каждого пакета реализовать следующим образом:

virtual Packet * clone() const {
    return new StatePacket(*this);
}

например для StatePacket. Пакетные классы должны быть неизменяемыми. Как только пакет получен, его данные могут быть скопированы или выброшены. Таким образом, оператор присваивания не требуется. Сделайте оператор присваивания частным и не определяйте его, что фактически запретит присваивать пакеты.

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

struct MessageFactory {
    std::map<Packet::IdType, Packet (*)()> map;

    MessageFactory() {
        map[StatePacket::Id] = &StatePacket::createInstance;
        // ... all other
    }

    Packet * createInstance(Packet::IdType id) {
        return map[id](); 
    }
} globalMessageFactory;

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

0 голосов
/ 01 декабря 2008

Почему мы, включая меня, всегда делаем такие простые проблемы такими сложными?


Возможно, я не здесь. Но я должен задаться вопросом: действительно ли это лучший дизайн для ваших нужд?

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

Полиморфизм - очень мощный и полезный инструмент. Но это только один из многих доступных нам инструментов.


Похоже, что каждому подклассу Packet потребуется собственный код Marshalling и Unmarshalling. Возможно, унаследовал код Маршаллинга / Unmarshalling Пакета? Возможно продлить это? Все сверху handle () и все остальное, что требуется.

Это много кода.

Несмотря на то, что он значительно более хитрый, он может быть короче и быстрее для реализации данных Пакета в качестве атрибута struct / union класса Packet.

Маршаллинг и демаршаллинг тогда будут централизованы.

В зависимости от вашей архитектуры это может быть так же просто, как запись (и данные). Предполагая, что между вашими клиент-серверными системами нет больших / мало-порядковых проблем, и нет проблем с заполнением. (Например, sizeof (data) одинаков в обеих системах.)

Запись (& data) / read (& data) является подверженной ошибкам техникой . Но часто это очень быстрый способ написать первый набросок. Позже, когда позволит время, вы можете заменить его на код типа Marshalling / Unmarshalling, основанный на типах для каждого атрибута.

Также: Я занялся хранением данных, которые отправляются / принимаются как структура. Вы можете побитово скопировать структуру с помощью operator = (), что иногда было ОЧЕНЬ полезно! Хотя, возможно, не так много в этом случае.


В конечном итоге вы будете иметь оператор switch где-то в этом типе идентификатора подкласса. Заводской метод (который сам по себе довольно мощный и полезный) делает этот переключатель за вас, ища необходимый метод / объект clone () или copy ().

ИЛИ Вы можете сделать это самостоятельно в пакете. Вы можете просто использовать что-то простое:

(getHandlerPointer (id)) (это)


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

В зависимости от плотности сетевого трафика в вашей игре распределение / освобождение может быть дорогим. Опять же, преждевременная оптимизация - корень всего зла. И вы всегда можете просто свернуть свои собственные операторы new / delete. (Еще больше затрат на кодирование ...)


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


Пример кода:

class Packet
{
public:
  enum PACKET_TYPES
  {
    STATE_PACKET = 0,
    PAUSE_REQUEST_PACKET,

    MAXIMUM_PACKET_TYPES,
    FIRST_PACKET_TYPE = STATE_PACKET
  };

  typedef  bool ( * HandlerType ) ( const Packet & );

protected:
        /* Note: Initialize handlers to NULL when declared! */
  static HandlerType  handlers [ MAXIMUM_PACKET_TYPES ];

  static HandlerType  getHandler( int thePacketType )
    {   // My own assert macro...
      UASSERT( thePacketType, >=, FIRST_PACKET_TYPE    );
      UASSERT( thePacketType, <,  MAXIMUM_PACKET_TYPES );
      UASSERT( handlers [ thePacketType ], !=, HandlerType(NULL) );
      return handlers [ thePacketType ];
    }

protected:
   struct Data
   {
            // Common data to all packets.
     int  number;
     int  type;

     union
     {
       struct
       {
         int foo;
       } statePacket;

       struct
       {
         int bar;
       } pauseRequestPacket;

     } u;

   } data;


public:

  //...
  bool readFromSocket() { /*read(&data); */ }  // Unmarshal
  bool writeToSocket()  { /*write(&data);*/ }  // Marshal

  bool handle() { return ( getHandler( data.type ) ) ( * this ); }

}; /* class Packet */

PS: Вы можете покопаться в Google и взять cdecl / c ++ decl. Это очень полезные программы. Особенно при игре с указателями функций.

например:.

c++decl> declare foo as function(int) returning pointer to function returning void
void (*foo(int ))()
c++decl> explain void (* getHandler( int  ))( const int & );
declare getHandler as function (int) returning pointer to function (reference to const int) returning void
0 голосов
/ 01 декабря 2008

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

class Packet { ... };

typedef Packet* (*packet_creator)();

class Factory {
public:
  bool add_type(int id, packet_creator) {
    map_[id] = packet_creator; return true;
  }
};

template<typename T>
class register_with_factory {
public:
  static Packet * create() { return new T; }
  static bool registered;
};

template<typename T>
bool register_with_factory<T>::registered = Factory::add_type(T::id(), create);

class MyPacket : private register_with_factory<MyPacket>, public Packet {
//... your stuff here...

  static int id() { return /* some number that you decide */; }
};
0 голосов
/ 01 декабря 2008

Вам нужно посмотреть фабричный шаблон.

Фабрика просматривает поступающие данные и создала объект правильного класса для вас.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...