4 Stimmen

C++ Design - Netzwerkpakete und Serialisierung

Ich habe, für mein Spiel, eine Klasse Packet, die Netzwerk-Paket darstellt und besteht im Grunde aus einem Array von Daten, und einige reine virtuelle Funktionen

Ich möchte dann z. B. Klassen haben, die sich von Packet ableiten: StatePacket, PauseRequestPacket, usw. Jede dieser Unterklassen würde die virtuellen Funktionen Handle() implementieren, die von der Netzwerk-Engine aufgerufen werden, wenn eines dieser Pakete empfangen wird, so dass sie ihre Aufgabe erfüllen kann, sowie mehrere get/set-Funktionen, die Felder im Datenfeld lesen und setzen würden.

Ich habe also zwei Probleme:

  1. Die (abstrakte) Klasse Packet müsste kopierbar und zuweisbar sein, aber ohne Slicing, wobei alle Felder der abgeleiteten Klasse erhalten bleiben müssten. Es wäre sogar möglich, dass die abgeleitete Klasse keine zusätzlichen Felder hat, sondern nur Funktionen, die mit dem Array der Basisklasse zusammenarbeiten würden. Wie kann ich das erreichen?
  2. Bei der Serialisierung würde ich jeder Unterklasse eine eindeutige numerische ID geben und diese dann vor der eigenen Serialisierung der Unterklasse in den Stream schreiben. Aber für unserialization, wie würde ich die Lese-ID auf die entsprechende Unterklasse zuordnen, um es zu instanziieren?

Wenn jemand eine Klarstellung wünscht, einfach fragen.

--Danke


Bearbeiten: Ich bin nicht ganz zufrieden damit, aber das habe ich hinbekommen:

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

Wenn jemand die Zeit hat, sie sich anzusehen und Verbesserungsvorschläge zu machen, wäre ich sehr dankbar.


Ja, ich kenne das Factory-Pattern, aber wie würde ich es codieren, um jede Klasse zu konstruieren? Eine riesige Switch-Anweisung? Das würde auch die ID für jede Klasse duplizieren (einmal in der Fabrik und einmal im Serialisierer), was ich gerne vermeiden möchte.

9voto

Johannes Schaub - litb Punkte 479831

Zum Kopieren müssen Sie eine Klonfunktion schreiben, da ein Konstruktor nicht virtuell sein kann:

virtual Packet * clone() const = 0;

Die jede Packet-Implementierung wie folgt implementiert:

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

zum Beispiel für StatePacket. Paketklassen sollten unveränderlich sein. Sobald ein Paket empfangen wurde, können seine Daten entweder herauskopiert oder weggeworfen werden. Ein Zuweisungsoperator ist also nicht erforderlich. Machen Sie den Zuweisungsoperator privat und definieren Sie ihn nicht, was die Zuweisung von Paketen effektiv verbieten wird.

Für die De-Serialisierung verwenden Sie das Factory-Muster: Erstellen Sie eine Klasse, die den richtigen Nachrichtentyp anhand der Nachrichten-ID erstellt. Hierfür können Sie entweder eine Switch-Anweisung über die bekannten Nachrichten-IDs oder eine Map wie diese verwenden:

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;

In der Tat sollten Sie prüfen, ob die ID wirklich bekannt ist und dergleichen. Das ist nur die grobe Idee.

1voto

Mr.Ree Punkte 8112

Warum machen wir, mich eingeschlossen, so einfache Probleme immer so kompliziert?


Vielleicht liege ich hier falsch. Aber ich muss mich fragen: Ist dies wirklich das beste Design für Ihre Bedürfnisse?

Im Großen und Ganzen kann die reine Funktionsvererbung besser durch Funktions-/Methodenzeiger oder Aggregation/Delegation und die Weitergabe von Datenobjekten erreicht werden als durch Polymorphismus.

Polymorphismus ist ein sehr mächtiges und nützliches Werkzeug. Aber es ist nur eines von vielen Werkzeugen, die uns zur Verfügung stehen.


