Template-Designfrage für Socket-Bibliothek



  • Hallo!
    ich will meine bestehende Socket-Bibliothek, die bisher TCP/UDP auf Win/Linux unterstützt erneuern. Bis jetzt basierte sie auf einer Klassenhierachie, mit virtuellen Methoden und dynamischer Polymorphie.
    Jetzt will ich noch ein wenig Performance rausholen und mich in Templates üben, deshalb habe ich jetzt meine neue Struktur umgestellt:

    Aus logischen Gründen und nicht permanentes Neudefinieren von Code usw. wird eine Vererbungshierachie bestehen bleiben, die so ausschaut:

    class basic_server { ... }; // abstrakte Klasse für Server
    class basic_netio { ... }; // abstrakte Klasse für I/O im Netzwerk
    class basic_client { ... }; // abstrakte Klasse für Clients
    
    // TCP-Server, implementiert die Schnittstellen
    class tcp_server : public basic_server, public netio { ... };
    // UDP-Server, implementiert die Schnittstellen
    class udp_server : public basic_server, public netio { ... };
    
    // TCP-Client, implementiert die Schnittstellen
    class tcp_client : public basic_client, public netio { ... };
    // UDP-Client, implementiert die Schnittstellen
    class udp_client : public basic_client, public netio { ... };
    
    // Server und Client in einem = Cloud in TCP
    class tcp_cloud : public tcp_client, public tcp_server { ... };
    // UDP-Cloud
    class udp_cloud : public udp_client, public udp_server { ... };
    

    Jedoch hat jede Klasse einen typedef von seiner Kategorie, namens socket_category . Der Trait-Typ wird denke ich mal nicht per Template variabel sein, sondern statisch im Code bleiben:

    class tcp_server {
    public:
       ...
       typedef typename server_tag socket_category;
       ...
    };
    

    Die tags sind ähnlich wie die Klassenhierachie aufgebaut:

    struct send_tag { };
    struct recv_tag { };
    
    struct server_tag : public send_tag, public recv_tag { };
    struct client_tag : public send_tag, public recv_tag { };
    
    struct cloud_tag : public server_tag, public client_tag { };
    

    Der Trait ist simpel definiert:

    template <typename SocketT>
    struct socket_traits {
       typedef typename SocketT::socket_category socket_category;
    };
    

    Dann habe ich eine Handling-Klasse namens NetIO , welche einen der oben gezeigten Socket-Typen als Template-Argument verlangt:

    template <typename SocketT>
    class NetIO { ... };
    

    Im Konstruktor wird eine Komposition durchgeführt, in dem ein Objekt des SocketT-Typen als Attribut angelegt wird.

    In NetIO sind sämtliche Methoden wie send() , recv() , accept() , connect() usw. definiert, in dynamischer Polymorphie wäre das also sowas wie eine "fette Schnittstelle", aber bei Fehl-Anwendung gibt's ja Kompilier statt Laufzeit-Fehler. Jede dieser Funktionen ruft die Methode des Socket-Typen auf, z.B. tcp_server::send() :

    // Attribut des Sockets: sock_;
    sock_ = tcp_server( [ Parameter ] );
    
    ...
    
    template <typename SocketT>
    const unsigned short NetIO<T>::send(const std::string data) const {
       return sock_.send(data, typename socket_traits<SocketT>::socket_category());
    }
    
    ...
    
    // Überladungen, je nach Socket-Trait
    
    const unsigned short tcp_server::send(const std::string &data, cloud_tag) {
       [ Implementierung zum allgemeinen Senden ]
    }
    
    const unsigned short tcp_server::send(const std::string &data, server_tag) {
       [ Implementierung zum senden von Server-Daten ]
    }
    
    ...
    

    Aufgerufen würde das gane dann so:

    NetIO<tcp_server> server;
    server.send("Das ist eine Nachricht!");
    

    Ich hoffe, das ist halbwegs verständlich, ist das erste mal, dass ich mit Designtechniken arbeite. 😞

    In tcp_server sind diverse send-Methoden definiert, je nach verschiedenen Traits, z.B. recv( ..., recv_tag) oder recv( ..., server_tag) .

    Ist dieses Design okay? Gibt es irgendwelchen Redundanzen, schreckliche Anfänger-Fehler oder Verbesserungsvorschläge? Ideen würde ich sehr gerne begrüßen, denn auf Basis dieser Bibliothek will ich ein Server-Programm bauen, was Sachen wie HTTP/FTP/IRC unterstützt.



  • Hab's mir noch mal überdacht. Ich habe eine Klasse Socket, die erst mal alles sein kann. An sich soll sie nur Basisfunktionalität haben wie close(), gethostbyname() usw.
    Als erstet Template-Argument soll ein Trait-Tag übergeben werden, z.B. socket<input_stream> oder socket<output_dgramm> . Der Socket soll dann nur Funktionen wie recv() für input_stream/input_dgram und für output_stream/output_dgram send() anbieten.
    So, jetzt die Frage: woher bekomme ich nun die flexible Implementierung und das Interface?
    Ich hatte folgende Idee:
    (C++0x)

    // Beispiel-Tags für Socket-Traits
    struct input_tag { };
    struct input_stream_tag : public input_tag { };
    
    // Trait-Struct
    template <typename sock_t>
    struct socket_traits {
    	typedef typename sock_t socket_category;
    };
    
    // Implementierung der eigentlichen Funktionalität der Sockets
    namespace impl {
    	unsigned short recv(input_tag&&) {
    	   // Implemtierung zum allgemeinen Datenempfangen
    	}
    
    	unsigned short recv(input_stream_tag&&) {
    	   // Implemtierung zum Datenempfangen von Streams
    	}
    
    // weitere Funktionen und Überladungen für statische Polymorphie, send() usw.
    }
    
    // Socket-Haupt-Klasse
    // erstes Argument ist der Trait, zweites der Typ für die Schnittstelle
    // bekommt die Schnittstelle durch öffentliche Vererbung
    template <typename trait_t, typename interface_t>
    class socket : public interface_t {
    public:
    	typedef typename trait_t socket_category;
    	// Deklaration von Handles und Standard-Funktionen
    };
    
    // Interface der Implementierung für Input-Stream-Sockets
    template <typename trait_t>
    class input_stream_interface {
    public:
    	typedef typename trait_t socket_category;
    
    	unsigned short recv() { // Ruft Implementierung auf
    		return impl::recv(typename socket_traits<trait_t>::socket_category());
    	}
          // Weitere Funktionen für input
    };
    
    // Verschönerungen
    typedef socket<input_stream, input_stream_interface<input_stream>> isockstream;
    
    // in der Anwendung...
    isockstream socket;
    
    [...]
    
    socket.recv();
    /* Ruft socket<input_stream, input_stream_interface<input_stream>>::recv(),
    vererbt aus input_stream_interface<input_stream>>::recv() auf,
    Implementierung wird durch Überladung polymorph in impl::recv() bestimmt */
    

    Damit hätte ich ein dynamisches Interface, aber alles statisch und zur Kompilierzeit, da ich eine performante Laufzeit haben will, wenn z.B. Stream-Sockets laufen.
    Ist dieser Weg günstig? Hat er zwischendurch irgendwelche Fallen? Sollte ich doch lieber auf Heap und dynamische Polymorphien und Klassenhierachien setzten?

    Ich fühle mich von den Möglichkeiten etwas überflutet. 😞



  • Ad aCTa schrieb:

    Ich fühle mich von den Möglichkeiten etwas überflutet. 😞

    Kann ich gut verstehen. Ich frage mich, ob Deine Struktur wirklich ausdrückt, was Sache ist, oder ob das gefrickelt ist. Also nicht gefrickelt im Sinne von fricky, sondern zu weit antigefrickelt. Ich würde überlegen, wie man es in C gemacht hätte, und dann Doppelcode zusammenlegen. Wenn ich mich recht täusche, sind Serversockets und Clientsockets keineswegs verwandt! Sie erben nicht von Socket.
    http://www.c-plusplus.net/forum/viewtopic-var-t-is-75672.html

    HTTP/FTP/IRC?
    Viele Protokolle sind zeilenbasiert, erast dei ganze Zeile ist eine zu verarbeitende Einheit. HTTP-Client BENUTZT(!!!!) einen Telnet-Client. FTP-Client benutzt einen Telnet-Client und einen normalen Client. IRC benutzt einen Telnet-Client. Weniger ist mehr. Noch weniger ist noch mehr. Nimm verrückte OO-Techniken immer, wenn Du einen Nutzen siehst, aber nicht aus Prinzip. Statt des Prinzips nimmste am besten dein Gefühl. Nachdem Du ein paarmal gegen die Wand gelaufen bist, weil es mal hier zu komplex wurde und mal da zu flach war, geht das schon. Ich denke, Du läufst hier gegen die K-Wand. Lauf, Ad aCTa, lauf! Eigentlich dürfte ich gar nicht warnen, denn die Erfahrung des Auas ist verdammt viel wert.

    gethostbyname? Aha! Ist für mich keine Methode eines HTTP-Clients. Nee, ehrlich nicht.



  • > gethostbyname? Aha! Ist für mich keine Methode eines HTTP-Clients. Nee, ehrlich nicht.

    Ja stimmt, ist etwas dumm. 🙂

    Das z.B. HTTP auf Telnet aufbaut, wusste ich gar nicht. Mit purem TCP müsste es doch auch klappen.
    Wie auch immer, ich werde mal ganz vorsichtig loslaufen, vielleicht komme ich bei entsprechender Geschwindigkeit ja ganz mit dem Kopf durch die K-Wand. Das trait-Prinzip hielt ich nur für sinnvoll, da ich später somit auch mit unabhängigen Funktionen wie sendtoallclients oder so arbeiten könnte, die ich mit Traits anpasse, Nicht-Element-Template-Funktionen soll man ja Elementfunktionen vorziehen, oder?
    Die getrennte Implementierung vom Interface ist zwar etwas kompliziert, aber ich wollte nicht in der Socket-Klasse jede Funktion deklarieren und definieren. Hätte ein Server jetzt connect(), würde er die Implementierung impl::connect() aufrufen, und spätestens da meckert der Compiler, dass der server_trait nicht in client_trait übersetzt werden kann. Aber gemäß dem, die Schnittstelle immer einfach zu halten, (und die Fehlermeldung versteht man als Kunde ja nicht), wollte ich lieber auf so ein "fat interface" verzichten und nur eine Schnittstelle anbieten, wo es auf dem Trait zutrifft.

    Zwar kann man den Trait als Template-Argument der Interface-Klasse übergeben, mir wäre aber eine implizitere Bindung zwischen den beiden lieber. Ich habe überlegt, die Interfaces (also Deklarationen der Funktionen wie recv()) in die Trait-Structs zu packen, aber dann hätte es ja irgendwie den Sinn von einem "Tag" verloren, oder?

    PS: Zu dem Link: Wirklich schöne Geschichte. Client und Server erben ja nicht von Socket, sondern von jeweils basic_client und basic_server. Als Gemeinsamkeit ist da die Klasse basic_netio in Multivererbung. Die soll ja lediglich ein gleiches Interface für gemeinsame Methoden bringen. Aber das Konzept habe ich ja mit meinem 2. eh verworfen.



  • Ad aCTa schrieb:

    Das z.B. HTTP auf Telnet aufbaut, wusste ich gar nicht. Mit purem TCP müsste es doch auch klappen.

    Ja, tut es gar nicht. Telnet-CLient ist auch nicht der richtige Name. Damit meinte ich Clients, die ReadLine können. Die haben bei mir eine eigene Klasse spendiert bekommen, weil ich mich ganz speziell um ReadLine kümmern wollte, damit das möglichst ohne Kopieren vonstatten geht.



  • Hm, was genau meinst du mit zeilenorientert? Nach dem Client-Request antwortet ja der Server mit einer Byte-Kette. ( unsigned char -Array). Also so:

    HTTP/1.1 200 OK\nServer: Test-Server\nContent-Length: 1024\n\n<html><head> ...
    [ in String verpacken ]
    send(string);
    

    Meinst du jetzt, dass ich nach jedem Newline ein send() durchführen sollte?



  • Ad aCTa schrieb:

    Hm, was genau meinst du mit zeilenorientert? Nach dem Client-Request antwortet ja der Server mit einer Byte-Kette. ( unsigned char -Array). Also so:

    HTTP/1.1 200 OK\nServer: Test-Server\nContent-Length: 1024\n\n<html><head> ...
    [ in String verpacken ]
    send(string);
    

    Meinst du jetzt, dass ich nach jedem Newline ein send() durchführen sollte?

    Nein, ja kein send() zu viel.
    Ich seh da erstmal Zeilen. Zeilen, die man kurz sind und mal lang. Ich möchte von meiner Client-Klasse erstmal Zeilen geschickt bekommen.


Anmelden zum Antworten