24.12.2013 Aufrufe

20.3 Elementare TCP-Socket-Funktionen - Pearson Bookshop

20.3 Elementare TCP-Socket-Funktionen - Pearson Bookshop

20.3 Elementare TCP-Socket-Funktionen - Pearson Bookshop

MEHR ANZEIGEN
WENIGER ANZEIGEN

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.

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!