Mehrere TCP/IP Verbindungen



  • Hier mal ein kleiner Echo-Server (für Linux), der pro Verbindung einen neuen Thread aufmacht:

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    
    #include <iostream>
    #include <thread>
    #include <vector>
    
    int main() {
        using namespace std;
        int listen_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(listen_socket < 0) {
            cerr << "socket() failure\n";
            return -1;
        }
    
        sockaddr_in saddr{ };
        saddr.sin_port = htons(40001);
        saddr.sin_family = AF_INET;
        saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        if(bind(listen_socket, reinterpret_cast<sockaddr*>(&saddr), sizeof saddr) < 0) {
            cerr << "bind() failure\n";
            close(listen_socket);
            return -2;
        }
    
        if(listen(listen_socket, SOMAXCONN) < 0) {
            cerr << "listen() failure\n";
            close(listen_socket);
            return -3;
        }
    
        for(;;) {
            sockaddr_in client_addr{ };
            socklen_t addr_len = sizeof client_addr;
            int client_socket = accept(listen_socket, reinterpret_cast<sockaddr*>(&client_addr), &addr_len);
            if(client_socket < 1) {
                cerr << "accept() failure\n";
            }
            else { 
                thread([=]() {
                    vector<char> buffer(512);
                    int result = 0;
                    do {
                       result = recv(client_socket, &buffer[0], buffer.size(), 0);
                       if(result < 0) {
                          cerr << "recv() failure\n";
                          close(client_socket);
                          return;
                       }
                       else if(result != 0) {
                           if(send(client_socket, &buffer[0], result, 0) < 0) {
                              cerr << "send() failure\n";
                              close(client_socket);
                              return;
                           }
                       }
                       else {
                           close(client_socket);
                           return;
                       }
                   } while(true);
                }).detach();
            }
        }
    }
    

    Edit: Trotzdem sei dir schwer geraten, eine Library zu benutzen. Dann musst du dich auch nicht mit Plattformabhängigkeit und I/O-Strategien wie 1 Thread pro Verbindung etc. rumplagen.



  • TyRoXx schrieb:

    Ach, aber "Otto" kommt von selbst auf die Idee einen neuen Thread für die Verbindung zu erstellen?

    Jupp, klar.



  • Jodocus schrieb:

    Edit: Trotzdem sei dir schwer geraten, eine Library zu benutzen. Dann musst du dich auch nicht mit Plattformabhängigkeit und I/O-Strategien wie 1 Thread pro Verbindung etc. rumplagen.

    Jop.
    Boost.Asio ist z.B. nicht grundsätzlich böse. Man muss nur "As" Teil ignorieren 🕶



  • Jodocus schrieb:

    Hier mal ein kleiner Echo-Server (für Linux), der pro Verbindung einen neuen Thread aufmacht:

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    
    #include <iostream>
    #include <thread>
    #include <vector>
    
    int main() {
        using namespace std;
        int listen_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(listen_socket < 0) {
            cerr << "socket() failure\n";
            return -1;
        }
    
        sockaddr_in saddr{ };
        saddr.sin_port = htons(40001);
        saddr.sin_family = AF_INET;
        saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        if(bind(listen_socket, reinterpret_cast<sockaddr*>(&saddr), sizeof saddr) < 0) {
            cerr << "bind() failure\n";
            close(listen_socket);
            return -2;
        }
        
        if(listen(listen_socket, SOMAXCONN) < 0) {
            cerr << "listen() failure\n";
            close(listen_socket);
            return -3;
        }
    
        for(;;) {
            sockaddr_in client_addr{ };
            socklen_t addr_len = sizeof client_addr;
            int client_socket = accept(listen_socket, reinterpret_cast<sockaddr*>(&client_addr), &addr_len);
            if(client_socket < 1) {
                cerr << "accept() failure\n";
            }
            else { 
                thread([=]() {
                    vector<char> buffer(512);
                    int result = 0;
                    do {
                       result = recv(client_socket, &buffer[0], buffer.size(), 0);
                       if(result < 0) {
                          cerr << "recv() failure\n";
                          close(client_socket);
                          return;
                       }
                       else if(result != 0) {
                           if(send(client_socket, &buffer[0], result, 0) < 0) {
                              cerr << "send() failure\n";
                              close(client_socket);
                              return;
                           }
                       }
                       else {
                           close(client_socket);
                           return;
                       }
                   } while(true);
                }).detach();
            }
        }
    }
    

    Edit: Trotzdem sei dir schwer geraten, eine Library zu benutzen. Dann musst du dich auch nicht mit Plattformabhängigkeit und I/O-Strategien wie 1 Thread pro Verbindung etc. rumplagen.

    Danke für die Antwort und das Beispiel. Aber ist es hier nicht so das alle Verbindungen über den gleichen Port laufen?

    Gruß



  • @volkard

    Beachte, daß http-serven trivial ist. Apache ist im Prinzip ein single-Threaded Nacheinanderausführer.

    aber auch nicht mehr wenn du 1000 Benutzer auf deiner Seite hast - irgendwann sieht das ganze - wenn man die Sockets betrachtet auch nicht mehr anders aus als ein stark frequentierter Chat-Server

    @hustbear

    heisse luft
    wo sind deine zahlen?
    wo sind die sources mit denen man die benchmarks selbst wiederholen kann?

    sind leider nur Erfahrungswerte aus meiner berufliche Praxis - habe dazu gerade keine "Studie" vorliegen, wenn du andere Erfahrungen gemacht hast würde ich mich gerne von deinen Lösungen hören - teilweise muss meine Software auch auf kleinen embedded Systemen laufen, deswegen achte ich sehr stark auf Skalierung oder auch die "Belastung" welche ganz klar von System zu System anders gelagert ist

    100 µs sind schon einmal die richtige Größenordnung. Invertiert sind das gerade einmal 10 kHz. Maximal gehen damit also 10000 Verbindungen pro Sekunde und Hardware-Thread.

    nicht zu vergessen wieviel Zeit dein Scheduler deinem Prozess überhaupt zuspricht und deine Threads sich um die Sub-Zuteilung schlagen (was mehr ist als die pure Switch-Zeit), plus die Contet-Switch-Cache-Misses und,und,und

    für seinen Chat-Server hier reicht aber wie gesagt sicherlich die Thread-Lösung



  • Brause schrieb:

    Danke für die Antwort und das Beispiel. Aber ist es hier nicht so das alle Verbindungen über den gleichen Port laufen?

    Jo, das wäre normal. Auf Port 40001 bietet dieser Server seine Dienste an.
    Die Verbindungen (bestehend aus je zwei Ports und zwei Adressen) sind dann unabhängig.



  • volkard schrieb:

    Brause schrieb:

    Danke für die Antwort und das Beispiel. Aber ist es hier nicht so das alle Verbindungen über den gleichen Port laufen?

    Jo, das wäre normal. Auf Port 40001 bietet dieser Server seine Dienste an.
    Die Verbindungen (bestehend aus je zwei Ports und zwei Adressen) sind dann unabhängig.

    Danke für die Antwort! War keine schlaue frage klar gibt ja zweit Sockets auf Server Seite!! Hatte einen denkfehler.

    Bei mir sieht es nämlich etwas anders aus. Da die angesprochenen Verbindungen Maschinen sind die komplett autonom sind ist meine Idee das die Maschinen jeweils der Server sind und ich der Client. Also das mein Programm jeweils einen Client für jede Maschine zur Verfügung stellt. Wie würde man das am besten mit Threads realisieren?

    Danke Gruß



  • Gast3 schrieb:

    @hustbear

    heisse luft
    wo sind deine zahlen?
    wo sind die sources mit denen man die benchmarks selbst wiederholen kann?

    sind leider nur Erfahrungswerte aus meiner berufliche Praxis - habe dazu gerade keine "Studie" vorliegen, wenn du andere Erfahrungen gemacht hast würde ich mich gerne von deinen Lösungen hören

    Ja, ich hab die Erfahrung gemacht dass es so-gut-wie egal ist ob man jetzt 10 oder 1000 Threads hat - so lange nicht mehr gleichzeitig laufen wollen als man physikalische CPUs hat merkt man keinen Unterschied.

    Vielleicht hast du noch mit einem Kernel aus Grossvaters Zeiten gearbeitet wo das Suchen eines aufzuweckenden Threads noch O(N) war?



  • Ja, ich hab die Erfahrung gemacht dass es so-gut-wie egal ist ob man jetzt 10 oder 1000 Threads hat - so lange nicht mehr gleichzeitig laufen wollen als man physikalische CPUs hat merkt man keinen Unterschied.

    und was ist wenn die gleichzeitigen deine physikalische CPU-Anzahl (du meinst Hardware-Threads?) per se überschreiten?
    denn erst da wird ja die Skalierbarkeit der Software überhaupt wichtig
    und eher das Szenario das ich beschrieben habe

    dieses "so lange nicht mehr gleichzeitig laufen wollen" ist genau das Skalierungsproblem an dem Thread-per-Client Prinzip - entweder beschränkt man die Client-Anzahl und/oder man bremst die "gleichzeitige" Kommunikation der Teilnehmer - beides könnte irgendwie den Sinn eines Chat-Server beschränken
    - und ist auch stark davon abhängig wieviel Daten über den Server zwischen den Clients ausgetauscht werden, weil dann ja auch der Synchronisationsaufwand steigt

    wenn die Client-Anzahl - also die parallel zu behandelnden Aufgaben gering ist oder die Durchsatzminderung bei
    steigender Clientanzahl irgendwie egal (unbewertet) ist - und auch die Systemressourcen da mitspielen kann man Threads leicht und einfach benutzen

    eines meiner Projekte: eine Win7/Linux 3.x,x64-Software welche mit ~230 embedded-Steuerungen über TCP-IP kommuniziert, lesend/schreibend - ~3-50ms, 100-500Byte Pakete, permanent, (fast) keine Allokationen aber Synchronisation zwischen Client-Daten - die Software ist da so eine Art Chat-Server-Message-Router - Linux macht es besser (aber Kunde mag leider Windows)

    es gab aber auch schon die Frage ob man nicht auch 3000 solcher embedded Systeme über so einen Server kommunizieren zu lassen - da musst dann aber wirklich mehr CPU,RAM,Netzwerke und Linux rein

    du hast wenigstens deine Einschränkung mit dem "so lange nicht mehr gleichzeitig laufen wollen" gebracht - manch andere Post hier könnte man teilweise so verstehen als wenn man immer alles mit Threads machen könnte

    Vielleicht hast du noch mit einem Kernel aus Grossvaters Zeiten gearbeitet wo das Suchen eines aufzuweckenden Threads noch O(N) war?

    naja so alt bin ich auch wieder nicht - habe aber noch Väterchen-Win2K-Software die Flausen ausgetrieben mit Threads skalieren zu wollen - und auch noch mit 16Bit-Interrupts "Threading" betrieben 🙂

    aber ich arbeite auch in einem Bereich in dem die Systemressoucen eines Win/Linux Industrie-PCs oder embedded-System wie RAM,CPU usw. nicht wie Kamelle
    vom Wagen geworfen werden wenn die Software schreit



  • Oh Mann, Gast3, bitte laber nicht.

    Gast3 schrieb:

    du hast wenigstens deine Einschränkung mit dem "so lange nicht mehr gleichzeitig laufen wollen" gebracht - manch andere Post hier könnte man teilweise so verstehen als wenn man immer alles mit Threads machen könnte

    Ja. Die Einschränkung ist das damit nicht irgend ein Schlaumeier daherkommt und das offensichtliche bekrittelt, nämlich dass irgendwas langsamer werden muss wenn man mehr Rechenzeit verbrauchen will als die Maschine mit allen Cores in Summe hat. Was logisch ist. Die Threading-Lösung wird aber auch mit mehr Threads die laufen wollen nicht automatisch sehr ineffizient.

    Deine Behauptung von "bei mehr als 300 wirds voll langsam" kann ich überhaupt nicht nachvollziehen.



  • Gast3 schrieb:

    Vielleicht hast du noch mit einem Kernel aus Grossvaters Zeiten gearbeitet wo das Suchen eines aufzuweckenden Threads noch O(N) war?

    naja so alt bin ich auch wieder nicht - habe aber noch Väterchen-Win2K-Software die Flausen ausgetrieben mit Threads skalieren zu wollen - und auch noch mit 16Bit-Interrupts "Threading" betrieben 🙂

    so lange ist das auch nicht her



  • manni hat recht, wenn es geht dann boost::asio



  • Deine Behauptung von "bei mehr als 300 wirds voll langsam" kann ich überhaupt nicht nachvollziehen.

    dann muss ich mich korrigieren - ich meinte eher ab einer Menge X in Abhängigkeit zur Synchronisationshäufigkeit der Threads kann es passieren das die deinem Prozess zugeteilten CPU Häppchen so klein werden das die eigentliche Thread-Arbeit und das Drumherum aus dem Gleichgewicht gerät... und ich kenne auch einfach keine Systeme die stark das Thread-per-Client Prinzip verwenden - ausser einfache Threadpools die meist auch im untern 2-stelligen Bereich agieren, ich sehe einfach nur sehr selten Tools die viel asynchrone Operation tätigen - DB, Server etc. und dabei permanent mit ein paar hundert Threads rumorgeln (Asynchronität an sich sehe ich genug - eigentlich nur)

    Boost.Asio fasst meine Erfahrungen schön zusammen

    Implementation strategies such as thread-per-connection (which a synchronous-only approach would require) can degrade system performance, due to increased context switching, synchronisation and data movement among CPUs. With asynchronous operations it is possible to avoid the cost of context switching by minimising the number of operating system threads — typically a limited resource — and only activating the logical threads of control that have events to process.

    oder teilweise Infos aus: http://stackoverflow.com/questions/14317992/thread-per-connection-vs-reactor-pattern-with-a-thread-pool

    wobei du das ja auch "...so lange nicht mehr gleichzeitig laufen wollen als man physikalische CPUs hat merkt man keinen Unterschied..." so siehst

    mag einfach keine Threads=Asychron-Problem-Lösungs-Aussagen, und nachher leidet ein weiteres Programm unter Thread-Lethargie weil einer wieder mal gedacht hat damit könnte man die "Kernenergie" viel leichter nutzbar machen - was einfach so nicht stimmt



  • Gast3 schrieb:

    wobei du das ja auch "...so lange nicht mehr gleichzeitig laufen wollen als man physikalische CPUs hat merkt man keinen Unterschied..." so siehst

    Nein, das sehe ich nicht so, das hast du falsch verstanden.
    Ich hab auch in meiner letzten Antwort an dich versucht dieses Misverständnis aufzuklären.
    Ich meinte: wenn es mehr zu tun gibt als CPU Leistung da ist, dann wird alles langsam, egal welche IO Strategie man verwendet. Die Frage ist nur: wie schnell wird es langsamer mit synchronem IO und wie schnell mit diversen async. IO Varianten.

    Und was den Rest angeht: klar kostet ein Context-Switch Zeit. Das Aufrufen des für die jeweilige Connection zuständigen Handlers aus einem gemeinsamen Thread-Pool heraus ist aber nichts grundsätzlich anderes. Hier müssen bei einem Server der gerade viele, viele Verbindungen bedient auch die Daten für die jeweilige Connection aus dem 2nd level Cache, 3rd level Cache oder sogar aus dem Hauptspeicher nachgeladen werden.
    Was beim Context-Switch noch dazukommt sind die CPU-Register sowie dass der Scheduler anlaufen muss. Was die CPU Register angeht, so hat die "1 Thread pro Connection" Lösung da keinen echten Nachteil, da in die async Variante weniger Daten in Registern halten kann sondern vor dem nächsten IO Call alles ins RAM zurückschreiben muss, zu Fuss diverse State Variablen updaten muss etc.

    Und was den Scheduler angeht so haben da nur Lösungen die mehrere IOs mit einem einzigen Kernel-Call absetzen können bzw. die "completion notification" für mehrere IOs mit einem einzigen Kernel-Call abholen können einen echten Vorteil. Denn ob der Kernel-Call jetzt ein synchroner send/recv Aufruf ist + Thread schlafen legen im Scheduler etc., oder ein asynchroner send/recv Aufruf + wieder beim Completion-Port nachfragen was es als nächstes zu tun gibt, das macht nicht wirklich den grossen Unterschied.

    Bei APIs wie epoll - die das oben erwähnte können, also mehrere Requests/Notifications mit einem Kernel-Call abschicken/abholen (wobei epoll nur den Abholen Teil unterstützt wenn ich das richtig verstanden habe) - lasse ich mir dagegen gerne einreden dass es einen Vorteil bringt. Wobei mich auch da interessieren würde wie gross der Vorteil wirklich ist. Also auf wie viele "Null-Requests/Second" (=Requests wo der Server so-gut-wie nix tut ausser ne Antwort zurückzuschicken) man mit einem gut implementierten epoll Server kommt, und auf wie viele man mit einem "1 Thread pro Connection" Server kommt.

    Sinnvollerweise vielleicht mit einem "1 Thread pro Connection" Server der nicht für jede Connection nen eigenen Thread erzeugt + zerstört, sondern einen "dynamically sized Thread-Pool" verwendet -- damit man nicht pro Connection den Overhead des Thread-Erstellens + Thread-Aufräumens bezahlt. (Bzw. alternativ mit einem "einfachen" Server, aber mit Clients die 10+ Requests pro Connection absetzen.)

    Dann hätte man mal Zahlen die man sich konkret ansehen kann. Und wo man konkret den geschätzten (oder idealerweise gemessenen) Bearbeitungsaufwand pro Request gegenüberstellen kann.

    Und dann kann man sinnvoll abschätzen ob es sich lohnt die deutlich höhere Komplexität eines asynchronen Servers in Kauf zu nehmen -- oder eben doch nicht.

    Und was diverse Zitate aus u.A. der ASIO Dokumentation angeht: das ist auch relativ unspannend. Der ASIO Entwickler kommt mir auch eher wie ein Evangelist vor, ich kann mich nicht erinnern irgendwo echte Vergleiche gesehen zu haben. Also so wirklich A vs. B, schön mit Zahlen und Source-Code um das alles nachzuvollziehen.



  • ps: Wie könnte man das deiner Meinung nach sinnvoll testen? Ich überleg mir das nämlich gerade, und komm da nicht wirklich auf nen grünen Zweig.

    Es haben ja die wenigsten 3+ Rechner zu hause. Und Clients + Server auf dem selben Rechner laufen lassen ist ... sicher nicht optimal. VMs scheiden mMn. auch aus. Natürlich könnte man den VMs jeweils nur 1-2 CPUs geben, so dass auch wenn alle VMs alle ihre virtuellen Cores auslasten der Host noch Cores "übrig" hat. Blöderweise teilen sich aber auch alle eine phys. CPU, und damit auch die Caches dieser CPU. Und auch andere Engstellen wie z.B. den Netzwerkkartentreiber des Hosts.

    Die beste Möglichkeit die mir einfällt wäre einen sehr starken PC zu verwenden wo man die Clients laufen lässt, und einen vergleichsweise schwachen PC auf dem man den Server laufen lässt. Dann sollte man davon ausgehen können dass der Client PC genug "Zupf" haben wird ausreichend Requests zu schicken, so dass der Server, ganz egal wie schnell er dann ist, ausreichend was zu tun bekommt.

    Wobei da natürlich auch noch das Bottleneck Netzwerk dazwischenkommt - denn kaum jmd. hat was schnelleres als Gigabit Ethernet zuhause.



  • hustbaer schrieb:

    Und was den Scheduler angeht so haben da nur Lösungen die mehrere IOs mit einem einzigen Kernel-Call absetzen können bzw. die "completion notification" für mehrere IOs mit einem einzigen Kernel-Call abholen können einen echten Vorteil. Denn ob der Kernel-Call jetzt ein synchroner send/recv Aufruf ist + Thread schlafen legen im Scheduler etc., oder ein asynchroner send/recv Aufruf + wieder beim Completion-Port nachfragen was es als nächstes zu tun gibt, das macht nicht wirklich den grossen Unterschied.

    Welche API bietet das denn nicht? Es klingt unterschwellig so, als ob du das den IOCPs unterstellen könntest, aber die haben auch GetQueuedCompletionStatusEx .



  • @Jodocus
    Danke für die Info.
    Die Funktion gibt's erst ab Vista bzw. Windows Server 2008, und daher kannte ich die einfach noch nicht.

    Wobei man auch hier zum "Erzeugen" der IOs pro IO einen Kernel-Call braucht.
    Ist also auch bloss die halbe Miete.
    Oder gibt's auch dazu 'was was ich bloss nicht kenne?

    Oder haben die Damen & Herren OS-Programmierer das etwa sogar hingebracht dass die Aufrufe zum Absetzen von IOs effizient sind trotz dem man pro IO eine OS-Funktion aufrufen muss?

    Würde mich wundern, aber möglich wär's natürlich. Ich kenne halt nur die Architektur von "normalen" Windows-Treibern, und da fangen bei jedem noch so einfachen IO-Request (egal wie man ihn absetzt) viele viele Heinzelmännchen im Kernel zu laufen an die viele viele Zahnräder drehen. Wenn das bei Netzwerk-Treibern anders aussieht, dann OK.



  • Also ich kenne den Leitfaden, wo man zum Compileren Kerneanzahl(incl HT)+1 nehmen soll. Mehr würde schaden. Nur kann ich den Rechner beim Compilieren nicht mit zu vielen Thread in die Knie zwingen, außer ich lass ihn dann doch mehr so viel RAM essen, daß kein Plattencache mehr da ist.
    Durch welchen Mechanismus skalieren Netzwerkverbindungen so unglaublich viel schlechter als Compile-Jobs?



  • ps: Wie könnte man das deiner Meinung nach sinnvoll testen? Ich überleg mir das nämlich gerade, und komm da nicht wirklich auf nen grünen Zweig.

    das stört mich auch sehr - Benchmarks oder Architektur/Technologie-Tests in solchen Größenordnungen laufen dabei fast immer direkt beim Kunden - ich hätte gerne 250 RASPBERRY PIs direkt auf meinem Tisch in einem grossen Rack mit ordentlich Kabel dazwischen, dann koennte man schoen spielen 🙂

    ...sehr starken PC zu verwenden wo man die Clients laufen lässt, und einen vergleichsweise schwachen PC auf dem man den Server laufen lässt...

    auch eine gute Idee

    denn kaum jmd. hat was schnelleres als Gigabit Ethernet zuhause

    ja da wirds schnell eng



  • Ich werfe einfach mal diesen Artikel in die Runde *g*duck'n'run* 🕶


Anmelden zum Antworten