11.15 POSIX-Threads und Netzwerkprogrammierung
Wenn sich fork() dazu eignet, einen parallelen Server zu entwickeln, dann müssten ja die Threads als Leichtgewichte sensationell im Vergleich zu den normalen Prozessen sein. Das Thema »Threads« wurde ja bereits in Kapitel 9 behandelt. Mit Threads würde man sich praktisch eine Menge Hauptspeicher sparen können, da ja das »aufwändige« Kopieren vom Eltern- in den Kindprozess entfallen würde und man auf Interprozesskommunikationen zum Austausch von Informationen zwischen Eltern- und Kindprozess verzichten könnte. Die Threads können ja als Leichtgewichtprozesse bis zu 100 Mal schneller sein als herkömmliche neue Prozesse, die mit fork() erzeugt wurden.
Bei all dem Hurra über Threads in der Netzwerkprogrammierung muss allerdings einiges beachtet werden, wenn man die Threads tatsächlich zur Socket-Programmierung verwenden will. Da wäre zum Beispiel die Anzahl der gleichzeitigen Threads. Wenn Threads also bei einem Server eingesetzt werden sollen, der mehrere tausend Clients bedienen soll, dann sind sie schon nicht mehr dafür geeignet.
Ein interessanter Vorschlag Man greift zur Extremlösung (die aber auch wunderbar funktioniert): Man forkt den Hauptprozess (Elternprozess) direkt nach dem listen() mehrmals, und jeder der Subprozesse (Kindprozess) erstellt dann ein paar Threads. Somit kommt man auch um das Limit gleichzeitiger Threads pro Prozess ...
|
Des Weiteren sind die Threads in der Netzwerkprogrammierung noch verhältnismäßig »jung«, so dass in puncto Zuverlässigkeit und Erfahrung noch recht wenige Kenntnisse vorhanden sind.
Hinweis Thread-sichere Versionen von Funktionen enden gewöhnlich mit der Endung _r. Aber dies ist kein Standard und somit für Netzwerkanwendungen, die ja gewöhnlich möglichst portabel sein sollten, eher ungeeignet.
|
Als Programmbeispiel soll auch hierzu wieder der Server verwendet werden, mit dem sich Dateien über das Netzwerk kopieren lassen. Allzu viel müssen Sie dabei im Gegensatz zum Beispiel des parallelen Servers mit fork() gar nicht ändern, um die Serveranwendung multithreading-fähig zu machen. Die Clientanwendung hingegen muss nicht verändert werden. Hierfür können Sie wieder dieselbe wie gehabt verwenden. Hier nun der Server, der Threads anstatt Prozesse verwendet, um einzelne Dateien über ein Netzwerk zu kopieren.
Hinweis Das Programm verwendet Linux-Threads und ist somit nicht unbedingt auf anderen UNIXen lauffähig. Voraussetzung sind Linux-Threads. Speziell unter FreeBSD müssen Sie die linuxthreads aus den ports installieren und das Programm folgendermaßen übersetzen:
$ gcc -o thserver thserver.c \
-I/usr/local/include/pthread/linuxthreads \
-L/usr/local/lib -llthread -llgcc_r
|
/* thserver.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd. h.>
#include <errno.h>
#include <pthread. h.>
#define MAX_ZEICHEN 1024
#define PORT_NUMMER 1234
static void * threading_socket( void *);
void copy (int);
pthread_t th;
static void *threading_socket (void *arg) {
pthread_detach (pthread_self ());
copy ((int) arg);
close ((int) arg);
return NULL;
}
static void copy (int connfd) {
int fd;
ssize_t j, n, ngesamt;
char puffer[MAX_ZEICHEN];
char path_file[MAX_ZEICHEN];
printf (" ... Daten empfangen\n");
/* Dateiname */
j = 0;
while ((n = read (connfd, &puffer[j], 1)) > 0) {
if (puffer[j] == '\n') {
puffer[j] = 0;
break;
}
j++;
}
if (n < 0) {
printf ("Fehler bei read() ...\n");
exit (EXIT_FAILURE);
}
printf ("Dateiname \"%s\" wird kopiert nach ", puffer);
strcpy (path_file, getenv ("HOME"));
strcat (path_file, "/tmp/");
strcat (path_file, puffer);
printf ("%s\n", path_file);
/* Datei zum Lesen öffnen */
if ((fd = open ( path_file,
O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) {
printf ("... kann %s nicht öffnen (%s)\n",
puffer, strerror(errno));
close (connfd);
}
/* Datei aus dem Socket lesen und in lokale Kopie schreiben */
ngesamt = 0;
while ((n = read (connfd, puffer, sizeof (puffer))) > 0) {
if (write (fd, puffer, n) != n) {
printf ("Fehler bei write() ...(%s)\n",
strerror(errno));
exit (EXIT_FAILURE);
}
ingesamt += n;
}
if (n < 0) {
printf ("Fehler bei read() ...\n");
}
printf ("... beendet (%d Bytes)\n", ingesamt);
close (fd);
close (connfd);
return;
}
int main (void) {
int sockfd, connfd;
struct sockaddr_in adresse;
size_t adrlaenge = sizeof (struct sockaddr_in);
const int y = 1;
if ((sockfd = socket (PF_INET, SOCK_STREAM, 0)) < 0) {
printf ("Fehler bei socket() ...(%s)\n",
strerror(errno));
exit (EXIT_FAILURE);
}
printf ("Socket erfolgreich angelegt\n");
adresse.sin_family = AF_INET;
adresse.sin_port = htons (PORT_NUMMER);
memset (&adresse.sin_addr, 0, sizeof (adresse.sin_addr));
setsockopt( sockfd, SOL_SOCKET,
SO_REUSEADDR, &y, sizeof(int));
if (bind ( sockfd,
(struct sockaddr *) &adresse,
sizeof (adresse) ) ) {
printf ("Fehler bei bind() ...(%s)\n",
strerror(errno));
exit (EXIT_FAILURE);
}
printf ("Server ist bereit und wartet ...\n");
if (listen (sockfd, 5)) {
printf ("Fehler bei listen() ...(%s)\n",
strerror(errno));
exit (EXIT_FAILURE);
}
while (1) {
connfd = accept ( sockfd,
(struct sockaddr *) &adresse,
&adrlaenge );
if (connfd < 0) {
if (errno == EINTR)
continue;
else {
printf ("Fehler bei accept() ...\n");
exit (EXIT_FAILURE);
}
}
pthread_create(&th, NULL, &threading_socket, connfd);
}
exit (EXIT_SUCCESS);
}
Ein Abdrucken des Programms bei der Ausführung kann ich mir hierbei ersparen, da sich nicht allzu viel im Gegensatz zu den Beispielen zuvor geändert hat. Außer in der Schleife des Servers hat sich auch nicht allzu viel im Listing verändert, nur dass das Beispiel jetzt ein wenig modularer aufgebaut ist.
while (1) {
connfd = accept ( sockfd,
(struct sockaddr *) &adresse,
&adrlaenge );
if (connfd < 0) {
if (errno == EINTR)
continue;
else {
printf ("Fehler bei accept() ...\n");
exit (EXIT_FAILURE);
}
}
pthread_create(&th, NULL, &threading_socket, connfd);
}
Hier wird ähnlich vorgegangen wie bei der Prozesserzeugung mittels fork() – mit dem Unterschied, wenn accept() zurückkehrt, dass hier ein Thread mit pthread_create() erzeugt wird anstatt ein neuer Prozess mit fork(). Jede Clientanfrage startet von nun an einen neuen Thread, und zwar den Thread threading_socket(). Zusätzlich übergeben Sie dem Thread als Argument den Deskriptor für das verbundene Socket (connfd).
static void * threading_socket (void *arg) {
pthread_detach (pthread_self ());
copy ((int) arg);
close ((int) arg);
return NULL;
}
In der Thread-Funktion threading_socket() wird der Thread mittels pthread_detach() ausgehängt, das heißt, der Haupt-Thread wartet nicht mehr auf die Rückkehr des von ihm angelegten Threads. Im nächsten Schritt wird dann die Funktion copy() aufgerufen, mit welcher der Kopiervorgang zwischen dem Client und dem Server-Thread abgearbeitet wird. Der Vorgang entspricht auch hier wieder demselben Code, den Sie schon im Beispiel des parallelen Servers mit fork() zuvor im Kindprozess kennen gelernt haben. Der Autor hat hierbei lediglich ein Copy & Paste gemacht. Als Argument übergeben Sie auch hier der Funktion den Deskriptor für das verbundene Socket. Wichtig ist dann, dass Sie vor der Rückkehr der Funktion threading_socket() das Socket mit close() schießen, da Threads ja alle Socket-Deskriptoren gemeinsam mit dem Main-Thread verwenden. Bei fork() ging dieser Vorgang ja nach Beendigung des Kindprozesses automatisch vor sich, da, wenn sich ein Prozess beendet, alle offenen Deskriptoren automatisch geschlossen werden.
|