20.3 Elementare TCP-Socket-Funktionen - Pearson Bookshop
20.3 Elementare TCP-Socket-Funktionen - Pearson Bookshop
20.3 Elementare TCP-Socket-Funktionen - Pearson Bookshop
Verwandeln Sie Ihre PDFs in ePaper und steigern Sie Ihre Umsätze!
Nutzen Sie SEO-optimierte ePaper, starke Backlinks und multimediale Inhalte, um Ihre Produkte professionell zu präsentieren und Ihre Reichweite signifikant zu maximieren.
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 681<br />
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong><br />
Abbildung 20.5 zeigt die elementaren <strong>Socket</strong>-<strong>Funktionen</strong>, die bei einer typischen <strong>TCP</strong>-<br />
Client/Server-Realisierung benötigt werden. Diese elementaren <strong>Socket</strong>-<strong>Funktionen</strong> werden<br />
nachfolgend beschrieben.<br />
<strong>TCP</strong>Server<br />
socket()<br />
bind()<br />
<strong>TCP</strong>Client<br />
socket()<br />
listen()<br />
accept()<br />
hier blockiert, bis<br />
Client verbindet<br />
read()<br />
Aufbau der Verbindung<br />
ClientAnforderung<br />
connect()<br />
write()<br />
write()<br />
read()<br />
close()<br />
ServerAntwort<br />
Verbindungsabbruch (EOF)<br />
read()<br />
close()<br />
Abbildung 20.5: <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> für eine <strong>TCP</strong>-Client/Server-Realisierung<br />
<strong>20.3</strong>.1 Kreieren eines <strong>Socket</strong>s mit Funktion socket<br />
Um ein <strong>Socket</strong> anzulegen, steht die Funktion socket zur Verfügung.<br />
#include <br />
#include <br />
int socket(int domain, int typ, int protokoll);<br />
gibt zurück: <strong>Socket</strong>deskriptor (bei Erfolg); -1 bei Fehler<br />
Parameter domain:<br />
domain legt die zu benutzende Protokoll- bzw. Adressfamilie fest. Hierfür ist eine der<br />
Konstanten aus Tabelle 20.4 anzugeben. Es kann dabei für domain entweder die Konstante,<br />
die mit PF_ (Protokollfamilie) oder die mit AF_ (Adressfamilie) beginnt, angegeben<br />
werden. Das Vorhandensein von zwei möglichen Konstanten für die gleiche Protokollart<br />
ist historisch begründet, da ursprünglich eine Protokollfamilie mehrere Adressfamilien<br />
unterstützen sollte.
682 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
<br />
<br />
Die PF_-Konstante sollte dabei beim Anlegen des <strong>Socket</strong>s und die AF_-Konstante in<br />
den <strong>Socket</strong>-Adressstrukturen verwendet werden.<br />
Da jedoch bisher keine Protokollfamilie mehrere Adressfamilien unterstützt, sind die<br />
entsprechenden PF_-Konstanten gleich den AF_-Konstanten, wobei in der Vergangenheit<br />
meist die AF_-Konstanten verwendet wurden.<br />
Hier werden wir uns an die POSIX-Vorgabe halten: PF_-Konstanten beim Anlegen von<br />
<strong>Socket</strong>s und AF_-Konstanten in den Adressstrukturen. Zu erwähnen ist noch, dass es für<br />
Unix Domain Protokolle neben der AF_- und PF_-Unterscheidung noch zwei Varianten<br />
gibt: _UNIX bzw. _LOCAL. Auch das ist historisch begründet: die _UNIX-Konstanten wuden<br />
früher angegeben. Der heutige Standard empfiehlt die Verwendung der neu eingeführten<br />
_LOCAL-Variante.<br />
Protokollfamilie Adressfamilie Protokollart<br />
PF_UNIX, PF_LOCAL AF_UNIX, AF_LOCAL Unix Domain Protokolle (zur lokalen<br />
Kommunikation)<br />
PF_INET AF_INET IPv4-Protokolle<br />
PF_INET6 AF_INET6 IPv6-Protokolle<br />
PF_IPX AF_IPX IPX Novell Protokolle<br />
PF_X25 AF_X25 X.25/ISO-8208 Protokoll<br />
PF_AX25 AF_AX25 Amateur Radio AX.25 Protokoll<br />
PF_PACKET AF_PACKET Low level Paket-interface<br />
Tabelle 20.4: Einige häufig benutzte Protokoll- und Adressfamilien<br />
Unter Linux befinden sich alle möglichen Angaben zu domain in der Datei /usr/include/<br />
bits/socket.h.<br />
Während man bei der in 20.2.2 vorgestellten Funktion socketpair für den Parameter<br />
domain nur AF_LOCAL bzw. PC_LOCAL angeben kann, kann die Funktion socket verwendet<br />
werden, um <strong>Socket</strong>s einer beliebigen Protokollfamilie anzulegen.<br />
Parameter typ:<br />
Für den Parameter typ kann man eine der folgenden Konstanten angeben:<br />
SOCK_STREAM Stream-<strong>Socket</strong> (siehe auch Seite 657)<br />
SOCK_DGRAM Datagram-<strong>Socket</strong> (siehe auch Seite 657)
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 683<br />
SOCK_SEQPACKET<br />
SOCK_RAW<br />
ähnlich zu SOCK_STREAM mit dem Unterschied, dass Daten in der gleichen<br />
Paketgröße empfangen werden, in der sie geschrieben werden, was bei<br />
SOCK_STREAM nicht zutrifft, da hier die Daten nur als Bytestream ankommen<br />
und der Empfänger nicht weiß, ob diese durch ein write oder mehrere<br />
write-Aufrufe in das <strong>Socket</strong> geschrieben wurden. SOCK_SEQPACKET wird bei<br />
Protokollen wie X.25 oder AX.25 verwendet.<br />
Raw-<strong>Socket</strong><br />
Es sind zwar noch weitere Angaben möglich, aber diese sind nur für sehr spezifische<br />
Anwendungen von Interesse. Unter Linux befinden sich alle möglichen Angaben zu typ<br />
in der Datei /usr/include/bits/socket.h.<br />
Parameter protokoll:<br />
Der Parameter protokoll wählt das zu benutzende Protokoll aus der mit den ersten beiden<br />
Parametern festgelegten Protokollfamilie aus. Üblicherweise gibt man hier 0 an und<br />
lässt den Systemkern das Standardprotokoll für die entsprechende Protokollfamilie auswählen.<br />
Für PF_INET bzw. PF_INET6 ist <strong>TCP</strong> das Standard-Stream-Protokoll und UDP das<br />
Standard-Datagram-Protokoll. Weitere Protokollnummern können bei Bedarf in /etc/<br />
protocols nachgeschlagen werden.<br />
Nicht alle möglichen Kombinationen von domain- und typ-Angaben sind gültig. Tabelle<br />
20.5 zeigt die wichtigsten gültigen Kombinationen zusammen mit dem dabei verwendeten<br />
Protokoll.<br />
AF_INET AF_INET6 AF_LOCAL<br />
SOCK_STREAM <strong>TCP</strong> <strong>TCP</strong> Lokales Stream-<strong>Socket</strong><br />
SOCK_DGRAM UDP UDP Lokales Datagram-<strong>Socket</strong><br />
SOCK_RAW IPv4 IPv4 nicht gültig<br />
Tabelle 20.5: Kombinationen von domain- und typ-Angaben bei der socket-Funktion<br />
Ein mit socket kreiertes <strong>Socket</strong> ist noch nicht initialisiert. Für das <strong>Socket</strong> wird bei seiner<br />
Erzeugung lediglich ein bestimmtes Protokoll festgelegt, es wird aber noch nicht mit<br />
einer Ressource verbunden, so dass ein lesender oder schreibender Zugriff auf ihn noch<br />
nicht möglich ist.<br />
Es ist nun die Aufgabe einer Seite, üblicherweise des Serverprozesses, eine Verbindung<br />
vorzubereiten und darauf zu warten, dass irgendjemand sich mit ihm verbindet. Clientprozesse<br />
erzeugen ihrerseits ein <strong>Socket</strong>, teilen dem System die gewünschte Adresse mit<br />
und versuchen dann eine Verbindung aufzubauen. Die Verbindung ist hergestellt, wenn<br />
der Server, der auf einen Client wartet, den Verbindungsversuch akzeptiert. Danach können<br />
der Server- und Clientprozess über das <strong>Socket</strong> miteinander kommunizieren. Ist ein<br />
<strong>Socket</strong> richtig initialisiert, kann auf ihn wie auf jeden anderen Filedeskriptor mit den elementaren<br />
E/A-<strong>Funktionen</strong>, wie z.B. read oder write, zugegriffen werden.
684 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
Die Reihenfolge, in der Server- und Clientprozess die entsprechenden <strong>Funktionen</strong> aufrufen<br />
müssen, um eine Verbindung herzustellen, ist in Abbildung 20.5 veranschaulicht.<br />
Nachfolgend werden diese <strong>Funktionen</strong> näher vorgestellt.<br />
<strong>20.3</strong>.2 Zuordnen einer lokalen Protokolladresse mit Funktion bind<br />
(Server)<br />
Die Funktion bind ordnet einem <strong>Socket</strong> eine lokale Protokolladresse zu, was bei Internet-<br />
Protokollen die Kombination aus einer IPv4- bzw. IPV6-Adresse mit einer 16-Bit <strong>TCP</strong>oder<br />
UDP-Portnummer ist.<br />
#include <br />
#include <br />
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);<br />
gibt zurück: 0 (bei Erfolg); -1 bei Fehler<br />
sockfd gibt den <strong>Socket</strong>deskriptor des entsprechenden <strong>Socket</strong>s an.<br />
Der zweite Parameter ist die Adresse der entsprechenden <strong>Socket</strong>-Adressstruktur und der<br />
dritte Parameter die Größe dieser Adressstruktur.<br />
In *my_addr kann beim Aufruf von bind die IP-Adresse und die Portnummer gezielt<br />
gesetzt sein. Es ist jedoch auch möglich, dass eine dieser beiden Komponenten oder auch<br />
beide auf 0 gesetzt sind, was bedeutet, dass man es dem Kernel überlässt, was er dafür<br />
wählt. Statt dem Wert 0 sollte man der Komponente sin_addr die Konstante INADDR_ANY<br />
(htonl(INADDR_ANY)) und der Komponente sin6_addr die Variable sin6addr_any<br />
zuweisen, die meist den Wert 0 repräsentieren. Solche INADDR_ANY- bzw. sin6addr_any-<br />
Adressen bezeichnet man als Wildcard-Adressen.<br />
Tabelle 20.6 fasst diese Möglichkeiten mit den daraus resultierenden Zuordnungen<br />
zusammen.<br />
Angabe<br />
Auswirkung<br />
sin_addr, sin6_addr<br />
sin_port, sin6_port<br />
Wildcard 0 Kernel wählt IP-Adresse und kurzlebige<br />
Portnummer<br />
Wildcard ungleich 0 Kernel wählt IP-Adresse; Portnummer<br />
festgelegt<br />
lokale IP-Adresse 0 festgelegte IP-Adresse; Kernel wählt kurzlebige<br />
Portnummer<br />
lokale IP-Adresse ungleich 0 festgelegte IP-Adresse und Portnummer<br />
Tabelle 20.6: Unterschiedliche Möglichkeiten und Auswirkungen bei Spezifikation der Protokolladresse
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 685<br />
Server binden beim Start deren bekannte Ports. Verzichtet ein <strong>TCP</strong>-Client bzw. -Server<br />
darauf, wählt der Kernel beim Aufruf von connect bzw. listen für das <strong>Socket</strong> einen kurzlebigen<br />
Port aus. Kurzlebige Portnummern sind bei Clients üblich.<br />
Um die lokale Protokolladresse eines <strong>Socket</strong>s zu ermitteln, steht die Funktion getsockname<br />
(siehe auch <strong>20.3</strong>.5) zur Verfügung.<br />
Programm 20.6 (sockname.c) verdeutlicht nochmals die Vergabe der Werte aus Tabelle 20.6.<br />
#include "netzprog.h"<br />
int<br />
main(int argc, char *argv[])<br />
{<br />
struct sockaddr_in sa;<br />
int<br />
i, sock, len_sa = sizeof(sa);<br />
if (argc != 2)<br />
fehler_meld(FATAL, "usage: %s IPv4-Nr", argv[0]);<br />
printf("%15s | %10s | %20s |\n", "IP-Adresse", "Portnummer", "Resultat");<br />
for (i=1; i
686 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
Nachdem man Programm 20.6 (sockname.c) kompiliert und gelinkt hat:<br />
cc -o sockname sockname.c netzprog.c fehler.c<br />
zeigt es abhängig von der übergebenen IPv4-Adresse die entsprechend resultierende IP-<br />
Adresse und Portnummer, wie z.B.:<br />
$ ./sockname 127.23.182.9<br />
IP-Adresse | Portnummer | Resultat |<br />
0.0.0.0 | 33970 | 0.0.0.0/33970 |<br />
0.0.0.0 | 12345 | 0.0.0.0/12345 |<br />
127.23.182.9 | 33971 | 127.23.182.9/33971 |<br />
127.23.182.9 | 12345 | 127.23.182.9/12345 |<br />
$<br />
<strong>20.3</strong>.3 Warten auf Verbindungsanforderungen mit listen und accept<br />
(Server)<br />
Nachdem ein <strong>Socket</strong> mit bind durch einen Serverprozess an eine Protokolladresse gebunden<br />
wurde, teilt der Serverprozess durch einen Aufruf der Funktion listen dem System<br />
mit, dass er bereit ist, mit anderen Prozessen über dieses <strong>Socket</strong> Verbindungen einzugehen.<br />
Bevor aber wirklich eine Verbindung für ein <strong>Socket</strong>, das mit listen vom Serverprozess<br />
abgehört wird, aufgebaut wird, muss der Serverprozess den Verbindungsversuch<br />
seitens des Clients mit dem Aufruf der Funktion accept akzeptieren.<br />
Ruft der Serverprozess accept vor einem Verbindungsversuch seitens des Clients auf, so<br />
blockiert normalerweise accept so lange, bis der Client einen Verbindungswunsch äußert.<br />
Um eine solche Blockierung bei accept zu unterbinden, muss das <strong>Socket</strong> mit fcntl als<br />
nicht blockierend markiert werden. In diesem Fall kehrt accept sofort mit einer entsprechenden<br />
Fehlernummer zurück. Um festzustellen, ob ein Verbindungswunsch seitens<br />
des Clients ansteht, was man mit pending bezeichnet, kann die Funktion select verwendet<br />
werden. Die beiden <strong>Funktionen</strong> listen und accept sind wie folgt deklariert:<br />
#include <br />
#include <br />
int listen(int sockfd, int backlog);<br />
gibt zurück: 0 (bei Erfolg); -1 bei Fehler<br />
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);<br />
gibt zurück: <strong>Socket</strong>deskriptor des akzeptierten <strong>Socket</strong>s (bei Erfolg); -1 bei Fehler<br />
Beide <strong>Funktionen</strong> erwarten als ersten Parameter den <strong>Socket</strong>deskriptor des entsprechenden<br />
<strong>Socket</strong>s.<br />
Der Parameter backlog legt die maximal erlaubte Anzahl von anstehenden (pending) Verbindungswünschen<br />
seitens des Clients fest. Wird dieses Maximum erreicht, so werden<br />
weitere Verbindungsversuche seitens des Clients abgelehnt, wobei dies dem Client mit
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 687<br />
dem Fehler ECONNREFUSED mitgeteilt wird. BSD legte früher als maximale Größe von<br />
backlog 5 fest. Bei heutigen Servern im Internet, die Hunderttausende von Anforderungen<br />
in der Stunde erhalten, muss man für backlog einen größeren Wert angeben, wie etwa<br />
16, 20 oder noch größer.<br />
Die Funktion accept macht aus einer anstehenden Verbindung eine wirkliche Verbindung,<br />
die auch einen neuen <strong>Socket</strong>deskriptor erhält. Dieser neue <strong>Socket</strong>deskriptor, der<br />
als Rückgabewert geliefert wird, erbt alle Attribute vom <strong>Socket</strong>, das zuvor mit listen<br />
abgehört wurde.<br />
Den als erstes Argument übergebenen <strong>Socket</strong>deskriptor bezeichnet man als horchenden<br />
<strong>Socket</strong> und den von accept zurückgegebenen als verbundenen <strong>Socket</strong>. Auf dem horchenden<br />
<strong>Socket</strong> kann der Server weitere Verbindungsanforderungen akzeptieren, wobei jedes Mal<br />
ein neues verbundenes <strong>Socket</strong> mit einem neuen <strong>Socket</strong>deskriptor zur Verfügung gestellt<br />
wird. Beendet der Server die Bedienung eines Clients, wird nur das jeweilige verbundene<br />
<strong>Socket</strong> geschlossen.<br />
Die Parameter addr und addrlen geben die Adressen an, in die vom Systemkern die<br />
Adresse des Clients zu schreiben ist. Es ist zu beachten, dass der Parameter addrlen zwei<br />
<strong>Funktionen</strong> hat: Beim Aufruf legt er die Länge der übergebenen Adressstruktur addr fest,<br />
und bei der Rückkehr von accept enthält er die aktuelle Anzahl von Bytes, die in addr<br />
geschrieben wurden. Deswegen ist dieser Parameter auch als Zeiger deklariert.<br />
<strong>20.3</strong>.4 Aufbauen einer Verbindung zum Server mit connect (Client)<br />
Auch ein Client könnte nach dem Erzeugen eines <strong>Socket</strong>s mit der Funktion bind diesem<br />
<strong>Socket</strong> eine Adresse zuweisen. Da diese lokale Adresse aber normalerweise für den<br />
Client nicht von Interesse ist, lässt er diesen Schritt oft aus und überlässt es dem Systemkern,<br />
irgendeine passende Adresse für das <strong>Socket</strong> zu finden. In jedem Fall muss jedoch<br />
ein Client die Funktion connect aufrufen, um eine Verbindung zum Server herzustellen.<br />
#include <br />
#include <br />
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);<br />
gibt zurück: 0 (bei Erfolg); -1 bei Fehler<br />
Die Parameter beim connect-Aufruf spezifizieren das zu verbindende <strong>Socket</strong> und die Zieladresse<br />
des Servers.<br />
Hat ein Prozess die Arbeit mit einem <strong>Socket</strong> beendet, sollte er dieses mit close schließen,<br />
um die damit verbundenen Ressourcen wieder freizugeben.
688 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
<strong>20.3</strong>.5 Ermitteln der lokalen bzw. der fremden Protokolladresse<br />
Um die lokale oder die mit einem <strong>Socket</strong> verbundene fremde Protokolladresse zu erfragen,<br />
stehen die beiden folgenden <strong>Funktionen</strong> zur Verfügung:<br />
#include <br />
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);<br />
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);<br />
geben zurück: 0 (bei Erfolg); -1 bei Fehler<br />
getsockname liefert über addr die lokale und getpeername die fremde Protokolladresse zu<br />
sockfd.<br />
Da beide <strong>Funktionen</strong> die Struktur addr füllen und den Parameter addrlen auf die Länge<br />
der Struktur setzen, muss für addrlen bei beiden beim Aufruf ein Zeiger (Adresse) angegeben<br />
werden.<br />
Diese beiden <strong>Funktionen</strong> werden in folgenden Situationen benötigt:<br />
getsockname<br />
<br />
<br />
<br />
in einem Client nach dem Aufruf von connect (ohne einen bind-Aufruf). In diesem<br />
Fall liefert getsockname die lokale IP-Adresse und Portnummer, die der Kernel dieser<br />
Verbindung zugeordnet hat.<br />
nach einem bind-Aufruf mit Portnummer 0. In diesem Fall liefert getsockname die<br />
vom Kernel zugewiesene lokale Portnummer.<br />
in einem Server nach einem bind mit einer Wildcard-Adresse und einem dann erfolgreichen<br />
accept-Aufruf. In diesem Fall liefert getsockname die der Verbindung zugeteilte<br />
lokale IP-Adresse. Für sockfd muss hier der <strong>Socket</strong>deskriptor des verbundenen<br />
<strong>Socket</strong>s (nicht des horchenden) angegeben werden.<br />
getpeername<br />
in einem Server, der nach einem fork im Kindprozess exec aufruft, um hier ein anderes<br />
Serverprogramm, wie z.B. telnet zu laden. In diesem Fall steht die <strong>Socket</strong>-Adressstruktur<br />
mit der Adresse des Peers dem neuen Server nicht mehr zur Verfügung, wohl aber bleibt<br />
der <strong>Socket</strong>deskriptor über den exec-Aufruf hinweg geöffnet. Um nun die IP-Adresse und<br />
Portnummer zu ermitteln, muss hier getpeername aufgerufen werden.<br />
Die folgende Funktion liefert die Adressfamilie zum <strong>Socket</strong> sockfd zurück:<br />
int sockfd_family(int sockfd) {<br />
union {<br />
struct sockaddr sa; /* MAXSOCKADDR = maximal mögliche */<br />
char daten[MAXSOCKADDR]; /* <strong>Socket</strong>-Adressstruktur in Bytes */<br />
} un;<br />
socklen_t len = MAXSOCKADDR;
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 689<br />
}<br />
if (getsockname(sockfd, (struct sockaddr *)un.daten, &len) < 0)<br />
return -1;<br />
return un.sa.sa_family;<br />
Diese Funktion nehmen wir wieder genauso wie die folgenden <strong>Funktionen</strong> in der Programmdatei<br />
netzprog.c auf:<br />
void my_shutdown(int fd, int wie) {<br />
if (shutdown(fd, wie) < 0)<br />
fehler_meld(FATAL_SYS, "shutdown-Fehler");<br />
}<br />
void my_getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {<br />
if (getpeername(sockfd, addr, addrlen) < 0)<br />
fehler_meld(FATAL_SYS, "getpeername-Fehler");<br />
}<br />
void my_getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {<br />
if (getsockname(sockfd, addr, addrlen) < 0)<br />
fehler_meld(FATAL_SYS, "getsockname-Fehler");<br />
}<br />
int my_socket(int family, int type, int protocol) {<br />
int n;<br />
if ( (n = socket(family, type, protocol)) < 0)<br />
fehler_meld(FATAL_SYS, "socket-Fehler");<br />
return n;<br />
}<br />
void my_bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {<br />
if (bind(sockfd, addr, addrlen) < 0)<br />
fehler_meld(FATAL_SYS, "bind-Fehler");<br />
}<br />
void my_listen(int sockfd, int backlog) {<br />
if (listen(sockfd, backlog) < 0)<br />
fehler_meld(FATAL_SYS, "listen-Fehler");<br />
}<br />
int my_accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {<br />
int n;<br />
while ( (n = accept(sockfd, addr, addrlen)) < 0)<br />
if (errno != EPROTO && errno != ECONNABORTED)<br />
fehler_meld(FATAL_SYS, "accept-Fehler");<br />
return n;<br />
}<br />
void my_connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {<br />
if (connect(sockfd, addr, addrlen) < 0)<br />
fehler_meld(FATAL_SYS, "connect-Fehler");<br />
}<br />
<strong>20.3</strong>.6 Eine einfache iterative <strong>TCP</strong>-Client/Server-Realisierung<br />
Programm 20.7 (server2.c) zeigt einen Server, der auf Clientanforderungen wartet. Beim<br />
Ankommen einer Clientanforderung schickt er dem Client einen Text mit einer Zufallszahl,<br />
den dieser dann ausgeben kann.
690 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
#include <br />
#include "netzprog.h"<br />
int<br />
main(int argc, char *argv[])<br />
{<br />
char *serv_addr, *serv_port; /* IP-Adresse und Portnummer */<br />
struct sockaddr_in adr_serv, adr_clnt; /* Server- und Clientadressstr. */<br />
int<br />
n, len_inet;<br />
int serv_sock, clnt_sock; /* Server- und Clientsocket */<br />
char puffer[MAX_ZEICHEN]; /* String für Serverantwort */<br />
srand(time(NULL));<br />
serv_addr = (argc >= 2) ? argv[1] : "127.0.0.1"; /* IP-Adresse */<br />
serv_port = (argc >= 3) ? argv[2] : "9876"; /* Portnummer */<br />
/*... Verbindungs-<strong>Socket</strong> erzeugen */<br />
serv_sock = my_socket(PF_INET, SOCK_STREAM, 0);<br />
/*... <strong>Socket</strong> mit eigenen Adressen verknüpfen */<br />
memset(&adr_serv, 0, sizeof(adr_serv));<br />
adr_serv.sin_family = AF_INET;<br />
adr_serv.sin_port = htons(atoi(serv_port));<br />
if (strcmp(serv_addr, "*")) {<br />
adr_serv.sin_addr.s_addr = inet_addr(serv_addr);<br />
if ( adr_serv.sin_addr.s_addr == INADDR_NONE )<br />
fehler_meld(FATAL, "Ungültige Adresse");<br />
else<br />
adr_serv.sin_addr.s_addr = htonl(INADDR_ANY); /* wildcard */<br />
/*... <strong>Socket</strong> eine Protokolladresse zuordnen */<br />
my_bind(serv_sock, (struct sockaddr *)&adr_serv, sizeof(adr_serv));<br />
/*... <strong>Socket</strong> in passiven Zustand für Verbindungsannahmen versetzen */<br />
my_listen(serv_sock, 10);<br />
printf("... Akzeptiere Verbindungen auf Port %d\n", ntohs(adr_serv.sin_port));<br />
while (1) {<br />
/*... Verbindung von Client akzeptieren */<br />
len_inet = sizeof(adr_clnt); /* Auf connect seitens Clients warten */<br />
clnt_sock = my_accept(serv_sock, (struct sockaddr *)&adr_clnt, &len_inet);<br />
n = sprintf(puffer, "--------------------------------------\n"<br />
"Verbindung von Client %s (Port %d) akzeptiert\n"<br />
".......... Deine Glückszahl ist %d\n"<br />
"--------------------------------------\n",<br />
inet_ntoa(adr_clnt.sin_addr), ntohs(adr_clnt.sin_port),<br />
rand()%100+1);<br />
my_write(clnt_sock, puffer, n, ""); /* Client die Antwort schicken */<br />
my_close(clnt_sock); /* Verbindung zum Client schließen */
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 691<br />
}<br />
}<br />
exit(0); /* wird aufgrund der Endlosschleife nie ausgeführt */<br />
Programm 20.7 (server2.c): Server, der Clients einen Text mit einer Zufallszahl schickt<br />
Nachdem man Programm 20.7 (server2.c) kompiliert und gelinkt hat:<br />
cc -o server2 server2.c netzprog.c fehler.c<br />
kann man den Server im Hintergrund starten:<br />
$ ./server2 & [Server im Hintergrund starten]<br />
... Akzeptiere Verbindungen auf Port 9876<br />
[1] 2608<br />
$<br />
Nun kann man mit telnet Clientanforderungen an diesen Server schicken; voreingestellt<br />
sind die IP-Nummer: 127.0.0.1 und die Portnummer 9876:<br />
$ telnet 127.0.0.1 9876<br />
Trying 127.0.0.1...<br />
Connected to 127.0.0.1.<br />
Escape character is '^]'.<br />
--------------------------------------<br />
Verbindung von Client 127.0.0.1 (Port 32938) akzeptiert<br />
.......... Deine Glückszahl ist 99<br />
--------------------------------------<br />
Connection closed by foreign host.<br />
$ telnet 127.0.0.1 9876<br />
Trying 127.0.0.1...<br />
Connected to 127.0.0.1.<br />
Escape character is '^]'.<br />
--------------------------------------<br />
Verbindung von Client 127.0.0.1 (Port 32939) akzeptiert<br />
.......... Deine Glückszahl ist 46<br />
--------------------------------------<br />
Connection closed by foreign host.<br />
$ kill %1 [Server beenden]<br />
[1]+ Beendet ./server2<br />
$<br />
Startet man einen gerade beendeten Server zu schnell wieder, kann ein Fehler auftreten,<br />
wie z.B.:<br />
$ ./server2 &<br />
bind-Fehler: Address already in use<br />
[2] 2971<br />
[2] Exit 1 ./server2<br />
$<br />
Das liegt daran, dass die <strong>TCP</strong>-Implementierungen der meisten Unix-Systeme üblicherweise<br />
Einschränkungen bezüglich der Wiederbenutzung eines Verbindungspunktes
692 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
(lokaler Port am lokalen Rechner) machen. So gilt z.B. für <strong>TCP</strong>-Ports, dass diese erst nach<br />
zwei Minuten wieder verwendet werden können. Mit der Option SO_REUSEADDR beim Aufruf<br />
der Funktion setsockopt kann man festlegen, dass diese Einschränkung aufzuheben<br />
ist und der entsprechende <strong>TCP</strong>-Port innerhalb einer kurzen Zeit wieder benutzt werden<br />
kann. Darauf wird in Kapitel 20.6 näher eingegangen.<br />
In den obigen Ablaufbeispielen konnte der gestartete Server nur über die fest vergebene<br />
IP-Adresse 127.0.0.1 kontaktiert werden. Gibt man beim Aufruf als Argument '*' an,<br />
wird eine wildcard-IP-Nummer verwendet, so dass der Server nicht nur über die lokale<br />
IP-Adresse 127.0.0.1 kontaktiert werden kann, sondern auch von einem fremden Rechner,<br />
da in diesem Fall über die externe Adresse die Anforderung an die lokale Adresse<br />
weitergeschickt wird:<br />
$ ./server2 '*' & [Server im Hintergrund starten]<br />
[1] 8212<br />
... Akzeptiere Verbindungen auf Port 9876<br />
$ telnet 127.0.0.1 9876<br />
Trying 127.0.0.1...<br />
Connected to 127.0.0.1.<br />
Escape character is '^]'.<br />
--------------------------------------<br />
Verbindung von Client 127.0.0.1 (Port 36393) akzeptiert<br />
.......... Deine Glückszahl ist 29<br />
--------------------------------------<br />
Connection closed by foreign host.<br />
$ /sbin/ifconfig [Erfragen der externen IP-Adresse des Rechners]<br />
eth0 Protokoll:Ethernet Hardware Adresse 00:E0:7D:7D:16:98<br />
inet Adresse:192.168.69.6 Bcast:192.168.69.255 Maske:255.255.255.0<br />
inet6 Adresse: fe80::2e0:7dff:fe7d:1698/64 Gültigkeitsbereich:Verbindung<br />
.......<br />
lo Protokoll:Lokale Schleife<br />
inet Adresse:127.0.0.1 Maske:255.0.0.0<br />
inet6 Adresse: ::1/128 Gültigkeitsbereich:Maschine<br />
.......<br />
$ telnet 192.168.69.6 9876<br />
Trying 192.168.69.6...<br />
Connected to 192.168.69.6.<br />
Escape character is '^]'.<br />
--------------------------------------<br />
Verbindung von Client 192.168.69.6 (Port 36397) akzeptiert<br />
.......... Deine Glückszahl ist 83<br />
--------------------------------------<br />
Connection closed by foreign host.<br />
$<br />
Nun probieren wir, den Server von einem fremden Rechner über seine IP-Adresse<br />
192.168.69.6 zu kontaktieren:<br />
Rechner birne $ telnet 192.168.69.6 9876<br />
Trying 192.168.69.6...<br />
Connected to 192.168.69.6.<br />
Escape character is '^]'.
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 693<br />
--------------------------------------<br />
Verbindung von Client 192.168.69.2 (Port 32791) akzeptiert<br />
.......... Deine Glückszahl ist 36<br />
--------------------------------------<br />
Connection closed by foreign host.<br />
Rechner birne $<br />
Durch die Verwendung von wildcard-IP-Nummern ist also der Server nicht mehr nur<br />
über eine feste IP-Nummer kontaktierbar.<br />
Beenden wir noch unseren Server:<br />
$ kill %1 [Server beenden]<br />
[1]+ Beendet ./server2<br />
$<br />
Statt des telnet-Kommandos soll nun ein eigenes Clientprogramm (siehe Programm 20.8<br />
(client2.c)) erstellt werden.<br />
#include "netzprog.h"<br />
int<br />
main(int argc, char *argv[])<br />
{<br />
char *serv_addr, *serv_port; /* IP-Adresse und Portnummer */<br />
struct sockaddr_in adr_serv; /* Serveradressstruktur */<br />
int n, sock; /* <strong>Socket</strong> */<br />
char puffer[MAX_ZEICHEN]; /* String für Serverantwort */<br />
serv_addr = (argc >= 2) ? argv[1] : "127.0.0.1"; /* IP-Adresse */<br />
serv_port = (argc >= 3) ? argv[2] : "9876"; /* Portnummer */<br />
/*... <strong>Socket</strong> erzeugen */<br />
sock = my_socket(PF_INET, SOCK_STREAM, 0);<br />
/*... <strong>Socket</strong> mit eigenen Adressen verknüpfen */<br />
memset(&adr_serv, 0, sizeof(adr_serv));<br />
adr_serv.sin_family = AF_INET;<br />
adr_serv.sin_port = htons(atoi(serv_port));<br />
adr_serv.sin_addr.s_addr = inet_addr(serv_addr);<br />
if ( adr_serv.sin_addr.s_addr == INADDR_NONE )<br />
fehler_meld(FATAL, "Ungültige Adresse");<br />
/*... Verbindung zum Server aufbauen */<br />
my_connect(sock, (struct sockaddr*)&adr_serv, sizeof(adr_serv));<br />
printf("... Verbindung zu Server %s (Port %d) aufgebaut\n",<br />
inet_ntoa(adr_serv.sin_addr), ntohs(adr_serv.sin_port));<br />
/*... Serverantwort lesen */<br />
n = my_read(sock, &puffer, sizeof(puffer)-1, "");<br />
puffer[n] = '\0'; /* \0-terminieren */<br />
printf("%s\n", puffer);
694 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
}<br />
my_close(sock); /* <strong>Socket</strong> schließen und Client beenden */<br />
exit(0);<br />
Programm 20.8 (client2.c): Client, der eine Anforderung an den Server schickt<br />
Nachdem man Programm 20.8 (client2.c) kompiliert und gelinkt hat:<br />
cc -o client2 client2.c netzprog.c fehler.c<br />
kann man den Server im Hintergrund starten:<br />
$ ./server2 '*' & [Server im Hintergrund starten]<br />
... Akzeptiere Verbindungen auf Port 9876<br />
[1] 8314<br />
$<br />
und dann unser Clientprogramm verwenden, um Clientanforderungen an den Server zu<br />
schicken:<br />
$ ./client2 127.0.0.1<br />
... Verbindung zu Server 127.0.0.1 (Port 9876) aufgebaut<br />
--------------------------------------<br />
Verbindung von Client 127.0.0.1 (Port 36401) akzeptiert<br />
.......... Deine Glückszahl ist 27<br />
--------------------------------------<br />
$ ./client2 192.168.69.6<br />
... Verbindung zu Server 192.168.69.6 (Port 9876) aufgebaut<br />
--------------------------------------<br />
Verbindung von Client 192.168.69.6 (Port 36402) akzeptiert<br />
.......... Deine Glückszahl ist 63<br />
--------------------------------------<br />
$ kill %1 [Server beenden]<br />
[1]+ Beendet ./server2<br />
$<br />
<strong>20.3</strong>.7 Eine einfache parallele <strong>TCP</strong>-Client/Server-Realisierung<br />
In Programm 20.7 (server2.c) wurde der Server iterativ realisiert. Für einen einfachen<br />
Server wie diesen, der nur wenig Bearbeitungszeit für die Antwort (Füllen eines kleinen<br />
Text-Puffers, der eine Zufallszahl enthält) benötigt, ist dies ausreichend. Benötigt der Server<br />
aber mehr Zeit für die Bearbeitung der Clientanforderung, so müssen neu ankommende<br />
Clientanforderungen warten, bis er mit der Beantwortung dieser einen<br />
Anforderung fertig ist. In diesem Fall sollten die Clientanforderung gleichzeitig (parallel)<br />
bedient werden, was mit dem Kreieren von Kindprozessen mittels fork möglich ist.<br />
Programm 20.9 (server3.c) zeigt eine erste (noch fehlerhafte) Realisierung eines solchen<br />
parallel arbeitenden Servers.<br />
1 #include <br />
2 #include "netzprog.h"
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 695<br />
3<br />
4 /*.... hier wird noch Code eingefügt */<br />
5<br />
6 int<br />
7 main(int argc, char *argv[])<br />
8 {<br />
9 char *serv_addr, *serv_port; /* IP-Adresse und Portnummer */<br />
10 struct sockaddr_in adr_serv, adr_clnt; /* Server- und Clientadressstr. */<br />
11 int n, len_inet;<br />
12 int serv_sock, clnt_sock; /* Server- und Clientsocket */<br />
13 char puffer[MAX_ZEICHEN]; /* String für Serverantwort */<br />
14 pid_t pid;<br />
15<br />
16 serv_addr = (argc >= 2) ? argv[1] : "127.0.0.1"; /* IP-Adresse */<br />
17 serv_port = (argc >= 3) ? argv[2] : "9876"; /* Portnummer */<br />
18<br />
19 /*... Verbindungs-<strong>Socket</strong> erzeugen */<br />
20 serv_sock = my_socket(PF_INET, SOCK_STREAM, 0);<br />
21<br />
22 /*... <strong>Socket</strong> mit eigenen Adressen verknüpfen */<br />
23 memset(&adr_serv, 0, sizeof(adr_serv));<br />
24 adr_serv.sin_family = AF_INET;<br />
25 adr_serv.sin_port = htons(atoi(serv_port));<br />
26 if (strcmp(serv_addr, "*")) {<br />
27 adr_serv.sin_addr.s_addr = inet_addr(serv_addr);<br />
28 if ( adr_serv.sin_addr.s_addr == INADDR_NONE )<br />
29 fehler_meld(FATAL, "Ungültige Adresse");<br />
30 } else<br />
31 adr_serv.sin_addr.s_addr = htonl(INADDR_ANY); /* wildcard */<br />
32<br />
33 /*... <strong>Socket</strong> eine Protokolladresse zuordnen */<br />
34 my_bind(serv_sock, (struct sockaddr *)&adr_serv, sizeof(adr_serv));<br />
35<br />
36 /*... <strong>Socket</strong> in passiven Zustand für Verbindungsannahmen versetzen */<br />
37 my_listen(serv_sock, 10);<br />
38 printf("... Akzeptiere Verbindungen auf Port %d\n", ntohs(adr_serv.sin_port));<br />
39<br />
40 /*.... hier wird noch Code eingefügt */<br />
41<br />
42 while (1) {<br />
43 srand(time(NULL));<br />
44<br />
45 /*... Verbindung von Client akzeptieren */<br />
46 len_inet = sizeof(adr_clnt); /* Auf connect seitens Clients warten */<br />
47 if ( (clnt_sock = accept(serv_sock, (struct sockaddr *)&adr_clnt,<br />
48 &len_inet)) == -1) {<br />
49 /*.....................................*/<br />
50 /*.... hier wird noch Code eingefügt */<br />
51 /*.....................................*/<br />
52 fehler_meld(FATAL_SYS, "accept-Fehler");<br />
53 }<br />
54<br />
55 if ( (pid = fork()) == -1)
696 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
56 fehler_meld(FATAL_SYS, "fork-Fehler");<br />
57<br />
58 else if (pid == 0) { /*..................................... Kindprozess */<br />
59 my_close(serv_sock); /* dupliziertes Serversocket schließen */<br />
60<br />
61 sleep(2+rand()%5); /* Lange Bearbeitungszeit simulieren */<br />
62 n = sprintf(puffer, "...Client %s akzeptiert; Glückszahl: %d\n",<br />
63 inet_ntoa(adr_clnt.sin_addr), rand()%100+1);<br />
64<br />
65 my_write(clnt_sock, puffer, n); /* Client die Antwort schicken */<br />
66 my_close(clnt_sock); /* Dupliziertes Clientsocket schließen */<br />
67 exit(0); /* Kindprozess beenden */<br />
68 } /*............................................... Ende von Kindprozess */<br />
69<br />
70 my_close(clnt_sock); /* Clientsocket im Elternprozess schließen */<br />
71 }<br />
72 exit(0); /* wird aufgrund der Endlosschleife nie ausgeführt */<br />
73 }<br />
74 /*.........................................................................*/<br />
75 /*.... */<br />
76 /*.... */<br />
77 /*.... hier wird noch Code eingefügt */<br />
78 /*.... */<br />
79 /*.... */<br />
80 /*.... */<br />
81 /*.........................................................................*/<br />
Programm 20.9 (server3.c): Server, der parallel ankommende Clientanforderungen bearbeitet<br />
Nachdem man Programm 20.9 (server3.c) kompiliert und gelinkt hat:<br />
cc -o server3 server3.c netzprog.c fehler.c<br />
kann man den Server im Hintergrund starten:<br />
$ ./server3 & [Server im Hintergrund starten]<br />
... Akzeptiere Verbindungen auf Port 9876<br />
[2] 6917<br />
$<br />
Nun kann man Clients starten (identisch zu Programm 20.8 (client2.c)):<br />
$ ./client2 127.0.0.1<br />
... Verbindung zu Server 127.0.0.1 (Port 9876) aufgebaut<br />
...Client 127.0.0.1 akzeptiert; Glückszahl: 3<br />
$ ./client2 127.0.0.1<br />
... Verbindung zu Server 127.0.0.1 (Port 9876) aufgebaut<br />
...Client 127.0.0.1 akzeptiert; Glückszahl: 26<br />
$ ./client2 127.0.0.1<br />
... Verbindung zu Server 127.0.0.1 (Port 9876) aufgebaut<br />
...Client 127.0.0.1 akzeptiert; Glückszahl: 87<br />
$
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 697<br />
Auf den ersten Blick scheint alles richtig zu funktionieren. Allerdings sieht man bei diesem<br />
Ablaufbeispiel nicht, dass mit jeder Clientanforderung ein Zombieprozess im System<br />
verbleibt, nämlich der für jede Clientanforderung kreierte Server-Kindprozess, was man<br />
an folgender Ausgabe erkennen kann:<br />
$ ps -l<br />
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD<br />
..................<br />
0 S 500 6917 2185 0 75 0 – 339 schedu pts/23 00:00:00 server3<br />
0 Z 500 6921 6917 0 75 0 – 0 exit pts/23 00:00:00 server3 <br />
0 Z 500 6925 6917 0 75 0 – 0 exit pts/23 00:00:00 server3 <br />
0 Z 500 6929 6917 0 75 0 – 0 exit pts/23 00:00:00 server3 <br />
..................<br />
$<br />
Da wir den exit-Status des Server-Kindprozesses nicht mittels waitpid erfragen, bleibt<br />
jeder gestartete Server-Kindprozess als Zombie im System erhalten, was natürlich bei vielen<br />
Clientanforderungen früher oder später zu einer Belastung des Systems wird. Erst<br />
wenn der Serverprozess beendet wird, was eventuell Tage, Wochen oder auch Monate<br />
dauern kann, verschwinden auch die Zombie-Prozesse.<br />
$ kill %2<br />
[2]+ Beendet ./server3<br />
$ ps -l<br />
.................. [nun keine Zombies mehr vorhanden]<br />
$<br />
Dieses Problem der Zombies lässt sich beheben, indem man das Signal SIGCHLD abfängt,<br />
das bei Beendigung jedes Kindprozesses geschickt wird. Dazu müssen wir die folgenden<br />
Codezeilen im Programm 20.9 (server3.c) einfügen:<br />
4 void sig_kind(int signr); /* Signalhandler zum Abfangen des SIGCHLD-Signals */<br />
...<br />
40 my_signal(SIGCHLD, sig_kind);<br />
...<br />
74 void sig_kind(int signr) {<br />
75 pid_t pid;<br />
76 int status;<br />
77<br />
78 while ( (pid = waitpid(-1, &status, WNOHANG)) > 0) /* Zombies verhindern */<br />
79 fprintf(stderr, ".... Kind %d hat sich beendet\n", pid);<br />
80 return;<br />
81 }<br />
Die Funktion my_signal ist in netzprog.c wie folgt realisiert:<br />
sigfunc *my_signal(int signr, sigfunc *func) {<br />
struct sigaction act, oldact;<br />
act.sa_handler = func;<br />
sigemptyset(&act.sa_mask);<br />
act.sa_flags = 0;
698 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
#ifdef SA_RESTART<br />
if (signr == SIGALRM)<br />
act.sa_flags |= SA_RESTART; /* unterbrochenen Systemaufruf<br />
automatisch wieder starten */<br />
#endif<br />
if (sigaction(signr, &act, &oldact) < 0)<br />
fehler_meld(FATAL_SYS, "sigaction-Fehler");<br />
}<br />
return oldact.sa_handler;<br />
Starten wir jetzt unseren Server wieder im Hintergrund und dann entsprechend Clients,<br />
erleben wir wieder eine Überraschung:<br />
$ ./server3 & [Server im Hintergrund starten]<br />
... Akzeptiere Verbindungen auf Port 9876<br />
[2] 7111<br />
$ ./client2 127.0.0.1<br />
... Verbindung zu Server 127.0.0.1 (Port 9876) aufgebaut<br />
...Client 127.0.0.1 akzeptiert; Glückszahl: 23<br />
.... Kind 7115 hat sich beendet<br />
accept-Fehler: Interrupted system call<br />
[2]- Exit 1 server3 [Server beendet sich]<br />
$<br />
Nun werden zwar keine Zombies mehr erzeugt, dafür bricht aber der Server unmittelbar<br />
nach der ersten Clientanforderung ab. Dieser Abbruch resultiert aus der Tatsache, dass<br />
beim Eintreffen des Signals SIGCHLD der accept-Aufruf für die Ausführung der Signalhandlers<br />
sig_kind unterbrochen wird, so dass diese Funktion als Rückgabewert -1 liefert<br />
und errno in diesem Fall auf EINTR gesetzt ist. Da dieser Spezialfall nicht abgefangen<br />
wird, wird fehler_meld aufgerufen und der Serverprozess beendet.<br />
Um diesen Abbruch zu vermeiden, müssen die Zeilen 49 bis 51 wie folgt ergänzt werden:<br />
47 if ( (clnt_sock = accept(serv_sock, (struct sockaddr *)&adr_clnt,<br />
48 &len_inet)) == -1) {<br />
49 if (errno == EINTR) /*... Wenn unterbrochen --> accept neu aufrufen */<br />
50 continue;<br />
51 else<br />
52 fehler_meld(FATAL_SYS, "accept-Fehler");<br />
53 }<br />
<strong>20.3</strong>.8 Besonderheiten beim Lesen und Schreiben in<br />
Stream-<strong>Socket</strong>s<br />
Beim Lesen bzw. Schreiben in Stream-<strong>Socket</strong>s (wie z.B. <strong>TCP</strong>-<strong>Socket</strong>s) gilt es einige Besonderheiten<br />
zu beachten, die bei normalen Dateien nicht gelten. Ein read bzw. write auf ein<br />
Stream-<strong>Socket</strong> kann weniger Bytes lesen bzw. schreiben als erwartet, was nicht zwangsläufig<br />
ein Fehler ist, da hier eventuell nur eine interne Puffergrenze überschritten wurde.<br />
Mit erneuten Aufrufen von read bzw. write können in solchen Fällen die noch verbliebe-
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 699<br />
nen Bytes gelesen bzw. geschrieben werden. Während ein solches unvollständiges Lesen<br />
jederzeit vorkommen kann, kann ein unvollständiges Schreiben nur bei nicht-blockierenden<br />
<strong>Socket</strong>s auftreten. Deshalb verwenden wird die folgenden eigenen <strong>Funktionen</strong> zum<br />
Schreiben in bzw. zum Lesen aus <strong>Socket</strong>s, die wieder in der Programmdatei netzprog.c<br />
aufgenommen werden.<br />
ssize_t write_sock(int fd, const void *puffer, size_t n) {<br />
<br />
size_t verbleiben = n;<br />
ssize_t geschrieben;<br />
const char *zgr = puffer;<br />
while (verbleiben > 0) { /* schreibt n Bytes nach fd */<br />
if ( (geschrieben = write(fd, zgr, verbleiben)) 0) { /* liest n Bytes von fd */<br />
if ( (gelesen = read(fd, zgr, verbleiben)) < 0) {<br />
if (errno == EINTR)<br />
gelesen = 0; /* neuer read-Versuch */<br />
else<br />
fehler_meld(FATAL_SYS, "read_sock-Fehler");<br />
} else if (gelesen == 0)<br />
break; /* EOF */<br />
verbleiben -= gelesen;<br />
zgr += gelesen;<br />
}<br />
return n-verbleiben;<br />
}<br />
static ssize_t lies_ein_puffer_zeichen(int fd, char *zgr) {<br />
static int noch_im_puffer = 0;<br />
static char lese_puffer[MAX_ZEICHEN];<br />
static char *lese_zgr;<br />
if (noch_im_puffer
700 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
if (noch_im_puffer == 0)<br />
return 0;<br />
lese_zgr = lese_puffer;<br />
}<br />
noch_im_puffer--;<br />
*zgr = *lese_zgr++;<br />
return 1;<br />
}<br />
ssize_t readline_sock(int fd, void *puffer, size_t maxlen) {<br />
int i, n;<br />
char zeich, *zgr = puffer;<br />
}<br />
for (i=1; i < (int)maxlen; i++) {<br />
if ( (n = lies_ein_puffer_zeichen(fd, &zeich)) == 1) {<br />
*zgr++ = zeich;<br />
if (zeich == '\n')<br />
break; /* Zeilenende */<br />
} else if (n == 0) {<br />
if (i == 1)<br />
return 0; /* EOF gleich am Anfang (keine Daten) */<br />
else<br />
break; /* EOF; es wurden Daten gelesen */<br />
} else<br />
fehler_meld(FATAL_SYS, "readline-Fehler");<br />
}<br />
*zgr = '\0'; /* 0 terminieren */<br />
return i;<br />
<strong>20.3</strong>.9 Verwendung von Standard E/A-<strong>Funktionen</strong> für <strong>Socket</strong>s<br />
Öffnen eines <strong>Socket</strong>deskriptors mit fdopen zum Lesen und Schreiben (Vorsicht)<br />
Neben den elementaren E/A-<strong>Funktionen</strong> wie read, write usw. können auch Standard<br />
E/A-<strong>Funktionen</strong> wie fprintf, fscanf usw. verwendet werden, um auf <strong>Socket</strong>s zuzugreifen.<br />
Dazu muss man sich mit der Funktion fdopen nur einen FILE-Zeiger zu einem <strong>Socket</strong>deskriptor<br />
zur Verfügung stellen lassen. Naheliegend wäre hier eine Implementierung<br />
wie die folgende, die jedoch falsch ist, wenn man in das <strong>Socket</strong> sowohl schreibt als auch<br />
aus ihm liest:<br />
int sockfd; /* <strong>Socket</strong>deskriptor */<br />
FILE *fz; /* FILE-Zeiger */<br />
sockfd = socket(PF_INET, SOCK_STREAM, 0);<br />
fz = fdopen(sockfd, "r+"); /* r+ = Lesen und Schreiben auf <strong>Socket</strong> (falsch) */<br />
if (fz == NULL)<br />
fehler_meld(FATAL_SYS, "fdopen-Fehler");<br />
Bei dieser nahe liegenden Implementierung treten jedoch Schwierigkeiten auf, denn auf<br />
eine Ausgabeoperation kann hier nur dann eine Eingabeoperation folgen, wenn dazwi-
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 701<br />
schen fflush, fseek, fsetpos oder rewind aufgerufen wird. Umgekehrt kann auf eine Eingabeoperation<br />
nur dann eine Ausgabeoperation folgen, wenn dazwischen fseek, fsetpos<br />
oder rewind aufgerufen wird, es sei denn, die Eingabeoperation hat EOF gelesen. Das<br />
Problem hier ist nun, dass die <strong>Funktionen</strong> fseek, fsetpos oder rewind ihrerseits lseek aufrufen,<br />
und dieser Aufruf schlägt bei <strong>Socket</strong>s fehl.<br />
Deswegen ist von dieser Implementierung abzuraten, und statt dessen die nachfolgend<br />
vorgestellte Methode zu verwenden.<br />
Getrennte FILE-Zeiger zum Lesen und Schreiben<br />
Statt eines FILE-Zeigers, der wie zuvor gleichzeitig Lesen und Schreiben ermöglicht, empfiehlt<br />
es sich, zwei unterschiedliche FILE-Zeiger zu verwenden: Einen zum Lesen und<br />
einem zum Schreiben aus dem <strong>Socket</strong>, wie z.B.:<br />
int sockfd; /* <strong>Socket</strong>deskriptor */<br />
FILE *lese_fz; /* FILE-Zeiger zum Lesen */<br />
FILE *schreib_fz; /* FILE-Zeiger zum Schreiben */<br />
sockfd = socket(PF_INET, SOCK_STREAM, 0);<br />
lese_fz = fdopen(sockfd, "r"); /* FILE-Zeiger zum Lesen aus <strong>Socket</strong> */<br />
if (lese_fz == NULL)<br />
fehler_meld(FATAL_SYS, "fdopen-Fehler (Lesen)");<br />
schreib_fz = fdopen(dup(sockfd), "w"); /* FILE-Zeiger zum Schreiben auf <strong>Socket</strong> */<br />
if (schreib_fz == NULL)<br />
fehler_meld(FATAL_SYS, "fdopen-Fehler (Schreiben)");<br />
Das Duplizieren des <strong>Socket</strong>deskriptors bei schreib_fz ist notwendig, da unterschiedliche<br />
Streams auch unterschiedliche Filedeskriptoren verwenden sollten. So ist es möglich mit<br />
fclose nur die Lese- bzw. Schreibhälfte des <strong>Socket</strong>s zu schließen. Ohne Duplizierung würden<br />
bei einem fclose beide geschlossen.<br />
Es gibt nun mehrere Möglichkeiten, das <strong>Socket</strong> zu schließen:<br />
<br />
Alleiniges Schließen der Schreibhälfte<br />
fflush(schreib_dz); /* Übertragen der noch im Scheibpuffer stehenden Daten */<br />
shutdown(fileno(schreib_dz), SHUT_WR); /* Schließen der Schreibhälfte */<br />
fclose(schreib_dz); /* FILE-Schreibzeiger schließen */<br />
<br />
Alleiniges Schließen der Lesehälfte<br />
shutdown(fileno(lese_dz), SHUT_RD); /* Schließen der Lesehälfte */<br />
fclose(lese_dz); /* FILE-Lesezeiger schließen */<br />
<br />
Gleichzeitiges Schließen der Schreib- und Lesehälfte<br />
fclose(schreib_dz); /* FILE-Schreibzeiger schließen */<br />
shutdown(fileno(lese_dz), SHUT_RDWR); /* Schließen der Lese- und Schreibhälfte */<br />
fclose(lese_dz); /* FILE-Lesezeiger schließen */
702 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
Der shutdown-Aufruf ist hierbei nur notwendig, wenn man mit fork Kindprozesse kreiert<br />
hat und sicherstellen will, dass eventuell in einem anderen Prozess offene <strong>Socket</strong>deskriptoren<br />
auch geschlossen werden.<br />
Abfangen der Fehlernummer EINTR<br />
Bereits in Kapitel <strong>20.3</strong>.7 (beim accept-Aufruf) wurde darauf hingewiesen, dass gewisse<br />
Systemaufrufe beim Auftreten eines Signals unterbrochen werden, damit die Signalhandler-Funktion<br />
ausgeführt werden kann. In diesem Fall liefert die entsprechende Funktion<br />
einen Fehler als Rückgabewert und setzt errno auf die Fehlernummer EINTR. Dies muss<br />
abgefangen werden, und die entsprechende Funktion dann erneut aufgerufen werden,<br />
wie z.B.:<br />
do {<br />
clearerr(lese_dz); /* Fehler-Flag löschen */<br />
zeich = fgetc(lese_dz);<br />
} while (ferror(lese_dz) && errno == EINTR); /* getc bei EINTR neu starten */<br />
if (ferror(lese_dz)<br />
fehler_meld(FATAL_SYS, "fgetc-Fehler");<br />
Typische <strong>Funktionen</strong>, die durch Signale unterbrochen werden können und bei denen<br />
man EINTR abfangen sollte, sind:<br />
connect accept read write readv<br />
writev recvfrom sendto select poll<br />
Pufferung bei den Standard E/A-<strong>Funktionen</strong><br />
In Kapitel 3.5 wurde die intern stattfindende Pufferung bei Standard E/A-<strong>Funktionen</strong><br />
beschrieben. Wenn z.B. Voll- bzw. Zeilenpufferung eingestellt ist, so werden die entsprechenden<br />
Zeichen erst wirklich in das <strong>Socket</strong> geschrieben, wenn der Puffer voll ist bzw.<br />
ein Neuezeilezeichen vorkommt. Dies muss man beachten, denn sonst kann dies zu Problemen<br />
führen, dass die gesendeten Daten im Puffer bleiben und der Leseversuch auf der<br />
anderen Seite für immer blockiert ist, da er vergeblich auf ankommende Daten wartet. In<br />
solchen Fällen muss man die Daten aus den entsprechenden Puffer entweder mit fflush<br />
explizit in das <strong>Socket</strong> schreiben lassen oder aber mit setbuf bzw. setvbuf eine andere Pufferung<br />
für den entsprechenden Stream (FILE-Zeiger) einstellen.<br />
Beispiel zur Verwendung der Standard E/A-<strong>Funktionen</strong> bei <strong>Socket</strong>s<br />
Hier realisieren wir zunächst eine eigene Funktion mkadr, die die als String übergebene<br />
Host-Adresse adr_str (Domainame oder IP-Adresse) und das ebenfalls als String übergebene<br />
protocol in eine entsprechende Adressstruktur umwandelt, die es über den Parameter<br />
sa zurückliefert. Die Länge dieser Adressstruktur liefert mkadr über den Parameter<br />
salen. Diese Funktion mkadr wird wieder in die Programmdatei netzprog.c aufgenommen:
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 703<br />
int mkadr(char *adr_str, char *protocol, void *sa, socklen_t *salen) {<br />
char *tmp_addr = strdup(adr_str); /* String 'adr_str' duplizieren */<br />
char *host = strtok(tmp_addr, ":");<br />
char *port = strtok(NULL, "\n");<br />
struct sockaddr_in *sin = (struct sockaddr_in *)sa;<br />
struct hostent *hname = NULL;<br />
struct servent *sname = NULL;<br />
char *rest;<br />
long portnr;<br />
/*... Voreinstellungen evtl. setzen */<br />
if (host == NULL) host = "*";<br />
if (port == NULL) port = "*";<br />
if (protocol == NULL) protocol = "udp";<br />
/*... Initialisieren der Adressstrukturen */<br />
memset(sin, 0, *salen);<br />
sin->sin_family = AF_INET;<br />
sin->sin_port = 0;<br />
sin->sin_addr.s_addr = htons(INADDR_ANY);<br />
/*... Host-Adresse füllen */<br />
if (isdigit(*host)) { /*... Numerische Adresse */<br />
if (inet_pton(AF_INET, host, &sin->sin_addr) h_addrtype != AF_INET)<br />
return -1;<br />
sin->sin_addr = *(struct in_addr *)hname->h_addr_list[0];<br />
<br />
/*... Portnummer festlegen */<br />
if (isdigit(*port)) { /*... Numerische Portnummer */<br />
portnr = strtol(port, &rest, 10);<br />
if ( (rest != NULL && *rest) || (portnr < 0L || portnr >= 32768))<br />
return -2;<br />
sin->sin_port = htons((short)portnr);<br />
} else if (strcmp(port,"*")) { /*... Symbolische Portnr. (nicht '*') */<br />
if ( (sname = getservbyname(port, protocol)) == NULL)<br />
return -2;<br />
sin->sin_port = (short)sname->s_port;<br />
}<br />
*salen = sizeof(*sin);<br />
}<br />
free(tmp_addr);<br />
return 0;<br />
Für adr_str kann Folgendes angegeben werden:<br />
hostname:service
704 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
wobei für hostname Folgendes erlaubt ist:<br />
IP-Nummer, wie z.B. 127.0.0.1<br />
<br />
<br />
Domainname, wie z.B. www.fh-nuernberg.de<br />
ein * für wildcard (INADDR_ANY)<br />
Die Angabe des Doppelpunktes und des service ist optional. Ist service angegeben, muss<br />
auch der Doppelpunkt angegeben werden. Für service kann Folgendes angegeben werden:<br />
Portnummer, wie z.B. 8080<br />
Ein Service-Name, wie z.B. telnet<br />
ein * für die Portnummer 0; in diesem Fall wird vom bind-Aufruf eine entsprechende<br />
Portnummer zugeteilt.<br />
Beispiele möglicher Angaben für hostname:service sind:<br />
www.fh-nuernberg.de:8080<br />
127.0.0.1:telnet<br />
ftp.suse.de:ftp<br />
Unter Verwendung dieser Funktion mkadr realisiert das Programm 20.10 (server4.c)<br />
einen Server, dem man Zahlen bis 9999 in Textschreibweise schicken kann, und der dann<br />
die entsprechende Zahl dazu in numerischer Form ermittelt und zurückschickt.<br />
#include <br />
#include "netzprog.h"<br />
static char *bindeWort[] = { "und", "hundert" };<br />
static char *bisZwanzig[] = {<br />
"ein", "eins", "zwei", "drei", "vier",<br />
"fünf", "sechs", "sieben", "acht", "neun",<br />
"zehn", "elf", "zwölf", "dreizehn", "vierzehn",<br />
"fünfzehn", "sechzehn", "siebzehn", "achtzehn", "neunzehn" };<br />
static char *bis100[] = { "zwanzig", "dreissig", "vierzig", "fünfzig",<br />
"sechzig", "siebzig", "achtzig", "neunzig" };<br />
static void server_antwort(char *zeile, FILE *dz);<br />
static int suche(int nr, char *string);<br />
int<br />
main(int argc, char *argv[])<br />
{<br />
char<br />
*serv_adr="127.0.0.1:8765";<br />
struct sockaddr_in adr_serv, adr_clnt; /* Server- und Clientadressstr. */<br />
int<br />
len_inet;<br />
int serv_sock, clnt_sock; /* Server- und Clientsocket */<br />
FILE *lese_dz, *schreib_dz; /* Lese-/Schreib FILE-Zeiger */
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 705<br />
char puffer[MAX_ZEICHEN]; /* E/A-Puffer */<br />
if (argc >= 2)<br />
serv_adr = argv[1];<br />
len_inet = sizeof(adr_serv);<br />
if (mkadr(serv_adr, "tcp", &adr_serv, &len_inet) < 0 ||<br />
adr_serv.sin_port == 0)<br />
fehler_meld(FATAL, "falsche Adresse oder Portnummer");<br />
/*... Verbindungs-<strong>Socket</strong> erzeugen */<br />
serv_sock = my_socket(PF_INET, SOCK_STREAM, 0);<br />
/*... <strong>Socket</strong> eine Protokolladresse zuordnen */<br />
my_bind(serv_sock, (struct sockaddr *)&adr_serv, len_inet);<br />
/*... <strong>Socket</strong> in passiven Zustand für Verbindungsannahmen versetzen */<br />
my_listen(serv_sock, 10);<br />
while (1) {<br />
/*... Verbindung von Client akzeptieren */<br />
len_inet = sizeof(adr_clnt); /* Auf connect seitens Clients warten */<br />
clnt_sock = my_accept(serv_sock, (struct sockaddr *)&adr_clnt, &len_inet);<br />
/*... FILE-Zeiger zum verbundenen Clientsocket anlegen */<br />
if ( (lese_dz = fdopen(clnt_sock, "r")) == NULL ||<br />
(schreib_dz = fdopen(clnt_sock, "w")) == NULL)<br />
fehler_meld(FATAL_SYS, "fdopen-Fehler");<br />
/*... Zeilenpufferung für lese_dz und schreib_dz einstellen */<br />
setvbuf(lese_dz, puffer, _IOLBF, sizeof(puffer));<br />
setvbuf(schreib_dz, puffer, _IOLBF, sizeof(puffer));<br />
while (fgets(puffer, sizeof(puffer), lese_dz) != NULL)<br />
server_antwort(puffer, schreib_dz);<br />
fclose(schreib_dz); /* Schreibzeiger schließen */<br />
fclose(lese_dz); /* Lesezeiger schließen */<br />
}<br />
exit(0); /* wird aufgrund der Endlosschleife nie ausgeführt */<br />
}<br />
static void server_antwort(char *zeile, FILE *dz) {<br />
int ergeb=0, i=1;<br />
char *wort = strtok(zeile, " \r\t\n");<br />
while (wort != NULL) {<br />
if (i % 2 == 0)<br />
ergeb *= suche(i, wort);<br />
else<br />
ergeb += suche(i, wort);<br />
i++;<br />
wort = strtok(NULL, " \r\t\n");<br />
}<br />
fprintf(dz, " ===> %d\n", ergeb);
706 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
fflush(dz);<br />
}<br />
static int suche(int nr, char *string) {<br />
int i;<br />
if (nr%2==0) {<br />
for (i=0; i 92<br />
zwölf<br />
===> 12<br />
drei hundert sieben und ein und zwanzig<br />
===> 328<br />
drei hundert ein und zwanzig<br />
===> 321
<strong>20.3</strong> <strong>Elementare</strong> <strong>TCP</strong>-<strong>Socket</strong>-<strong>Funktionen</strong> 707<br />
^] [Eingabemodus beenden]<br />
telnet> c [telnet beenden]<br />
Connection closed.<br />
$ kill %1 [Server beenden]<br />
[1] Beendet server4<br />
$<br />
<strong>20.3</strong>.10 Übung<br />
Paralleler Server, der Zeit in einem vom Client angeforderten Format schickt<br />
Erstellen Sie ein paralleles Serverprogramm (tageszeit.c) und ein zugehöriges Clientprogramm<br />
(client.c). Das Clientprogramm soll dabei dem Serverprogramm einen entsprechenden<br />
Zeitformat-String (man strftime) schicken, den das Serverprogramm mit der<br />
aktuellen Zeit entsprechend dem geschickten Zeitformat-String füllt, und dem Client<br />
zurückschickt, der diese empfangene Zeit dann nur noch ausgibt. Zusätzlich soll das<br />
Clientprogramm noch die Mikrosekunden der aktuellen Zeit mit gettimeofday ermitteln<br />
und ausgeben.<br />
Nachdem man den Server im Hintergrund gestartet hat mit<br />
$ ./tageszeit &<br />
....<br />
$<br />
könnte man das folgende bash-Skript verwenden, das 100 Clientanforderungen an den<br />
Server schickt:<br />
i=1<br />
while [ "$i" -le 100 ]<br />
do<br />
./client 127.0.0.1 9876 &<br />
i=$(expr $i + 1)<br />
done<br />
so dass sich z.B. die folgende Ausgabe ergibt:<br />
$ ./clients<br />
064.Tag des Jahres; 12:50:09 (0.408)<br />
Thursday; 04.03.04; 12:50:09 (0.420)<br />
Thursday; 04.03.04; 12:50:09 (0.432)<br />
Thursday; 04.03.04; 12:50:09 (0.443)<br />
064.Tag des Jahres; 12:50:09 (0.468)<br />
Thursday; 04.03.04; 12:50:09 (0.480)<br />
064.Tag des Jahres; 12:50:09 (0.492)<br />
......<br />
Thursday; 04.03.04; 12:50:10 (0.902)<br />
Thursday; 04.03.04; 12:50:10 (0.922)<br />
064.Tag des Jahres; 12:50:10 (0.942)<br />
064.Tag des Jahres; 12:50:10 (0.963)<br />
Thursday; 04.03.04; 12:50:10 (0.983)<br />
$
708 20 Netzwerkprogrammierung mit <strong>Socket</strong>s<br />
Kopieren von Dateien über <strong>TCP</strong>-<strong>Socket</strong>s<br />
Erstellen Sie ein Serverprogramm copysrv.c, das an einem <strong>Socket</strong> auf das Ankommen<br />
von Daten wartet, diese liest und in eine Datei datei_1 schreibt. Wird das <strong>Socket</strong> seitens<br />
des Clientprogramms copycli.c, das die Daten schickt, geschlossen, wartet der Server auf<br />
eine neue Verbindungsanforderung seitens des Clients. Die dann hier geschickten Daten<br />
schreibt das Serverprogramm in eine Datei datei_2 usw.<br />
Beim Serverprogramm muss man als Argument die IP-Adresse und Portnummer angeben,<br />
was auch für das Clientprogramm zutrifft, dem man aber zusätzlich als weitere<br />
Argumente die Namen der zu kopierenden Dateien auf der Kommandozeile mitteilen<br />
muss.<br />
Nachdem man diese beiden Programme kompiliert und gelinkt hat:<br />
cc -o copysrv copysrv.c netzprog.c fehler.c<br />
cc -o copycli copycli.c netzprog.c fehler.c<br />
kann man sie testen, wie es z.B. nachfolgend gezeigt ist.<br />
Ablaufbeispiel am lokalen Rechner<br />
Zunächst startet man das Serverprogramm in einem Terminalfenster:<br />
$ ./copysrv 127.0.0.1:8765<br />
In einem anderen Terminalfenster ruft man dann das Clientprogramm auf:<br />
$ ./copycli 127.0.0.1:8765 iban.txt route.txt test.txt<br />
...Datei 'iban.txt' (51 Bytes) geschickt<br />
...Datei 'route.txt' (256 Bytes) geschickt<br />
...Datei 'test.txt' (44235082 Bytes) geschickt<br />
$<br />
Im Terminalfenster des Servers erscheinen nun folgende Meldungen:<br />
...Datei 'datei_1' (51 Bytes) von '127.0.0.1'<br />
...Datei 'datei_2' (256 Bytes) von '127.0.0.1'<br />
...Datei 'datei_3' (44235082 Bytes) von '127.0.0.1'<br />
Startet man das Clientprogramm wiederum in einem anderen Terminalfenster, wie z.B.:<br />
$ ./copycli 127.0.0.1:8765 ping.txt s819.txt<br />
...Datei 'ping.txt' (769 Bytes) geschickt<br />
...Datei 's819.txt' (1121 Bytes) geschickt<br />
$<br />
erscheinen im Terminalfenster des Servers folgende Meldungen:<br />
...Datei 'datei_4' (769 Bytes) von '127.0.0.1'<br />
...Datei 'datei_5' (1121 Bytes) von '127.0.0.1'<br />
Strg-C<br />
$
20.4 <strong>Elementare</strong> UDP-<strong>Socket</strong>-<strong>Funktionen</strong> 709<br />
Ablaufbeispiel, wo Dateien von einem entfernten Rechner geschickt werden<br />
Zunächst startet man das Serverprogramm am lokalen Rechner:<br />
$ ./copysrv 192.168.69.6:7777<br />
Nun startet man das Clientprogramm an einem entfernten Rechner, wie z.B.:<br />
Rechner birne $ ./copycli 192.168.69.6:7777 abakus.pdf sortalgo.pdf<br />
...Datei 'abakus.pdf' (242394 Bytes) geschickt<br />
...Datei 'sortalgo.pdf' (152177 Bytes) geschickt<br />
Rechner birne $<br />
Es erscheinen dann im Terminalfenster des Servers am lokalen Rechner folgende Meldungen:<br />
...Datei 'datei_1' (242394 Bytes) von '192.168.69.5'<br />
...Datei 'datei_2' (152177 Bytes) von '192.168.69.5'<br />
Strg-C<br />
$<br />
20.4 <strong>Elementare</strong> UDP-<strong>Socket</strong>-<strong>Funktionen</strong><br />
Während es sich bei <strong>TCP</strong> um ein verbindungsorientiertes, zuverlässiges Stream-Protokoll handelt,<br />
ist UDP ein verbindungsloses, unzuverlässiges Datagram-Protokoll. Hier werden die elementaren<br />
UDP-<strong>Socket</strong>-<strong>Funktionen</strong> beschrieben, die für eine UDP-Client/Server-Realisierung<br />
benötigt werden.<br />
20.4.1 Überblick<br />
Abbildung 20.6 zeigt die elementaren <strong>Socket</strong>-<strong>Funktionen</strong>, die bei einer typischen UDP-<br />
Client/Server-Realisierung benötigt werden. Anders als bei <strong>TCP</strong> baut der Client bei UDP<br />
keine Verbindung mit dem Server auf. Statt dessen sendet der Client mit sendto nur ein<br />
Datagramm an den Server, das die Adresse des Ziels, nämlich des Servers, enthält.<br />
Ebenso baut auch der Server keine Verbindung zum Client auf, sondern ruft statt dessen<br />
die Funktion recvfrom auf, die wartet, bis Daten (Datagramme) von irgendwelchen<br />
Clients eintreffen. recvfrom liefert dabei die Protokolladresse des Clients zusammen mit<br />
dem Datagramm zurück, so dass der Server den Absender kennt und seine Antwort an<br />
den richtigen Client zurückschicken kann.<br />
Bei einer verbindungslosen Kommunikation wird der Server meist iterativ implementiert:<br />
Er empfängt eine Anforderung nach der anderen und schickt die Antworten an die<br />
entsprechenden Clients zurück. Hierbei ist es nicht wichtig, ob mehrere Anfragen vom<br />
gleichen Client oder aber von unterschiedlichen Clients stammen. Aber auch beim UDP-<br />
Protokoll gilt, dass ein iterativer Server nur geeignet ist, wenn der Server nicht zu lange<br />
für die Bearbeitung einer Anforderung benötigt.