fcntl nur für den Übertragungssocket?



  • Nabend,

    epoll ist allerdings Linux-spezifisch.

    gruss
    v R



  • Marc21Ja schrieb:

    Hallo!

    Um einen Server zu schreiben, braucht man ja einen Hauptsocket, der dann über accept die verschiedenen Verbindungsanfragen annimmt und pro Anfrage einen Übertragungssocket herstellt. Das Senden und Empfangen möchte ich gerne in Threads erledigen.

    Das Senden und Empfangen selbst soll nicht blockierend sein, während der Hauptsocket weiterhin blockierend bleiben soll. Gibt es nun die Möglichkeit, dass der Hauptsocket blockierend bleibt und man den Befehl fcntl NUR für die Übertragungssockets benutzt?

    Vielen Dank für eure Hilfe,
    Marc

    nutze select() für den verbindungssocket. select() blockt auch dann, wenn der socket mit NON_BLOCKING erstellt wurde. der vorteil in dieser variante besteht darin, dass man bei select() oder pselect() timeouts übergeben kann.



  • virtuell Realisticer schrieb:

    Nabend,

    epoll ist allerdings Linux-spezifisch.

    gruss
    v R

    Die portable Lösung wäre der System-Call select. Der ist aber langsamer, als epoll. Andernfalls kannst du auch die Biliothek libevent benutzen. Die nimmt immer der System-Call, der auf dem System am schnellsten ist.

    Noch was: Wenn eine Verbingung verloren geht, dann blockiert der send und read Befehl nicht mehr, sondern beendet mit einer Fehlermeldung.



  • Schau Dir mal meine cxxtools an. Da gibt es Netzwerkklassen für tcp und udp. Die kannst Du entweder verwenden oder aber einfach abschauen.

    Zur eigentlichen Frage: wenn Du einen Socket non-blocking setzt, dann gilt das genau für diesen Deskriptor. Ob der listen blockierend ist oder nicht, ein accept liefert immer einen blockierenden Deskriptor, da das der default ist. Den Deskriptor musst du erst auf non-blocking setzen.

    Grundsätzlich ist das richtig, den listen-Socket blockierend zu lassen und den stream-socket nicht blockierend. Das ist dann sinnvoll, wenn Du einen Timeout überwachen willst. Das geht dann nur über poll (oder select oder epoll). Hast Du nur einen Socket zu überwachen, kannst Du poll nehmen. epoll bringt was, wenn Du viele Socket auf ein mal überwachen willst. Aber wenn Du einen Thread pro client hast, willst Du das offensichtlich nicht.

    Eine bessere, wenn auch aufwändigere Architektur wäre, einen Pool von Threads zu haben, die dann aufwachen, wenn einer der Teilnehmer was zu sagen hat. Alle Teilnehmersockets hängen dann in einem großen poll (oder epoll). So kannst Du mit wenigen Threads viele Teilnehmer überwachen oder möglicherweise ganz auf Threads verzichten, da Du ja auch den listen-socket in den poll mit aufnehmen kannst.

    Ob poll oder epoll zu nehmen ist, hängt davon ab, ob Du Linux-Spezifisch programmieren willst oder nicht. Wenn nicht, ist es sowieso sinnvoller so etwas wie autoconf zu nehmen und zu testen, ob das vorliegende System epoll kennt. Dann machst Du beide Implementierungen und verwendest epoll nur wenn es auf dem Zielsystem verfügbar ist. So habe ich das in meinem tntnet gemacht. Da kannst Du natürlich auch schauen, wie man so einen Workerpool machen kann.



  • Hallo!! Vielen Dank erstmal für die ganzen Informationen!! 🙂

    Der wichtigste Punkt schien erstmal zu sein, dass bei Verbindungsabbruch der gesamte Prozess mit ner SIGPIPE abgebrochen wird, was man aus main() mit dem Signal-Befehl und nem Handler mit int a=raise(18) (also Prozess fortsetzen) und try und catch bei den send/recv-Abfragen abfangen kann.

    Da man beim HTTP-Protokoll nur einmal empfangen kann und dann nur noch gesendet wird, ist select/epoll offenbar nur noch für den Hauptsocket notwendig - an die bestehenden Clients braucht man nur noch die Chat-Zeilen zu verschicken, fertig. Die Frage ist jetzt: Wenn ich mit Threads arbeite, wird dann der Signalhandler genauso aufgerufen wie aus der Main-Funktion (signal(SIGPIPE, Handler-Funktion)) oder muss man in jedem Thread einen Extra-Handler und einen speziellen Aufruf angeben?

    Ein weiteres Problem ist, dass die Cliends bei nicht-blockierenden Sockets trotz positivem if(FD_ISSET(s,&fds)) den HTTP_REQUEST anfangs offenbar nicht so schnell senden, es werden 0 Bytes übertragen, sodass ich am Anfang ein usleep(50000) dazwischen gesetzt und ne Schleife mit 3 Versuchen reingesetzt hab, sicher nicht grad die schickste Lösung, vielleicht wären hier doch blockierende Sockets zumindest für die Abfrage des REQUESTS sinnvoller, aber so funktionierts erstmal.

    Viele Grüße, Marc



  • Marc21Ja schrieb:

    Hallo!! Vielen Dank erstmal für die ganzen Informationen!! 🙂

    Der wichtigste Punkt schien erstmal zu sein, dass bei Verbindungsabbruch der gesamte Prozess mit ner SIGPIPE abgebrochen wird, was man aus main() mit dem Signal-Befehl und nem Handler mit int a=raise(18) (also Prozess fortsetzen) und try und catch bei den send/recv-Abfragen abfangen kann.

    Du bekommst einen SIGPIPE bei einem Verbindungsabbruch??? Das stimmt so nicht. SIGPIPE bekommst Du nur bei einer pipe. Zitat aus signal(7):

    SIGPIPE 13 Term Broken pipe: write to pipe with no readers

    Da hast Du bestimmt was falsch interpretiert.

    Marc21Ja schrieb:

    Da man beim HTTP-Protokoll nur einmal empfangen kann und dann nur noch gesendet wird, ist select/epoll offenbar nur noch für den Hauptsocket notwendig - an die bestehenden Clients braucht man nur noch die Chat-Zeilen zu verschicken, fertig.

    Das gilt nur, wenn Du kein HTTP-keep-alive verwendest.

    Marc21Ja schrieb:

    Ein weiteres Problem ist, dass die Cliends bei nicht-blockierenden Sockets trotz positivem if(FD_ISSET(s,&fds)) den HTTP_REQUEST anfangs offenbar nicht so schnell senden, es werden 0 Bytes übertragen, sodass ich am Anfang ein usleep(50000) dazwischen gesetzt und ne Schleife mit 3 Versuchen reingesetzt hab, sicher nicht grad die schickste Lösung, vielleicht wären hier doch blockierende Sockets zumindest für die Abfrage des REQUESTS sinnvoller, aber so funktionierts erstmal.

    Nach dem der Client die Verbindung aufgebaut hat, schickt er in der Regel unmittelbar einen Request. Das ist allerdings keine Atomare Operation und so bekommst Du beim HTTP-Protokoll in der Regel immer erst mal 0 Bytes, wenn Du sofort anfängst zu lesen. Deine Lösung ist aber langsam und falsch. Das funktioniert nur, wenn der Client schnell genug schickt. Manchmal teste ich meinen http-Server mit telnet. Da können bis zum Request schon mal mehrere Sekunden vergehen.

    Die richtige Lösung ist, nach dem accept erst mal einen poll zu machen und damit zu blockieren, bis entweder der Client Daten schickt oder ein Timeout aufgetreten ist. Und vor jedem Lesevorgang erst mal mit poll fragen, ob auch Daten anliegen. Alternativ kannst Du auch erst lesen und wenn Du dann 0 Bytes bekommst, einen poll absetzen. Das kann einen Systemaufruf einsparen. Aber das sind dann Feinheiten.

    Eine weitere Linux-Spezifische Feinheit ist die Socket option TCP_DEFER_ACCEPT. Das kannst Du Dir unter tcp(7) nach lesen. Aber nicht daß Du auf die Idee kommst, daß der komplette Request gelesen werden kann, sobald das erste Byte ankommt.

    Ich empfehle Dir nochmals die Implementierung in meinen cxxtools anzuschauen. Die tcp-Klasse findest Du auch unter http://www.tntnet.org/download/cxxtools-1.4.6/src/tcpstream.cpp. Da schaust Du Dir die Methode Stream::read und Stream::write an.



  • Hallo!

    Du bekommst einen SIGPIPE bei einem Verbindungsabbruch??? Das stimmt so nicht. SIGPIPE bekommst Du nur bei einer pipe. Zitat aus signal(7):

    Also wo er vorher bei Verbindungsabbruch der anderen Seite ständig den Prozess beendet hat, was beim Chatserver ja nicht passieren darf, ruft er mit signal(SIGPIPE, handler) tatsächlich den Handler auf. Ich hatte das auf der Seite http://www.zotteljedi.de/doc/socket-tipps/send.html gelesen ("Wenn man auf einen Socket sendet, der von der Gegenstelle geschlossen wurde, wird das Signal SIGPIPE generiert, das den Prozeß normalerweise tötet, wenn man keinen eigenen Handler installiert hat.").

    Ich empfehle Dir nochmals die Implementierung in meinen cxxtools anzuschauen. Die tcp-Klasse findest Du auch unter http://www.tntnet.org/download/cxxtools-1.4.6/src/tcpstream.cpp. Da schaust Du Dir die Methode Stream::read und Stream::write an.

    Vielen Dank für den Tipp! Ich bin noch ziemlich neu mit den verschiedenen Signalen und so. Bei der Methode poll(), wo werden da eigentlich die FDs gesetzt, kannst du mir auch nochmal deinen Aufruf mit "if (poll(POLLIN) & POLLHUP)" erklären? Ich hab im Internet im Gegensatz zu select() kaum richtige Tutorials oder ein einfaches und funktionierendes (!) Beispielprogramm zu poll() gefunden, gibts da (außer dem manual und für Neulinge verständlich) etwas? Gibt es bei poll eigentlich eine Beschränkung für die Anzahl der Filedeskriptoren pro Prozess?

    Vielen Dank + viele Grüße,
    Marc



  • Marc21Ja schrieb:

    Vielen Dank für den Tipp! Ich bin noch ziemlich neu mit den verschiedenen Signalen und so. Bei der Methode poll(), wo werden da eigentlich die FDs gesetzt, kannst du mir auch nochmal deinen Aufruf mit "if (poll(POLLIN) & POLLHUP)" erklären? Ich hab im Internet im Gegensatz zu select() kaum richtige Tutorials oder ein einfaches und funktionierendes (!) Beispielprogramm zu poll() gefunden, gibts da (außer dem manual und für Neulinge verständlich) etwas? Gibt es bei poll eigentlich eine Beschränkung für die Anzahl der Filedeskriptoren pro Prozess?

    Vielen Dank + viele Grüße,
    Marc

    Dieses "if (poll(POLLIN) & POLLHUP)" ruft "short cxxtools::Socket::poll(short) const" auf, welches ein Wrapper um den eigentlichen Systemaufruf poll ist. Das findest Du in src/net.cpp. Der Rückgabewert ist ein Flagfeld und mit dem &-Operator frage ich ab, ob das Flag POLLHUP gesetzt ist.

    Informationen zu poll findest Du natürlich in den man-pages. "man poll" oder im Konqueror in der Adresszeile "#poll" eingeben. Ist zwar auch kein Tutorial, aber es lohnt sich zu lernen, mit solchen Erklärungen zurecht zu kommen. Die sind in der Regel präziser und vollständiger als ein Tutorial.

    Ein komplexeres Beispiel zu poll und auch epoll findest Du in den tntnet-Sourcen unter framework/common/poller.cpp.

    Eine Beschränkung der Filedeskriptoren gibt es bei Poll nicht. Aber es gibt eine Beschränkung der maximalen Anzahl von Filedeskriptoren pro Prozess. Ich weiß nicht wo sie liegt, aber ich habe schon mit ein paar tausend offenen Sockets gearbeitet.



  • Vielen Dank!



  • Vielen Dank erstmal! 🙂 Im Prinzip ist das ja dann gar nicht so schwer.

    Was ich nicht verstehe, du schreibst z.B.:

    p = ::poll(&fds, 1, getTimeout());
    ...
    else if (p == 0)
        {
          log_debug("poll timeout (" << getTimeout() << ')');
          throw Timeout();
        }
    

    So stehts ja auch im Manual und dann fragst du EAGAIN ab. Nach dem accept, dem Setzen des Socket c auf NON-BLOCK und dem Auslesen des HTTP-Headers habe ich das jetzt mal so getestet:

    struct pollfd fdc[1];
    timeout=1000;
    int p;
    
    for(int a=0;a<6;a++){
      fdc[0].fd = c;
      fdc[0].events = POLLOUT;
      p=poll(fdc,1,timeout);
      cout << "p: " << p << endl;
    
      if(p<=0 || errno==EAGAIN){
        cout << "Timeout oder Fehler" << endl;
        close(c);
        return -1;
      }
      bytes = send(c,antwort,bufsize,0);
    
      if(bytes<0){
        cout << "Fehler beim Schreiben" << endl;
        close(c);
        return -1;
      }
    
      sleep(2);
    }
    

    Beim Reloaden oder Zumachen des Browsers funktioniert das wunderbar. Breche ich hingegen nach der ersten Ausgabe die Internetverbindung ab und stelle sie nach ca. 5 Sekunden wieder her, gibt er (ohne die 2 Sekunden Pause dazwischen) die restlichen Zeilen komplett und jeweils p=1 aus, anstatt die Funktion abzubrechen. Kann es sein, dass da irgendwas im Server gepuffert wird und er deshalb denkt, der Socket sei zum Schreiben bereit? Eigentlich soll er ja beim Timeout abbrechen (nachher soll er natürlich länger als 1000 sein).

    Danke + viele Grüße,
    Marc


Anmelden zum Antworten