11.17 UDP
Neben dem bisher kennen gelernten TCP bieten viele UNIX-Systeme und -Netze mit UDP noch ein weiteres Transportprotokoll an. Da dieses Transportprotokoll einen verbindungslosen und unzuverlässigen Übertragungsdienst anbietet, wird es in der Praxis auch wesentlich seltener eingesetzt. Wenn mit UDP z. B. ein Anwender einem anderen Anwender eine Nachricht sendet, so ist nicht sichergestellt, dass diese Nachricht je ihr Ziel erreicht. Des Weiteren ist auch nicht, wie bei TCP (wo die Pakete ggf. sortiert werden), garantiert, in welcher Reihenfolge die einzelnen Pakete am Ziel angekommen. Allerdings bewirkt diese Unzuverlässigkeit, dass mit der Übertragung mit UDP ein höherer Datendurchsatz erreicht wird, als dies bei TCP der Fall ist. Dies ist z. B. bei Anwendungen wie Internet-Spielen (Spieleservern) recht interessant. Dabei ist es nicht so schlimm, wenn ein UDP-Paket unterwegs verloren geht, weil das nächste Paket ja eh wieder die richtige Position enthält. Sofern der Client nicht zu sehr laggt, fällt den anderen Spielern das relativ selten auf. Oder ein noch populäreres Beispiel für UDP wäre NFS, das bei Linux (< 2.6) per Default beim Mounten auf UDP setzt (mehr dazu finden Sie in der Manual Page zu nfs).
Warum UDP schneller als TCP bei der Datenübertragung ist, will ich Ihnen auch hier nicht vorenthalten. TCP ist deshalb so zuverlässig, weil dabei einige Protokollmechanismen wie die Behandlung verloren gegangener Pakete (Timeout- und Retransmissions-Mechanismus) oder Fehlerbehandlungsfälle wie doppelt oder in der falschen Reihenfolge eintreffende Pakete implementiert sind. Damit TCP auf die eben genannten Fälle reagieren kann, wird natürlich auch Speicherplatz dafür benötigt, damit die gesendeten Pakete so lange verfügbar sind, bis diese bestätigt werden. Wenn dabei mehrere Pakete unterwegs sind, wächst auch der Speicherbedarf enorm an. Neben dem Speicherbedarf wird auch die CPU benötigt, womit u. a. die gesendeten Pakete auf Ihre Korrektheit (richtige Sequenznummer, doppelt vorhanden ...) überprüft werden müssen. Das alles benötigt Zeit und Speicher, den UDP nicht benötigt, da keiner dieser eben genannten Mechanismen verwendet wird. Bei UDP wird ein eintreffendes Paket von dem IP-Protokoll direkt ohne Umwege an die Anwendung weitergegeben.
Ein weiterer Vorteil von UDP ist die Nachrichtenorientierung. Im Gegensatz zu TCP (stream-orientiert) werden nicht erst mehrere Aufrufe von Socket-Optionen benötigt, um von allen Partnern eingehende Daten auszulesen. Das Paket wird komplett auf einmal übertragen, was bedeutet, dass Sie das Paket mit einem Leseaufruf lesen können. Trotzdem hat auch ein UDP-Paket eine maximale Größe, spätestens ab der Hardware (Ethernet: 1500 Bytes).
Trotz der Vorteile sollten Sie immer den Hauptnachteil im Augen behalten. UDP eignet sich nicht für eine sichere Datenübertragung!
Der Aufbau der Anwendungen mit dem UDP-Übertragungsprotokoll unterscheidet sich nur geringfügig von dem TCP-Übertragungsprotokoll. Im Prinzip können dieselben Funktionen der Socket-Bibliothek verwendet werden, die Sie bereits mit TCP verwendet haben. Der Begriff verbindungslos bringt einige Anfänger immer ins Schleudern. Letztendlich bedeutet dies lediglich, dass hier keine Verbindung mit connect() (meistens von der Clientanwendung ausgehend) angefordert wird, sondern dass die Daten gleich zum entsprechenden Rechner geschickt werden. Auf der Gegenseite ist dabei natürlich auch kein accept()-Aufruf nötig. Das entspricht der Erst-schließen-dann-fragen-Strategie.
In der Abbildung 9.11 sehen Sie den typischen Ablauf bei einer UDP-Übertragung. Die Clientanwendung baut mit dem Server keine Verbindung auf und drischt gleich mit sendto() Datagramme an den Server. Der Server akzeptiert hingegen auch keine Verbindung zum Client und ruft lediglich die Funktion recvfrom() auf und wartet darauf, dass irgendein Client Daten schickt.
Der Server hingegen kann dem Client dann ebenfalls mit sendto()antworten, da der Client mit sendto() auch seine Protokolladresse mitgeschickt hat. Der Client wiederum kann die Daten vom Server mit recvfrom() empfangen.
11.17.1 Clientanwendung
Wie schon beim TCP sollte der Client den Namen des Hosts, mit dem er sich unterhält, und den Dienst, der dort läuft, wissen. Für diese Informationen kann wieder die Funktion gethostbyname() und getservbyname() verwendet werden.
Auch hier gilt, wie schon bei TCP (Client: >1024 und Server <1025), dass die Freigabe von Portnummern vom Admin abhängig ist. In der Zeit der Viren und Würmer werden Dienste gerne auf andere Ports gelegt, um die Firewalls auf den Standardports dichtzumachen.
Anstelle des Dienstnamens kann natürlich auch die Portnummer verwendet werden. Beachten Sie bitte, dass bei getservbyname() der Dienst UDP angegeben werden muss. Wenn alle Informationen des Gegenübers verfügbar sind, kann ein neues Socket geöffnet werden. Des Weiteren müssen Sie, im Gegensatz zu TCP, für den zweiten Parameter die symbolische Konstante SOCK_DGRAM anstatt SOCK_STREAM angeben, um über dieses Socket Datagramme austauschen zu können. Es hindert Sie übrigens niemand daran, auch bei der Clientanwendung connect() zu verwenden. Allerdings führt dies lediglich dazu (wie es übrigens bei TCP auch der Fall ist), dass die Adressinformationen gespeichert werden. Anders als bei TCP, wo nach der Speicherung der Adressinformationen überprüft wird, ob der Server überhaupt existiert, kehrt connect() bei UDP aber sofort wieder zurück. Jetzt können die Daten an den Server gesendet werden.
Über connect() kann dann auch send() statt sendto() benutzt werden. Ein weiterer Vorteil von connect() ist, dass man zur Laufzeit entscheiden kann, ob SOCK_STREAM oder SOCK_DGRAM verwendet werden soll.
Bei UDP wird aber auch überprüft, ob die Serveranwendung in irgendeiner Form horcht (listen()). Denn schickt man UDP-Pakete an ein nicht existentes Socket (= geschlossenen Port), so gibt das OS eine ICMP-Fehlermeldung zurück.
11.17.2 Serveranwendung
Der Aufbau der Serveranwendung ist bei UDP dem von TCP recht ähnlich. Wie beim TCP werden getservbyname(), socket() und bind() verwendet. Bei socket() muss natürlich auch hier der Typ SOCK_DGRAM angegeben werden. Was hier wegfällt, sind die Aufrufe von listen(), connect() und accept(), da ja hiermit eine Verbindung aufgebaut wird, und UDP ist ja ein verbindungsloses Protokoll (kann man nicht oft genug wiederholen). Jetzt kann der Server mit recvfrom() auf Nachrichten eines beliebigen Clients vom Socket warten.
11.17.3 recvfrom() und sendto()
In diesem Abschnitt sollen jetzt die Funktionen recvfrom() und sendto() näher beschrieben werden. Beide Funktionen sind den Funktionen write()/send() und read()/recv() recht ähnlich – allerdings mit zusätzlichen Argumenten.
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom( int s, void *buf, size_t len,
int flags, struct sockaddr *from,
socklen_t *fromlen);
ssize_t sendto(int s, const void *msg, size_t len,
int flags, const struct sockaddr *to,
socklen_t tolen);
Die ersten drei Parameter entsprechen jeweils denen der Funktionen read()/recv() und write()/send(). Hierbei kommen jedoch drei weitere Parameter hinzu. Den vierten Parameter flag werden Sie in diesem Kapitel nicht benötigen, weshalb ich Sie für weitere Informationen auf die Manual Page verweisen will. Sie geben hierfür immer 0 an – womit der Parameter keinerlei Effekt hat. Mit dem Parameter to bzw. from muss jedes Mal die Adresse des Verbindungsparameters angegeben werden. Die Parameter to und from entsprechen dem Parameter serv_addr der Funktion connect(). Mit tolen und fromlen müssen Sie die Größe der Struktur sockaddr angeben.
Nun erfolgt ein einfaches Client-Server-Beispiel zum UDP-Übertragungsprotokoll. Der Client schickt an den Server eine Textnachricht und beendet sich, ohne darauf zu warten, ob die Nachricht angekommen ist oder nicht (was ja auch UDP-typisch ist). Der Server hingegen liest stur aus dem von ihm erzeugten Socket und gibt eventuell empfangene Daten mitsamt Uhrzeit auf die Standardausgabe aus.
Zuerst das Listing zum UDP-Server:
/* server.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd. h.>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>
#define LOCAL_SERVER_PORT 1234
#define BUF 255
int main (int argc, char **argv) {
int s, rc, n, len;
struct sockaddr_in cliAddr, servAddr;
char puffer[BUF];
time_t time1;
char loctime[BUF];
char *ptr;
const int y = 1;
/* Socket erzeugen */
s = socket (AF_INET, SOCK_DGRAM, 0);
if (s < 0) {
printf ("%s: Kann Socket nicht öffnen ...(%s)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
/* Lokalen Server Port bind(en) */
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl (INADDR_ANY);
servAddr.sin_port = htons (LOCAL_SERVER_PORT);
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &y, sizeof(int));
rc = bind ( s, (struct sockaddr *) &servAddr,
sizeof (servAddr));
if (rc < 0) {
printf ("%s: Kann Portnummern %d nicht binden (%s)\n",
argv[0], LOCAL_SERVER_PORT, strerror(errno));
exit (EXIT_FAILURE);
}
printf ("%s: Wartet auf Daten am Port (UDP) %u\n",
argv[0], LOCAL_SERVER_PORT);
/* Serverschleife */
while (1) {
/* Puffer initialisieren */
memset (puffer, 0, BUF);
/* Nachrichten empfangen */
len = sizeof (cliAddr);
n = recvfrom ( s, puffer, BUF, 0,
(struct sockaddr *) &cliAddr, &len );
if (n < 0) {
printf ("%s: Kann keine Daten empfangen ...\n",
argv[0] );
continue;
}
/* Zeitangaben präparieren */
time(&time1);
strncpy(loctime, ctime(&time1), BUF);
ptr = strchr(loctime, '\n' );
*ptr = '\0';
/* Erhaltene Nachricht ausgeben */
printf ("%s: Daten erhalten von %s:UDP%u : %s \n",
loctime, inet_ntoa (cliAddr.sin_addr),
ntohs (cliAddr.sin_port), puffer);
}
return EXIT_SUCCESS;
}
Jetzt noch die Clientanwendung:
/* client.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd. h.>
#include <string.h>
#include <stdlib.h>
#include <sys/time.h>
#include <errno.h>
#define SERVER_PORT 1234
int main (int argc, char **argv) {
int s, rc, i;
struct sockaddr_in cliAddr, remoteServAddr;
struct hostent *h;
/* Kommandozeile auswerten */
if (argc < 3) {
printf ("Usage: %s <server> <data1> ... <dataN> \n",
argv[0] );
exit (EXIT_FAILURE);
}
/* IP-Adresse vom Server überprüfen */
h = gethostbyname (argv[1]);
if (h == NULL) {
printf ("%s: unbekannter Host '%s' \n",
argv[0], argv[1] );
exit (EXIT_FAILURE);
}
printf ("%s: sende Daten an '%s' (IP : %s) \n",
argv[0], h->h_name,
inet_ntoa (*(struct in_addr *) h->h_addr_list[0]) );
remoteServAddr.sin_family = h->h_addrtype;
memcpy ( (char *) &remoteServAddr.sin_addr.s_addr,
h->h_addr_list[0], h->h_length);
remoteServAddr.sin_port = htons (SERVER_PORT);
/* Socket erzeugen */
s = socket (AF_INET, SOCK_DGRAM, 0);
if (s < 0) {
printf ("%s: Kann Socket nicht öffnen (%s) \n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
/* Jeden Port bind(en) */
cliAddr.sin_family = AF_INET;
cliAddr.sin_addr.s_addr = htonl (INADDR_ANY);
cliAddr.sin_port = htons (0);
rc = bind ( s, (struct sockaddr *) &cliAddr,
sizeof (cliAddr) );
if (rc < 0) {
printf ("%s: Konnte Port nicht bind(en) (%s)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
/* Daten senden */
for (i = 2; i < argc; i++) {
rc = sendto (s, argv[i], strlen (argv[i]) + 1, 0,
(struct sockaddr *) &remoteServAddr,
sizeof (remoteServAddr));
if (rc < 0) {
printf ("%s: Konnte Daten nicht senden %d\n",
argv[0], i-1 );
close (s);
exit (EXIT_FAILURE);
}
}
return EXIT_SUCCESS;
}
Das Programm bei der Ausführung:
$ gcc -o server server.c
$ gcc -o client client.c
$./server
./server: Wartet auf Daten am Port (UDP) 1234
[Während dessen irgendwo anders im Netzwerk]
$ ./client 127.0.0.1 Test1 Test2 Test3
./client: sende Daten an '127.0.0.1' (IP : 127.0.0.1)
$ ./client 127.0.0.1 erde pluto sonne
./client: sende Daten an '127.0.0.1' (IP : 127.0.0.1)
[Beim Server ergibt sich folgender Zustand]
Mon May 10 07:05:44 2004: Daten erhalten von 127.0.0.1:UDP32838 : Test1
Mon May 10 07:05:44 2004: Daten erhalten von 127.0.0.1:UDP32838 : Test2
Mon May 10 07:05:44 2004: Daten erhalten von 127.0.0.1:UDP32838 : Test3
Mon May 10 07:06:04 2004: Daten erhalten von 127.0.0.1:UDP32838 : erde
Mon May 10 07:06:04 2004: Daten erhalten von 127.0.0.1:UDP32838 : pluto
Mon May 10 07:06:04 2004: Daten erhalten von 127.0.0.1:UDP32838 : sonne
11.17.4 bind() verwenden oder weglassen
Sicherlich haben Sie schon gehört, dass Sie beim Clientprogramm den Funktionsaufruf bind() ganz weglassen können. Wenn Sie beim Client also bind() ganz weglassen, akzeptiert das Programm jede beliebige Ausgangsschnittstelle, um Datagramme an das Ziel zu schicken. Umgekehrt akzeptiert dieses Programm wiederum auch jedes eingehende Datagramm an der Eingangsschnittstelle. Und da hierbei ja auch die Portnummer fehlt, wird auch jede beliebige Portnummer akzeptiert. Man spricht hierbei von einem »wilden« Socket – den man übrigens auch erreicht, wenn bind() mit der IP-Adresse INADDR_NONE und der Portnummer 0 aufgerufen wird:
...
cliAddr.sin_family = AF_INET;
cliAddr.sin_addr.s_addr = htonl (INADDR_NONE);
cliAddr.sin_port = htons (0);
rc = bind ( s, (struct sockaddr *) &cliAddr,
sizeof(cliAddr) );
...
Wenn kein bind() verwendet wird, werden die entsprechenden Daten (IP-Adresse und Portnummer) des Servers beim Versenden von Datagrammen mit der Funktion sendto() angegeben. Diese IP-Adresse und Portnummer wird vom System (Kernel) festgelegt und im Datagramm mitgesendet, damit der Server weiß, wohin er seine Antwort senden kann.
|