Ein weiter Code, für den ich ebenfalls gerne auf mein Standard-Template zurückgreife ist die Basis-Implementierung eines stabilen Threading Socket-Servers. Im Wesentlichen wird der Socket mit den selben Aufrufen initialisiert wie im Client-Fall. Beim Server muss aber noch bind() und listen() aufgerufen werden. Zu dem Zweck von Bind und Listen möchte ich auf Google verweisen weil es hier nur um die Server-Funktionalität geht und nicht um die Socket-API im Detail.
In diesem Beispiel wird auch die Socket-Option SO_REUSEADDR gesetzt um während der Entwicklung nicht nach jedem Start ein paar Sekunden warten zu müssen, bis der Port im TCP-Stack wieder freigegeben wird. Es gibt einige Artikel zu dieser Option im Web. Ich habe nach set sock opt reuse address gesucht und fand den Artikel im ldn.net recht hilfreich. Am ausführlichsten sind natürlich immer noch die Werke von W. Richard Stevens aber leider auch ein bisschen ausschweifend.
Ein entscheidender Teil des Beispiels ist die Thread-Organisation. Das Grundprinzip ist fire and forget. Ein Traum! Die Threads die mit std::async erzeugt werden blockieren im Destruktor. Einfach ausgedrückt bedeutet das, man muss sich nicht den Thread merken, warten bis er beendet ist, joinen und Resourcen freigeben. Zu Promise und Future gibt es im Web ebenfalls viel zu lesen. Mit der Suche promise future c++ findet man die Seiten von Rainer Grim (grim-jaud.de), in denen die Anwendung übersichtlich darstellt ist (zumindest für meinen Geschmack).
Der Server ist sehr stabil und äußerst performant. Dazu habe ich zwei Tests durchgeführt.
Das folgende Shell-Skipt öffnet sequenziell 100.000 Telnet-Sitzungen, liest vom Server und gibt den Text auf die Konsole aus. Ein Blick in /proc und in den Speicher zeigt, dass es keine Resource-Leaks gibt. Die Laufzeit für 100.000 Durchläufe lag in zwei getrennten Versuchen bei etwa 3,5 Minuten. Das bedeutet eine Rate von etwa 500 Anfragen pro Sekunde. Es ist klar, dass bei dieser Art zu Messen, die Zeit für das Starten und Beenden des Telnet-Clients mitgezählt wird. Dennoch ist die Größenordnung bereits beeindruckend. Einen weiteren Eindruck von der Stabilität erhält man, wenn man die Shell-Anweisungen in mehreren Shells gleichzeitig ausführt.
time (export i=0; while [ $i -le 100000 ]; do typeset -i i=$i+1; telnet localhost 12345; done)
#include <string.h>
#include <netinet/in.h>
#include <unistd.h>
#include <future>
void service(int);
int main(int, char**)
{
const static int port = 12345;
int server = socket(AF_INET, SOCK_STREAM, 0);
if( server < 0)
{
perror("Server::Socket()\n"), exit(0);
}
struct sockaddr_in serv_addr;
memset((char *) &serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(port);
const int yes = 1;
if (setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0)
{
return 1;
}
if(bind(server, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
perror("Server::Bind()"), exit(0);
listen(server, 5);
struct sockaddr_in cli_addr;
int clilen = sizeof(cli_addr);
while(true)
{
int client = accept(server, (struct sockaddr *) &cli_addr, (socklen_t *)&clilen);
if(client < 0)
perror("Server::accept()"), exit(0);
std::async([&client]() { service(client); });
}
return 0;
}
std::mutex cs;
int value = 0;
void service(int client)
{
int v;
cs.lock();
v = value++;
cs.unlock();
char response[1024];
snprintf(response, sizeof(response), "Bisher %d Verbindungen abgefrühstückt\n", v);
int written = write(client, response, strlen(response));
fprintf(stderr, "%d Bytes gesendet\n", written);
close(client);
}