Es sieht so aus, dass jede Unterklasse von Packet ihren eigenen Marshalling- und Unmarshalling-Code benötigt. Vielleicht den Marshalling/Unmarshalling-Code von Packet erben? Vielleicht ihn erweitern? Alles zusätzlich zu handle() und was sonst noch benötigt wird.

Das ist eine Menge Code.

Obwohl es wesentlich umständlicher ist, könnte es kürzer und schneller sein, die Daten von Packet als struct/union-Attribut der Klasse Packet zu implementieren.

Das Ein- und Ausmarschieren würde dann zentralisiert.

Abhängig von Ihrer Architektur könnte es so einfach sein wie write(&data). Vorausgesetzt, es gibt keine Big/Little-Endian-Probleme zwischen Ihren Client/Server-Systemen und keine Probleme mit dem Auffüllen. (Z.B. sizeof(data) ist auf beiden Systemen gleich.)

Schreiben(&Daten)/Lesen(&Daten) ist eine fehleranfällige Technik . Aber es ist oft ein sehr schneller Weg, den ersten Entwurf zu schreiben. Später, wenn es die Zeit erlaubt, können Sie ihn durch individuellen, attributtypbasierten Marshalling/Unmarshalling-Code ersetzen.

Auch: Ich habe mir angewöhnt, Daten, die gesendet/empfangen werden, als struct zu speichern. Sie können eine Struktur mit operator=() bitweise kopieren, was manchmal SEHR hilfreich war! Obwohl vielleicht nicht so sehr in diesem Fall.


Letztendlich werden Sie eine Schalter Anweisung irgendwo auf diesem Unterklassen-Id-Typ. Die Factory-Technik (die an sich schon sehr leistungsfähig und nützlich ist) übernimmt diese Umschaltung für Sie, indem sie die erforderliche clone()- oder copy()-Methode bzw. das entsprechende Objekt nachschlägt.

OR können Sie das in Packet selbst tun. Sie könnten einfach etwas so Einfaches wie verwenden:

( getHandlerPointer( id ) ) ( this )


Ein weiterer Vorteil dieses plumpen Ansatzes (Funktionszeiger), abgesehen von der schnellen Entwicklungszeit, ist, dass Sie nicht ständig ein neues Objekt für jedes Paket zuweisen und löschen müssen. Sie können ein einzelnes Paketobjekt immer wieder verwenden. Oder einen Vektor von Paketen, wenn man sie in eine Warteschlange stellen will. (Allerdings würde ich das Packet-Objekt löschen, bevor ich read() erneut aufrufe! Nur um sicher zu gehen...)

Je nach der Dichte des Netzwerkverkehrs in Ihrem Spiel kann die Zuweisung/Deallokation teuer werden. Andererseits, Vorzeitige Optimierung ist die Wurzel allen Übels. Und Sie können auch einfach Ihre eigenen Operatoren für das Neu-/Löschen erstellen. (Noch mehr Codierungsaufwand...)


Was Sie (mit Funktionszeigern) verlieren, ist die saubere Trennung der einzelnen Pakettypen. Insbesondere die Möglichkeit, neue Pakettypen hinzuzufügen, ohne bereits vorhandenen Code/Dateien zu ändern.


Beispiel-Code:

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: Du könntest mit Google herumstöbern und cdecl/c++decl ausfindig machen. Das sind sehr nützliche Programme. Besonders wenn man mit Funktionszeigern herumspielt.

Z.B.:

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

0voto

Martin York Punkte 245363

Sie müssen das Factory Pattern nachschlagen.

Die Fabrik prüft die eingehenden Daten und erstellt ein Objekt der richtigen Klasse für Sie.

0voto

coryan Punkte 1173

Um eine Factory-Klasse zu haben, die nicht über alle Typen im Voraus Bescheid weiß, müssen Sie ein Singleton bereitstellen, in dem sich jede Klasse selbst registriert. Ich verstehe die Syntax für die Definition statischer Mitglieder einer Vorlagenklasse immer falsch, also nicht einfach ausschneiden und einfügen:

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 */; }
};

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X