Suche Implentierungsideen
-
Hallo zusammen,
ich kämpfe momentan mit einem - da bin ich mir eigentlich sicher- relativ banalem Problem. Irgendwie stehe ich aber auf dem Schlauch und bekomme nichts vernünftiges hin.
Vielleicht hat ja von euch jemand eine zündende Idee
Es geht um folgendes:
Ich brauche eine Klasse, die UDP Datenpakete wrapped.
Momentan habe ich das folgendermaßen aufgezogen:
Meine Basisklasse enthält im Grunde nichts weiter als einen char*, den ich brauche um meine Daten später an SDLNet_UDP_Send weitergeben kann (benötigt seine Daten als char*)#pragma once /// SYSTEM INCLUDES /// PROJECT INCLUDES /// LOCAL INCLUDES /// FORWARD DECLARATIONS namespace netCodeCommon { class CUDPPacketData { //////////////////////// PUBLIC ////////////////////////// public: /// Constructor CUDPPacketData( ) : mpData(0) {} /// Destructor virtual ~CUDPPacketData() {} ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS ///////////// MEMBERS /////////////////////// PROTECTED /////////////////////// protected: ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS const char* BaseData() const { return mpData; } ///////////// MEMBERS char* mpData; /////////////////////// PRIVATE ////////////////////////// private: ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS ///////////// MEMBERS }; } // netCodeCommonDavon leite ich mir meine UDP Paket Basisklasse ab, die als pure virtual Klasse die Interfaces zur Verfügung steht, die letztendlich von den Implentierungsklassen implementiert werden:
#pragma once /// SYSTEM INCLUDES /// PROJECT INCLUDES /// LOCAL INCLUDES #include "UDPPacketData.hpp" /// FORWARD DECLARATIONS namespace netCodeCommon { //template <typename T = void> class CUDPPacket : public CUDPPacketData { //////////////////////// PUBLIC ////////////////////////// public: /// Constructor CUDPPacket( ) { } /// Destructor virtual ~CUDPPacket() { PacketReceived(); } ///////////// METHODS void PacketReceived() { if ( 0 != mpData ) delete [] mpData; } ///////////// OPERATORS ///////////// ACCESSORS const char* PacketData() { return mpData; } // virtual const T* Data() const = 0; virtual const size_t DataLen() const = 0; virtual const size_t TotalLen() const = 0; virtual const int ID() const = 0; virtual void RestoreFromData(char* data) = 0; ///////////// MEMBERS /////////////////////// PROTECTED /////////////////////// protected: ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS ///////////// MEMBERS int mID; /////////////////////// PRIVATE ////////////////////////// private: ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS ///////////// MEMBERS }; } // netCodeCommonSo, jetzt geht es ans Eingemachte.
Ich habe für jede Aufgabe ein eigenes UDP Paket, ergo auch eine eigene Klasse.
Nehmen wir mal exemplarisch meine Ping Klasse:#pragma once /// SYSTEM INCLUDES /// PROJECT INCLUDES #include <netCode/common/UDPPacket.tpp> #include <netCode/common/exception.hpp> /// LOCAL INCLUDES #include "UDPPacketIDs.h" /// FORWARD DECLARATIONS namespace netCodeCommon { struct S_PingData { unsigned int magicNumber; bool reply; }; class CUDPPing : public CUDPPacket // <S_PingData>, CAllocatableObject { //////////////////////// PUBLIC ////////////////////////// public: /// Constructor CUDPPing(char* data) : mID(Type_Ping) { RestoreFromData(data); } CUDPPing(unsigned int magicNumber, bool reply) : mID(Type_Ping) { Init(magicNumber, reply); } /// Destructor virtual ~CUDPPing() { if ( 0 != mpData ) delete [] mpData; } ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS virtual const S_PingData* Data() const { return reinterpret_cast<S_PingData*>(mpData + sizeof(mID)); } virtual const size_t DataLen() const { return sizeof(S_PingData); } virtual const size_t TotalLen() const { return sizeof(S_PingData) + sizeof(mID); } virtual const int ID() const { return mID; } virtual void RestoreFromData(char* data) { if ( 0 == data) return; unsigned int magicNumber = 0; bool reply = false; memcpy(&magicNumber, data+sizeof(mID) /* skip ID */, sizeof(magicNumber)); memcpy(&reply, data+sizeof(mID)+sizeof(magicNumber) /*skip ID & magicNumber*/, sizeof(reply)); Init(magicNumber, reply); } ///////////// MEMBERS /////////////////////// PROTECTED /////////////////////// protected: ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS ///////////// MEMBERS int mID; /////////////////////// PRIVATE ////////////////////////// private: ///////////// METHODS void Init(unsigned int magicNumber, bool reply) { mData.magicNumber = magicNumber; mData.reply = reply; mpData = new char[sizeof(mID) + sizeof(S_PingData)]; memcpy(mpData, &mID, sizeof(mID)); memcpy(mpData + sizeof(mID), &mData, sizeof(mData)); } ///////////// OPERATORS ///////////// ACCESSORS ///////////// MEMBERS S_PingData mData; }; } // netCodeCommonWie ihr sehen könnt allokiere ich beim Anlegen eines Objekts einen char array in der Größe meiner Datenstruktur und kopiere dann nacheinander alles per memcpy in den Array. Durch das händische Kopieren der einzelnen struct members umgehe ich evtl memory alignments, denn meine Daten müssen linear im Speicher liegen, dass SDL mein Paket korrekt verschicken kann.
So, prinzipiell funktioniert das auch, aber mir gefällt diese Art der Implementierung aus zweierlei Gründen nicht:
- Bei größeren Paketen wird das Kopieren der Daten aufgrund der einzelnen memcpys schnell unübersichtlich und fehleranfällig.
- Aus Performancesicht ist diese Lösung sicherlich auch suboptimal. Ausserdem kommts mir irgendwie stümperhaft vor

