Funktionsweise von IO-Streams



  • Noch mal einen Tipp, da ich gerade Deinen anderen Thread hier gesehen habe.
    Dieses Buch über neben-läufige Pattern könnte für Dich interessant sein. Im Netz gibt es auch einiges zum Design von Programmen die viele Anfragen verarbeiten müssen - insbesondere zum Proactor-Pattern

    Und ansonsten liefert boost.asio eigentlich schon alles. Selbst wenn man es nicht benutzt, als Vorlage taugt es alle mal.

    So damit bist Du eine Weile beschäftigt 😉
    Werner



  • Okay, ich habe schon ma etwas weiter gemacht. Mei Interface schaut so aus:

    #include <iostream>
    #include "ionstream.hpp"
    
    int main() {
    	try {
    		net::endpoint server("127.0.0.1", 55555);
    		net::onstream nout(server, net::tcp);
    
    		if(!nout)
    			nout.error();
    
    		nout << " ahahl" << std::flush;
    		for(;;);
    
    	}
    	catch(const net::net_error& e) {
    		std::cerr << e.what() << "\nError code: " << e.number() << '\n';
    	}
    
    }
    

    Jetzt habe ich eine Frage:
    Wie funktioniert die Kommunikation zwischen Stream und Puffer beim operator <<()? Ich habe die Überladung leider nicht gefunden, ich nehme aber an, dass der Operator buffer.xsputn() aufruft, oder? Wenn das der Fall ist, was muss xsputn() (von meinem Puffer überladen) zurückgeben, damit der Stream auf badbit oder failbit gesetzt wird? Standardmäßig ist es ja die Anzahl der in den internen Puffer geschriebenen Zeichen, unabhängig davon, ob ein overflow() dazwischenkommt. Wie bewerkstellige ich das? Sind die beiden Methoden so okay?

    virtual std::streamsize xsputn(const Ch* p, std::streamsize n) {
    	setp(pbuf, pbuf + buflen());
    	std::streamsize i;
    	for(i = 0; i < n; i++)
    		if(sputc(p[i]) == -1)  // löst irgendwann overflow() aus
    			return 0; // ein Fehler ist passiert, bloß wie benachrichtigen?
    	return i;
    }
    
    virtual int_type overflow(int_type c = Tr::eof()) {
    	int_type result = ::send(handle, pbase(), pptr() - pbase(), 0); // den bis jetzt beschriebenen Puffer senden
    	bstate = WSAGetLastError(); // Pufferzustand retten
    	seekpos(0, std::ios_base::out); // pptr() auf 0 zurückstellen
    	sputc(c); // neues Zeichen in den Puffer schreiben
    	return result; // bei Fehler -1, sonst 0
    }
    

    Danke :xmas2:



  • Ach und die zweite Frage betrifft setbuf().
    Wie genau arbeitet die Funktion für gewöhnlich? Etwa so?

    virtual basic_sockbuf<Ch, Tr>* setbuf(Ch* p, std::streamsize n) {
    	std::cout << "PUFFER SETZEN: " << p << std::endl;
    	setp(p, p + n); // put-area setzen
    	setg(p, p, p + n); // get-area setzten
    	return this;
    }
    

    Wird die put/get-Area eigentlich nicht erst gesetzt, wenn eine Put/Get-Operation aufgerufen wird? Ich stehe da leider etwas auf dem Schlach. 😕



  • Kann mir da niemand helfen? 😞

    Ich will nur erreichen, dass das erst mal funktioniert:

    char buf[] = "Hello"; // = 6 byte
    net::basic_onstream nout;
    nout.rdbuf()->pubsetbuf(buf, 6); // --> setbuf() --> setp() UND setg() ?
    nout << "Ich bin eine Nachricht"; // operator <<() --> sputn() --> xsputn() --> sputc() --> overflow() bei 'i'
    

    Was genau verursacht setbuf()? Wird die get-Area UND die put-Area neu gesetzt, nicht aber pptr(), gptr(), eptr() usw.? Werden die dann erst durch setp/setg bei Aufrufen von xsgetn() und xsputn() beeinflusst?

    Und da hätte ich auch den nächsten Salad: der physikalische Puffer (Ch*) ist ein Heap-Array, welches im Destruktor frei gegeben wird. Wenn ich nun in setbuf() den Ch*-Zeiger auf das per Parameter erhaltene neue Ch*-Array umbiege, wird natürlicherweise das vom Destruktor mit delete [] gelöscht. Nur: wer garantiert, dass das kein Stack-Array ist? delete auf Stack ist ja undefiniert. Muss ich dann 2 Puffer anlegen (Ch* default_buf, *user_buf)?



  • Oh hallo - gleich drei Postings und so viele Fragen ...

    Ad aCTa schrieb:

    Wie funktioniert die Kommunikation zwischen Stream und Puffer beim operator <<()? Ich habe die Überladung leider nicht gefunden, ich nehme aber an, dass der Operator buffer.xsputn() aufruft, oder?

    Nein - das ist etwas anders. Solange Du nicht genau weißt, wie es funktioniert, vergisst bitte 'xsputn' zunächst mal.

    Ad aCTa schrieb:

    ... was muss xsputn() (von meinem Puffer überladen) zurückgeben, damit der Stream auf badbit oder failbit gesetzt wird?

    Mit der Methode overflow allein, kannst Du einen überladenen Streambuf realisieren. Mit dem Zeichen, das nicht mehr in den internen Buffer (s.u.) geschrieben werden kann, wird streambuf::overflow aufgerufen. Kommt die Methode mit EOF zurück, so liegt ein Fehler vor und der Stream, der den Buffer nutzt, setzt das failbit.

    Da Du aber auf jeden Fall einen Speicherbereich mit mehr als einem Zeichen für die Socketkommunikation benötigst, gehe ich gleich einen Schritt weiter.

    Du benötigst in dem basic_sockbuf ein Stück Memory (den internen Buffer), wo man Zeichen hineinschreiben kann, am besten als 'Member' des basic_sockbuf. Der Einfachheit halber nehme ich mal an, dass da ein std::vector< char_type > existiert. Dieser wird als interner Buffer benutzt. Das sieht im Prinzip so aus:

    #include <streambuf>
    #include <vector>
    
    template< typename Ch, typename Tr = std::char_traits< Ch > >
    class basic_sockbuf : public std::basic_streambuf< Ch, Tr >
    {
    public:
        basic_sockbuf()
            : m_buf( 400 ) // einfach mal 400 Zeichen reservieren
        {
            setp( &m_buf[0], &m_buf[0] + m_buf.size() - 1 ); // Put-Pointer auf den Buffer; ein Zeichen Platz lassen
        }
    protected:
        virtual int_type overflow( int_type m = traits_type::eof() )
        {
            const char* pend = pptr();
            if( !traits_type::eq_int_type( m, traits_type::eof() ) )
            {
                // es wurde noch ein Zeichen übergeben
                *pptr() = traits_type::to_char_type( m ); // hinten anfügen
                ++pend;
            }
            if( !senddata( pbase(), pend ) )
                return traits_type:eof(); // Fehler
            setp( &m_buf[0], &m_buf[0] + m_buf.size() - 1 ); // Put-Pointer neu setzen
            return traits_type::not_eof( m ); // ok
        }
    
    private:
        bool senddata( const char_type* first, const char_type* last )
        {
            // ... hier alle Zeichen im Intervall [first,last) über den Socket schicken
            return true; // ok
        }
        // --  Members
        std::vector< char_type > m_buf;
    };
    

    Das bezeichnet man als eine gepufferte Ausgabe. Wie Du siehst benötigt man dazu weder setbuf (hat damit nichts zu tun) noch xsputn.

    Ad aCTa schrieb:

    Mei Interface schaut so aus: ..

    Es sollte ungefähr so aussehen:

    #include <iostream>
    #include "sockbuf.hpp"
    
    int main() {
        try {
            basic_sockbuf< char > server; // ("127.0.0.1", 55555); Parameter nach Bedarf hinzufügen
            std::ostream nout(&server); // Du brauchst keinen(!) eigenen ostream
    
            nout << " ahahl" << std::flush;
            for(;;);
    
        }
        catch(const net::net_error& e) { // das könnte von 'basic_sockbuf< char >' geworfen werden
            std::cerr << e.what() << "\nError code: " << e.number() << '\n';
        }
        return 0;
    }
    

    Damit das std::flush funktioniert, musst Du noch das basic_streambuf::sync überladen.

    Danach sollte das Senden von Daten grundsätzlich funktionieren.

    Gruß
    Werner



  • Erst mal vielen Dank. Um das mit dem Overflow zu klären:

    // Aufruf von overflow durch basic_streambuf<Ch, Tr>::sputc, wenn
    // pptr() - pbase() = 1, also noch ein Zeichen in den Puffer passt
    virtual int_type overflow(int_type c = Tr::eof()) {
    	const Ch* end = pptr();
    	if(!Tr::eq_int_type(c, Tr::eof())) { // letztes Zeichen in den Puffer lesen, voll
    		*pptr() = Tr::to_char_type(c);
    		++end;
    	}
    	if(::send(handle, pbase(), end - pbase(), 0) == -1) {
    		bstate = WSAGetLastError();
    		return Tr::eof();
    	}
    	setp(&pbuf[0], &pbuf[0] + pbuf.size() - 1);
    	return Tr::not_eof(c);
    }
    

    Das sehe ich so richtig, oder?
    Ich habe meinen Stream jetzt auch mit Manipulatoren getestet und den sockbuf mal für std::cout gesetzt, klappt alles wunderbar. Ich habe den Stream einfach wegen besserer Fehlerbehandlung erstellt, und auch wegen der TCP/UDP-Steuerung, das kann man einfach nicht vernünftig mit std::basic_ostream alleine implementieren. Der vector zum speichern ist mir noch ein Dorn im Auge. Wenn man den Puffer (und dessen Größe) manuell setzten will, was im Netzwerk vorkommen kann, wegen lahmer Bandbreite etc., sollte pubsetbuf funktionieren. Ich muss mir da noch was einfallen lassen.



  • Ad aCTa schrieb:

    Erst mal vielen Dank. Um das mit dem Overflow zu klären:

    // Aufruf von overflow durch basic_streambuf<Ch, Tr>::sputc, wenn
    // pptr() - pbase() = 1, also noch ein Zeichen in den Puffer passt
    virtual int_type overflow(int_type c = Tr::eof()) {
    	const Ch* end = pptr();
    	if(!Tr::eq_int_type(c, Tr::eof())) { // letztes Zeichen in den Puffer lesen, voll
    		*pptr() = Tr::to_char_type(c);
    		++end;
    	}
    	if(::send(handle, pbase(), end - pbase(), 0) == -1) {
    		bstate = WSAGetLastError();
    		return Tr::eof();
    	}
    	setp(&pbuf[0], &pbuf[0] + pbuf.size() - 1);
    	return Tr::not_eof(c);
    }
    

    Das sehe ich so richtig, oder?

    ich denke - ja. Nur der Kommentar ist falsch - wenn 'pptr() - pbase() == 1', dann liegt genau schon ein Zeichen im Buffer. Mit epptr()-pptr() bekommt man den freien Bereich im internen Buffer, wenn der 0 ist - also epptr()==pptr() - dann wird vom stream overflow gerufen.

    Ad aCTa schrieb:

    Ich habe den Stream einfach wegen besserer Fehlerbehandlung erstellt, und auch wegen der TCP/UDP-Steuerung, das kann man einfach nicht vernünftig mit std::basic_ostream alleine implementieren.

    Wenn ich das lese, bin ich mir gar nicht sicher, ob Du das mit den Streams richtig verstanden hat. Ein Stream selbst hat per se nichts mit der Fehlerbehandlung z.B. einer Socketverbindung zu tun. Auch die TCP/UDP-Steuerung ist Sache des basic_sockbuf. Ein eigener Stream macht Sinn, wenn er z.B. von std::ostream abgeleitet ist, den socketbuf als Member beinhaltet und dessen spezifische Methoden durchleitet. Wenn Du das so meinst, was ist dann aber net::endpoint?
    Wie sieht denn Dein net::onstream aus? Von was ist er abgeleitet?

    Ad aCTa schrieb:

    Der vector zum speichern ist mir noch ein Dorn im Auge. Wenn man den Puffer (und dessen Größe) manuell setzten will, was im Netzwerk vorkommen kann, wegen lahmer Bandbreite etc., sollte pubsetbuf funktionieren. Ich muss mir da noch was einfallen lassen.

    Der vector war nur ein Beispiel, es ist natürlich prinzipiell jedes Stück Memory möglich; es ist schlicht praktisch, wenn das innerhalb des socketbuf liegt. Aber auch hier verstehe ich Deine Anforderung nicht so ganz.

    Nachtrag zu sync. Das kann so aussehen:

    virtual int sync()
        {
            return traits_type::eq_int_type( overflow(), traits_type::eof() )? -1: 0; // -1: Fehler; 0: Ok
        }
    

    Gruß
    Werner



  • Naja, mein sync() sieht eigentlich nur so aus:

    virtual int_type sync() {
    	return overflow();
    }
    

    Wenn ein Fehler auftritt und overflow() Tr::eof() zurückgibt, kann ich das auch gleich weiterreichen.

    > Auch die TCP/UDP-Steuerung ist Sache des basic_sockbuf.

    Habe mich etwas unglücklich ausgedrückt. Ich will nur die Informationen an den Puffer weitergeben. Einen Endpunkt hat nun mal jede Verbindung, deshalb habe ich ihn extern einmal repräsentiert. Der Konstruktor muss nun einen Endpunkt aufnehmen können und zusätzlich ein paar Zusatzspezifikationen:

    template <typename Ch, typename Tr = std::char_traits<Ch> >
    class basic_onstream : public std::basic_ostream<Ch, Tr> {
    public:
    	typedef basic_sockbuf<Ch, Tr> buf_type;
    
    	basic_onstream()
    		: std::basic_ostream<Ch, Tr>(&buffer) {
    	}
    
    	basic_onstream(const std::basic_streambuf<Ch, Tr>* b)
    		: basic_ostream<Ch, Tr>(b), buffer(*b) {
    	}
    
    	// default: net::tcp
    	explicit basic_onstream(const endpoint& ep, const sockmodes& sm = sockmodes(0xF0F0))
    		: std::basic_ostream<Ch, Tr>(&buffer) {
    			// basic_sockbuf initialisieren, Socketmodus weiterreichen, Flags setzten etc.
    	}
    
    	// noch etwas anderes....
    
    private:
    	// kopieren verhindern etc.
    	buf_type buffer;
    };
    

    Die Sockmodes sin ähnlich ios_base::in/out, nur noch um Eigenschaften wie TCP und UDP erweitert. Die Konstruktoren sind einfach zu speziell. Ich denke so wie fstream extra existieren muss, muss auch ein socket_stream existieren, alleine schon wegen der Konstruktoren.

    Was mich hierbei etwas stört ist das doppelte Vorhandensein des Puffers. Einmal speichere ich einen und den selben als Zeiger in der Basisklasse, einmal habe ich selber einen da. Es ist das Slicing-Problem: wenn ich buffer nicht als Element in meiner Klasse hätte, müsste ich rdbuf() anwenden. Da das aber einen Zeiger auf basic_streambuf zurückgibt, entfallen alle speziellen Methoden meines eigenen Puffers (das Interface ist logischerweise nicht da).



  • Ad aCTa schrieb:

    Naja, mein sync() sieht eigentlich nur so aus:

    virtual int_type sync() {
    	return overflow();
    }
    

    Wenn ein Fehler auftritt und overflow() Tr::eof() zurückgibt, kann ich das auch gleich weiterreichen.

    Nicht ganz, da sync protected ist.
    Und lt. Standard liefert sync -1 im Fehlerfall und 0, wenn alles ok ist.

    Ad aCTa schrieb:

    Was mich hierbei etwas stört ist das doppelte Vorhandensein des Puffers.

    Ich sehe immer nur einen Puffer. Wo ist da was doppelt?

    Dein net::onstream ist das was ich meinte - das sollte ok sein.

    Gruß
    Werner



  • > Ich sehe immer nur einen Puffer. Wo ist da was doppelt?

    Na die Basisklasse hat ja auch einen. Irgendwo muss sie irgendeinen basic_streambuf<>*-Member haben, der von rdbuf() zurückgegeben wird. Ich würde ja gerne genau diesen internen Puffer benutzen (der mit dem Member identisch ist), aber das geht ja eben (wegen Slicing-Problem) nicht.



  • Ad aCTa schrieb:

    > Ich sehe immer nur einen Puffer. Wo ist da was doppelt?

    Na die Basisklasse hat ja auch einen. Irgendwo muss sie irgendeinen basic_streambuf<>*-Member haben, der von rdbuf() zurückgegeben wird. Ich würde ja gerne genau diesen internen Puffer benutzen (der mit dem Member identisch ist), aber das geht ja eben (wegen Slicing-Problem) nicht.

    Welche Basisklasse meinst Du? std::basic_streambuf<> hat keinen Buffer nicht ein Byte.
    Der Stream (z.B.: std::ostream) hat einen Pointer (nicht Komposition) auf einen std::basic_streambuf<>, das ist ein Objekt und kein Puffer im Sinne eines Memory-Buffers. Diesen Pointer auf das Buffer-Objekt kann man mit rdbuf() abholen. (Bem.: rdbuf() erbt jeder Stream von std::basic_ios<>)
    Ein Stream selbst puffert nie ein Zeichen, er arbeitet eher wie ein Durchlauferhitzer.

    Ein Slicing-Problem mit Stream oder Buffer kann es hier gar nicht geben, da weder die Streams noch die Buffer kopierbar bzw. zuweisbar sind (zumindest vom Sinn her). Sollten sie es rein formal sein, ist das eher als Fehler zu sehen.

    Gruß
    Werner



  • Mit Puffer meine ich ja den Streambuf. Dass man sich den mit rdbuf() abholt etc. ist mir ja bewusst, ich kenne Sinn und Bedeutung von Stream (Formatierung/Kommunikation) und Puffer (Speicher/Physikalische Verbindung).

    Wenn mein streambuf eine neue Methode dazubekommt (meinetwegen open()), dann kann ich diese Methode nur über einen Zeiger dieser selben Klasse aufrufen. Wenn ich aber nun rdbuf() in meinem Stream benutze, um Methoden wie connect/init/etc im Streamkonstruktor aufzurufen, werde ich scheitern, da rdbuf() einen Zeiger auf die Basisklasse zurückgibt, bei dem es diese Methoden nicht gibt, obwohl der Zeiger selbst auf ein Objekt zeigt, welche sie hat (dank statischer Bindung). Das meine ich mit slicing.


Anmelden zum Antworten