Protokoll-Umsetzung in C++ - eure Meinung



  • Hallo,

    da ich C++ nicht oft programmiere und gerne lernen möchte vernünftigen Code zu erstellen, bitte ich Euch mir Euer Feedback zur folgenden Umsetzung eines Protokolls zu geben:

    Gegeben ist ein Protokoll, dass vier Datentypen kennt:

    Bit read -only
    Bit read/write
    Word read-only
    Word read/write

    Dann gibt es eine einige Funktionen zum Lesen und Schreiben der Daten. Jeweils:

    ReadSingle...
    WriteSingle...
    ReadMultiple...
    WriteMultiple...

    Das Protokoll besitzt ein eigenes Register in dem die Daten gehalten werden.
    Es wird immer ein Paket gesendet, dass den Funktionscode (jede Funktion hat eine ID-Nr.) enthält und anschließend die (Start-)Adresse (im Register). Dann kommen je nach Funktion, die Anzahl der Daten und/oder die zu schreibenen Werte.

    Als Antwort kommt dann ein Datenpaket mit dem selben Anfang (Funktionscode + Adresse) und dann entweder die gelesenen Daten oder als Schreib-Bestätigung die Anzahl der Daten.

    So, nun zur Implementierungs-Idee:

    Da das Protokoll mehrere Funktionen kennt, jetzt erstmal aber nur einige davon gebraucht werden, wollte ich das ganze flexibel halten, so dass Funktionen später nachgepflegt werden können, ohne das bestehender Code neu angefasst werden muss.

    Daher habe ich mir das Command-Pattern genauer angesehen und als passend befunden. Die Umsetzung folgt im nächsten Post...



  • Zunächst einmal alles in:

    namespace Protocols{
       namespace MyProtocol{
    
       };
    }:
    

    Erstmal die Datentypen:

    Datatype.h:
    
    namespace Datatype{
    
    // Basisklasse aller Datentypen
    template <class T>
    class Datatype{
    
    public:
        Datatype() : m_address(0), m_value(0){}
        Datatype(unsigned int address) : m_address(address), m_value(0){}
        virtual ~Datatype();
    public:
        unsigned int GetAddress() const { return m_address; }
        T            GetValue()   const { return m_value;   }
        void         SetValue(T)        { m_value = T;      }  
    private:
        unsigned int m_address;  // Adresse bzw. Startadresse der Daten
        T            m_value;
    };
    };
    

    Jetzt die eigentlichen Datentypen...

    class Type1:Datatype<bool>        {/*...*/}; // Single Bit read-only
    class Type2:Datatype<bool>        {/*...*/}; // Single Bit r/w
    class Type3:Datatype<unsigned int>{/*...*/}; // 16Bit Word read-only
    class Type4:Datatype<unsigned int>{/*...*/}; // 16Bit Word r/w
    

    Dann die Funktionen:

    Functions.h:
    
    #include <memory> //shared_ptr
    
    // Basisklasse der Funktionen
    template<class T>
    class Function{
    public:
       Function(const std::tr1::shared_ptr<T>& data, unsigned int id)
           : m_data(data), m_funcId(id) {};
       virtual ~Function() {};
    public:
       virtual void Execute(MySocket& socket) = 0;
           // Nutzt einen Socket-Wrapper, der eine Socket-Implementierung kapselt.
           // Bildet das Datenpaket, schickt Anfrage raus, wertet Antwort aus und 
           // schreibt ggf. Daten in Datentyp rein. 
    private:
       unsigned int           m_funcId;
       std::tr1::shared_ptr<T> m_data;
    };
    
    // einzelnes Typ 1-Element (read-only Bit) lesen 
    class ReadType1:Function<const Datatype::Type1> {/*...*/};
    // einzelnes Typ 2-Element (r/w Bit) schreiben 
    class WriteType2:Function<Datatype::Type2> {/*...*/};
    // Mehrere Typ 2-Elemente schreiben:
    class WriteMultipleType2:Function<std::vector<Datatype::Type2>> {/*...*/};
    //...
    

    Anwendung/Ablauf:

    Es wird ein Cleint erstellt, der ein SocketVerbindung aufbaut. Die bevorzugte Socket-Implementierung wird mittels MySocket gekapselt. Will ich ein Bit lesen, so erstelle ich ein entsprechendes Objekt von Typ1, Erstelle mir eine Object der Funktion ReadType1() mit Referenz auf das Datenobjekt. und gebe es dem Client. Der ruft Function.Execute(tcpSocket) auf. Bei Schreib-Funktionen kann ich dann den Wert mittels GetValue() abrufen. Im Fehlerfall fliegt eine Exception.



  • Ok, bei den Datentypen benörige ich ja keine Basisklasse. Daher kann ich das wie folgt vereinfachen:

    Habe außerdem wegen der 16Bit Definition für die Größe eines Wortes im Protokoll, dass unsigned int gegen std::tr1::uint16_t getauscht.

    Datatype.h:
    #include <tr1/cstdint> 
    
    namespace Datatype  {
    
    template <class T>
    class Datatype{
    
    public:
        Datatype() : m_address(0), m_value(0) {}
        Datatype(unsigned int address, T value = 0) : m_address(address), m_value( value){}
    public:
        unsigned int GetAddress() const { return m_address; }
        T            GetValue()   const { return m_value;   }
        void         SetValue (T value) { m_value = T;      }
        void         operator=(T value) { m_value = T;      }
                     operator T&()      { return m_value;   } 
    private:
        unsigned int m_address;  // Adresse bzw. Startadresse der Daten
        T            m_value;
    };
    
    //Jetzt die eigentlichen Datentypen...
    typedef Datatype<bool>               Type1; // Single Bit read-only
    typedef Datatype<bool>               Type2; // Single Bit r/w
    typedef Datatype<std::tr1::uint16_t> Type3; // 16Bit Word read-only
    typedef Datatype<std::tr1::uint16_t> Type4; // 16Bit Word r/w
    //...
    
    }; // namespace Datatype
    


  • Hmm, keiner eine Anmekrung dazu? Zuviel? Zu allgemein gehalten? 😕

    Wäre über Feedback wirklich sehr dankbar! 🙂 Kann man das so machen? Was würdet Ihr anders machen?

    Oder ist irgendwas noch total verkorkst und unklar?

    Gruß Roger Wilco



  • Kann es sein, dass du Bit und Byte verwechselst? bool ist nicht ein bit, sondern byte



  • Nein, bool hat zwei Zustände: 0/1 oder true/false. Also ein Bit.

    http://www.cppreference.com/wiki/keywords/bool

    Byte sind 8 Bit. Für ein Byte gibt es aber in C++ kein Datentyp. Ein char ist aber i.d.R. ein Byte. Oder im TR1 gibt es:

    #include <tr1/cstdint>
    
    std::tr1::int8_t  byte;  // 8 Bit mit Vorzeichen
    std::tr1::uint8_t byte;  // 8 Bit ohne Vorzeichen
    

    Oder bei MS Visual C++:

    __int8           byte; // 8 Bit mit Vorzeichen
    unsigned __int8  byte; // 8 Bit ohne Vorzeichen
    


  • bool kann zwar nur 2 Zustände haben, belegt aber in der Regel mindestens ein Byte (bzw. eine char -Größe) Speicher (d.h. sizeof(bool) >= 1 ). Zum einen, weil der Standard nicht unbedingt die Existenz von Bits vorraussetzt, zum anderen weil ein Byte / ein char nunmal die kleinste addressierbare Einheit ist und sich auf Bitebene so blöd rechnet (stell dir vor, du hättest einen bool , der ein Bit belegt und im Anschluss einen char , die die restlichen 7 Bits und das erste Bit des nächsten Byts belegt. Das wird eine fröhliche Rechnerei, wenn man auf den char zugreifen will).



  • Ach so ja, Ok auf Bitebene.

    Ich war jetzt ganz auf den Wertebereich der C++-Datentypen fixiert.

    Wie die Datenpakete des Protokolls nacher aufgebaut werden habe ich jetzt hier erstmal ganz außen vor gelassen. Da kommt ja auch noch das Thema Endianess dazu, wo ich auch nicht weiß, wie ich das mache... Das ganze wird in den Funktionen gekapselt (Execute()).



  • Ich stecke gerade im Thema "Erstellung der Datenpakete" fest:

    Eigentlich hatte ich daran gedacht die hton/ntoh Funktionen für die richtige Endianess zu nutzen. Das Protokoll schreibt nämlich Network-Byteorder für seine Wörter und Doppelwörter vor.

    Nun bin ich bei der Suche auf folgende Aussage von Tachyon getoßen:

    Tachyon schrieb:

    htons (host to net short) und htonl (host to net long) sind erstmal nur dafür da, IP-Header Standardkonform zu befüllen und helfen einem nur bei 16 Bit bzw. 32 Bit unsigned Integer Werten. Für Endian-Konvertierung sind diese nur sehr bedingt geeignet und auch nicht gedacht.
    Weitgehende plattfromunabhängigkeit ist es am Besten, seine Werte als ASCII-String zu übertragen/speichern.

    Wie die Datenpakete aufgebaut sind steht nun aber fest. Somit muss ich die Endianess der Plattform beachten und ggf. konvertieren. Wie mache ich das nun am besten? 😕



  • Roger Wilco schrieb:

    Byte sind 8 Bit. Für ein Byte gibt es aber in C++ kein Datentyp.

    sizeof( char ) ist per Definition 1, d.h. char deckt immer und überall genau ein Byte ab.

    Wenn du die Daten im ASCII Format überträgst stellt Endianess kein Problem für dich da, da die Konvertierung vom Host unter Berücksichtigung seiner Endianess durchführt wird.
    Wenn du die Daten allerdings binär überträgst musst du im Protokoll spezifizieren, welches Datenformat (z.B. Endianess) benutzt wird. Der Host muss seine eigene Endianess kennen und, falls nötig, eine Konvertierung durchführen.

    boost definiert für bekannte CPU Typen die Konstanten
    BOOST_LITTLE_ENDIAN/BOOST_GIB_ENDIAN und BOOST_BYTE_ORDER. Wenn du boost nicht benutzen kannst oder willst oder eine exotische CPU hast kannst du die Defines selbst setzen.
    Eine andere Möglichkeit wäre vielleicht folgendes:

    char IntRep[sizeof( int )] = { 0 };
    IntRep[0] = 1;
    
    if( *(int*)( IntRep ) == 1 )
    {
       // little endian
    }
    else
    {
       // big endian
    }
    

    Aber damit wäre ich lieber vorsichtig, ist mehr ein Hack als anerkannte Endianess Bestimmung 😃



  • DocShoe schrieb:

    sizeof( char ) ist per Definition 1, d.h. char deckt immer und überall genau ein Byte ab.

    Falscher Schluss. sizeof gibt nicht die Größe in Bytes zurück, sondern in char s. Demnach ist sizeof(char) immer per Definition 1, trotzdem kann char auch mehrere Byte groß sein.



  • Mein größter Kritikpunkt an der Umsetzung: Protocol wird mit nur einem L geschrieben.

    Den Rest habe ich mir nicht angesehen. 😉



  • Fellhuhn schrieb:

    Protocol wird mit nur einem L geschrieben.

    Zum Glück geht es nicht um Protocol sondern um Protokoll. 🙂



  • Bei mir ist es so, dass ich ein gegebenes Protokoll habe. Das möchte ich umsetzen. Es wird immer ein Datenpaket als Anfrage abgeschickt und es kommt immer genau ein Datenpaket (mit gleichem Header) als Antwort.

    Die Datenpakete setzten sich aus Bytes (z.B. Funktons-ID), Wörtern (16Bit - Register-Adressen oder Daten) und Doppelwörtern (32Bit - Daten) zusammen. Per Definition ist die Byteorder "Big-Endian". Eigentlich soll das ganze auf Windows-PCs laufen, aber ich möchte lernen portabel zu schreiben. Mein Kollege schreibt mit VC6.0 immer mit MFC und im C-Klassen-Stil. Ich selber benutze VS2008. Daher wollte ich hier nach Feedback fragen, um es "richtig(er)" zu machen...

    Die Funktions-Klassen besitzen die Methode Execute(). Hier soll das Datenpaket geschnürrt werden. Ich dachte da an sowas:

    #include "WinSock2.h" //hton/ntoh - nur Windows
    #include <sstream> //stringstream
    
    void Funktion::Execute(){
       std::stringstream data(std::stringstream::out |
                              std::stringstream::in  |
                              std::stringstream::binary);
    
       data << m_funcId                 // unsigned __int8
            << htons(m_address)         // unsigned __int16
            << htonl(m_data);           // unsigned __int32
    
       m_socket.SendRequst(data.str());
    
       // Auf Antwort warten...
       // Antwort lesen und auswerten
       // evtl. Daten ins Datenobjekt schreiben oder Exception werfen...
    }
    


  • Fellhuhn schrieb:

    Mein größter Kritikpunkt an der Umsetzung: Protocol wird mit nur einem L geschrieben.

    😮 Ups, habe ich geändert...

    Fellhuhn schrieb:

    Den Rest habe ich mir nicht angesehen. 😉

    Schade... 😞


Anmelden zum Antworten