Es muss doch irgendetwas schickes geben, womit ich arbiträre Daten linear in den Speicher kriege und über einen char* ansprechen, und die Daten auch von einem char* wieder rekonstruieren kann (wenn das Paket am Zielrechner ankommt).
STL, Boost o.ä. bin ich selbstverständlich nicht abgeneigt.
Hat jemand vielleicht eine Idee?
Danke schonmal

Edit:
Oder was evtl sogar sinnvoller wäre: Gibts es irgendwelche Bibliotheken mit denen man Klassen per TCP/IP, bzw. UDP serialisieren kann?
-
was hast du gegen string?
-
An und für sich nichts, allerdings bläst sich dann mein Speicherverbrauch ziemlich auf.
Bei diesem Ping Paket würde es sicherlich gehen, aber ich habe auch Pakete, die floating point 4x4 Matrizen beinhalten, von welchen pro Sekunde etwa 180-200 Stück verschickt werden müssen.
Das wären bei einer Stringrepräsentation ~22kb/s - eine Menge Holz finde ich.Ausserdem müsste ich dann ziemlich viel hin und her casten.
Aber danke für den Ansatz - ich denke auf jeden Fall noch mal genauer drüber nach.
-
Die Bevölkerung ist geschockt. Volkard schläg einen String als Lösungsvorschlag für die Serialisierung vor. Nach ansicht führender Serialisierungsexperten eine komplette Fehleinschätzung des Themas mit weitreichenden Konsequenzen. Ich schalte jetzt live nach Komutischtun, wo die Lage zu eskalieren droht.
-
Ich beschäftige mich jetzt mal etwas genauer mit Serialisierung im Allgemeinen.
Ich denke hier oder hier werde ich schon irgendwie weiterkommen.Aber sagt ruhig bescheid, wenn euch was gutes einfällt

-
nein, nein. ich meine doch nur string und keine serialisierung.
string ist eine sequenz von chars. sie kann auch nullen enthalten. und man kann effizient hintendranhängen. string einfach nur als datenspeicher klasse, die dir das new/delete und wachsenlassen abnimmt.outputString.append(reinterpret_cast<char const*>(&magicNumber),sizeof(magicNumber));und wenn das paket dann weg soll
send(socket,outputString.begin(),outputString.size());oder so.
kann mir nicht vorstellen, daß dabei irgendwas merklich teurer wird, als wenn man den speicher selber anfaßt und memcpy macht.
in der nähe tollen sich noch vector<char> mit push_back() und ostringstream mit ganz normalem write.
-
Warum nicht std::vector<char> ? Wirkt für mich natürlicher als std::string.
Simon
-
@volkard: Ah, jetzt hab ichs kapiert

Das geht dann ja auch in die Richtung was die Serialisierungs Sektion bei Parashift erklärt - nur halt eben mit nem String.
Gefällt mir...werd mal bischen rumspielen und schauen, was sich am besten für mich eignet - String, vector oder streamDanke

-
theta schrieb:
Warum nicht std::vector<char> ?
Weil es zum Teil wahnsinnig mühsam ist, in einen
std::vector<char>zu schreiben. Probier mal das aus, was volkard im Beispielcode gezeigt hat:char const* first = reinterpret_cast<char const*>(&magicNumber); char const* last = first + sizeof(magicNumber); outputVector.insert(outputVector.end(), first, last);-> Gah!
Grüssli

-
theta schrieb:
Warum nicht std::vector<char> ?
weil ich's am häufigsten mit string gesehen habe, fiel mir das spontan ein. aber im moment glaube ich, daß ostringstream naheliegender als string ist. wie gut, daß ich das problem nicht habe und Hydrael es rausfinden muß. hehe.
-
volkard schrieb:
theta schrieb:
Warum nicht std::vector<char> ?
weil ich's am häufigsten mit string gesehen habe, fiel mir das spontan ein. aber im moment glaube ich, daß ostringstream naheliegender als string ist. wie gut, daß ich das problem nicht habe und Hydrael es rausfinden muß. hehe.
Werde heute Nacht irgendwann bescheidgeben wie es geklappt hat - aber ausm Bauch heraus denke ich, dass es mit nem ostringstream ziemlich gut klappen müsste

-
ja, da hat Dravere recht.
aber ein wenig später schreibt man sich eh wrappers.template<typename T> writeAny(outputVector<char>& v,T& magicNumber){ char const* first = reinterpret_cast<char const*>(&magicNumber); char const* last = first + sizeof(magicNumber); outputVector.insert(outputVector.end(), first, last); }und später
writeAny(outputVector,magicNumber);dann ists wieder egal.
-
@volkard,
Auch wieder wahr.@Hydrael,
Bin gespannt auf deine Testergebnisse mit demstringstream. Ich persönlich hatte mal einen Fall, wo ichstd::stringObjekte zusammen setzen musste. Seither weiss ich, dass das einfache zusammenfügen vonstd::stringObjekte oft schneller ist, wenn man keinstringstreamverwendet, sondern direkt über diestd::stringMöglichkeiten geht.Grüssli
-
Warum gebt ihr nicht gleich den reinterpret_cast<char const*>(&magicNumber) an den Socket?
-
Themen-Überblick (Aktual schrieb:
Warum gebt ihr nicht gleich den reinterpret_cast<char const*>(&magicNumber) an den Socket?
Weil es ja im Grunde nicht nur um die magicNumber geht, sondern um komplette datenstrukturen. Kann also durchaus auch sowas sein:
struct foo { char bar; int foo1; short bar1; }Und da kann man ganz schnell in memory alignment Geschichten reinlaufen.
-
Die hast du bei memcpy und volkards Methode auch, da macht ihr ja auch nur nen reinterpret cast.
-
So, ich habe jetzt mal ein wenig herumprobiert - letzendlich bin ich beim std::strstream hängengeblieben, weil ich beim stringstream das Problem hatte, dass er die Daten als strings reinterpretiert und ich somit bei den Binärdaten bullshit herausbekommen habe.
Mit dem strstream funktioniert die Sache tadellos und ist auch schön einfach implementierbar

Einen herzlichen Dank an alle Helfer - die Lösung gefällt mir jetzt echt gut.
Einzig zur Performance kann ich momentan noch nichts genaues sagen, da ich die im derzeitigen Projektstand noch nicht realistisch messen kann.Wen es interessiert...meine Klassen sehen jetzt folgendermaßen aus (diesmal am Beispiel eines anderen Pakets, so dass zu sehen ist, wie ich mit strings umgehe):
Basisklasse
#pragma once /// SYSTEM INCLUDES #include <strstream> /// PROJECT INCLUDES /// LOCAL INCLUDES /// FORWARD DECLARATIONS namespace netCodeCommon { class CUDPPacket { //////////////////////// PUBLIC ////////////////////////// public: /// Constructor CUDPPacket( ) { } /// Destructor virtual ~CUDPPacket() { } ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS virtual const size_t DataLen() const = 0; virtual const size_t TotalLen() const = 0; const int ID() const { return mID; } virtual const std::strstream& Serialize() = 0; virtual void Deserialize(const std::strstream& data) = 0; ///////////// MEMBERS /////////////////////// PROTECTED /////////////////////// protected: ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS ///////////// MEMBERS int mID; std::strstream mSerializationStream; /////////////////////// PRIVATE ////////////////////////// private: ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS ///////////// MEMBERS }; } // netCodeCommonAbgeleitete Klasse
#pragma once /// SYSTEM INCLUDES #include <string> /// PROJECT INCLUDES #include <netCode/common/UDPPacket.tpp> #include <netCode/common/exception.hpp> /// LOCAL INCLUDES #include "UDPPacketIDs.h" /// FORWARD DECLARATIONS namespace netCodeCommon { struct S_RegisterPlayerData { S_RegisterPlayerData() : magicNumber(0), playerName("") { } S_RegisterPlayerData(unsigned int magic, const std::string& name) : magicNumber(magic), playerName(name) { } unsigned int magicNumber; std::string playerName; const size_t size() const { return sizeof(magicNumber) + playerName.length(); } }; class CUDPRegisterPlayer : public CUDPPacket { //////////////////////// PUBLIC ////////////////////////// public: /// Constructor CUDPRegisterPlayer(char* data, size_t len) { mID = Type_RegisterPlayer; mSerializationStream.write(data, len); Deserialize(mSerializationStream); } CUDPRegisterPlayer(unsigned int magicNumber, const std::string& playerName) { mID = Type_RegisterPlayer; mData.playerName = playerName; mData.magicNumber = magicNumber; } /// Destructor virtual ~CUDPRegisterPlayer() { } ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS virtual const S_RegisterPlayerData& Data() const { return mData; } virtual const size_t DataLen() const { return mData.size() + sizeof(mID); } virtual const size_t TotalLen() const { return mData.size() + sizeof(mID) + sizeof(size_t); } virtual const std::strstream& Serialize() { size_t length = mData.playerName.length(); mSerializationStream.clear(); mSerializationStream.write(reinterpret_cast<char*>(&mID), sizeof(mID)); mSerializationStream.write(reinterpret_cast<char*>(&mData.magicNumber), sizeof(mData.magicNumber)); mSerializationStream.write(reinterpret_cast<char*>(&length), sizeof(length)); mSerializationStream.write(mData.playerName.c_str(), mData.playerName.length()); mSerializationStream.seekg(0, std::ios::beg); return mSerializationStream; } virtual void Deserialize(const std::strstream& data) { size_t length = 0; mSerializationStream.read(reinterpret_cast<char*>(&mID), sizeof(mID)); mSerializationStream.read(reinterpret_cast<char*>(&mData.magicNumber), sizeof(mData.magicNumber)); mSerializationStream.read(reinterpret_cast<char*>(&length), sizeof(length)); char *tmp = new char[length + 1]; mSerializationStream.read(tmp, length); tmp[length] = '\0'; mData.playerName = tmp; mSerializationStream.clear(); } ///////////// MEMBERS /////////////////////// PROTECTED /////////////////////// protected: ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS ///////////// MEMBERS /////////////////////// PRIVATE ////////////////////////// private: ///////////// METHODS ///////////// OPERATORS ///////////// ACCESSORS ///////////// MEMBERS S_RegisterPlayerData mData; }; } // netCodeCommon
-
Hydrael schrieb:
letzendlich bin ich beim std::strstream hängengeblieben, ...
std::strstreamist deprecated und es ist wahrscheinlich, dass in zukünftigen Versionen des Standards diese Klasse nicht mehr existieren wird.Hydrael schrieb:
weil ich beim stringstream das Problem hatte, dass er die Daten als strings reinterpretiert und ich somit bei den Binärdaten bullshit herausbekommen habe.
Wie hast du denn in den
std::stringstreamgeschrieben? Aber nicht mit demoperator <<? Es gibt nämlich einewriteMethode, welche du nutzen solltest:
http://www.cplusplus.com/reference/iostream/ostream/write/Den
std::stringstreamzudem mit dem Flagstd::ios::binaryerstellen, damit die Daten beim Schreiben nicht verändert werden.Grüssli
-
sieht gut aus.
aber warum nicht
virtual void Serialize(std::strstream& data) = 0; virtual void Deserialize(std::strstream& data) = 0;damits gleicher aussieht. der returnwert war eh fake. und const auf einem stream gleube ich dir nicht.
uups, nee, dann schon gleich
virtual void Serialize(std::ostrstream& data) = 0; virtual void Deserialize(std::istrstream& data) = 0;uups, dann gleich
virtual void Serialize(std::ostream& data) = 0; virtual void Deserialize(std::istream& data) = 0;dann kannst du mit derselben funktion auch in dateien speichern und daraus laden.
oder gar im netz nach einer sockestream-implementierungen suchen, falls es sowas gibt.
-
Dravere schrieb:
std::ios::binaryDas war es, was mir gefehlt hatte

@volkard: Der const stream war noch ein copy/paste überbleibsel - ist mir kurz nach dem posten auch aufgefallen.
Ansonsten hab ich deine Anmerkungen auch noch übernommen.
Wie man sieht bin ich nicht so sehr fit mit streams. Hatte die bisher immer nur für file I/O benötigt und da war mir oldschool fread/fwrite irgendwie immer lieber
Schönen Dank euch beiden