Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

 << zurück
Linux-UNIX-Programmierung von Jürgen Wolf
Das umfassende Handbuch – 2., aktualisierte und erweiterte Auflage 2006
Buch: Linux-UNIX-Programmierung

Linux-UNIX-Programmierung
1216 S., mit CD, 49,90 Euro
Rheinwerk Computing
ISBN 3-89842-749-8
gp Kapitel 11 Netzwerkprogrammierung
  gp 11.1 Einführung
  gp 11.2 Aufbau von Netzwerken
    gp 11.2.1 ISO/OSI und TCP/IP – Referenzmodell
    gp 11.2.2 Das World Wide Web (Internet)
  gp 11.3 TCP/IP – Aufbau und Struktur
    gp 11.3.1 Netzwerkschicht (Datenübertragung)
    gp 11.3.2 Internetschicht
    gp 11.3.3 Transportschicht (TCP, UDP)
    gp 11.3.4 Anwendungsschicht
  gp 11.4 TCP Socket
  gp 11.5 Kommunikationsmodell
  gp 11.6 Grundlegende Funktionen zum Zugriff auf die Socket-Schnittstelle
    gp 11.6.1 Ein Socket anlegen – socket()
    gp 11.6.2 Verbindungsaufbau – connect()
    gp 11.6.3 Socket mit einer Adresse verknüpfen – bind()
    gp 11.6.4 Auf Verbindungen warten – listen() und accept()
    gp 11.6.5 Senden und Empfangen von Daten (1) – write() und read()
    gp 11.6.6 Senden und Empfangen von Daten (2) – send() und recv()
    gp 11.6.7 Verbindung schließen – close()
  gp 11.7 Aufbau eines Clientprogramms
    gp 11.7.1 Zusammenfassung: Clientanwendung und Quellcode
  gp 11.8 Aufbau des Serverprogramms
    gp 11.8.1 Zusammenfassung: Serveranwendung und Quellcode
  gp 11.9 IP-Adressen konvertieren, manipulieren und extrahieren
    gp 11.9.1 inet_aton(), inet_pton() und inet_addr()
    gp 11.9.2 inet_ntoa() und inet_ntop()
    gp 11.9.3 inet_network()
    gp 11.9.4 inet_netof()
    gp 11.9.5 inet_lnaof()
    gp 11.9.6 inet_makeaddr()
  gp 11.10 Namen und IP-Adressen umwandeln
    gp 11.10.1 Name-Server
    gp 11.10.2 Informationen zum Rechner im Netz – gethostbyname und gethostbyaddr
    gp 11.10.3 Service-Informationen – getservbyname() und getservbyport()
  gp 11.11 Der Puffer
  gp 11.12 Standard-E/A-Funktionen verwenden
    gp 11.12.1 Pufferung von Standard-E/A-Funktionen
  gp 11.13 Parallele Server
  gp 11.14 Syncrones Multiplexing – select()
  gp 11.15 POSIX-Threads und Netzwerkprogrammierung
  gp 11.16 Optionen für Sockets setzen bzw. erfragen
    gp 11.16.1 setsockopt()
    gp 11.16.2 getsockopt()
    gp 11.16.3 Socket-Optionen
  gp 11.17 UDP
    gp 11.17.1 Clientanwendung
    gp 11.17.2 Serveranwendung
    gp 11.17.3 recvfrom() und sendto()
    gp 11.17.4 bind() verwenden oder weglassen
  gp 11.18 UNIX-Domain-Sockets (IPC)
    gp 11.18.1 Die Adressstruktur von UNIX-Domain-Sockets
    gp 11.18.2 Lokale Sockets erzeugen – socketpair()
  gp 11.19 Multicast-Socket
    gp 11.19.1 Anwendungsgebiete von Multicast-Verbindungen
  gp 11.20 Nicht blockierende I/O-Sockets
  gp 11.21 Etwas zu Streams und TLI, Raw Socket, XTI
    gp 11.21.1 Raw Socket
    gp 11.21.2 TLI und XTI
    gp 11.21.3 RPC (Remote Procedure Call)
  gp 11.22 IPv4 und IPv6
    gp 11.22.1 IPv6 – ein wenig genauer
  gp 11.23 Netzwerksoftware nach IPv6 portieren
    gp 11.23.1 Konstanten
    gp 11.23.2 Strukturen
    gp 11.23.3 Funktionen
  gp 11.24 Sicherheit und Verschlüsselung


Rheinwerk Computing

11.17 UDP  downtop

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!


Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 11.12    UDP besitzt keine Fehlerkontrolle wie TCP zur Ü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.


Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 11.13    Das Client-Sever-Prinzip mit UDP


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.


Rheinwerk Computing

11.17.1 Clientanwendung  downtop

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.


Rheinwerk Computing

11.17.2 Serveranwendung  downtop

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.


Rheinwerk Computing

11.17.3 recvfrom() und sendto()  downtop

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

Rheinwerk Computing

11.17.4 bind() verwenden oder weglassen  toptop

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.

 << zurück
  
  Zum Rheinwerk-Shop
Neuauflage: Linux-UNIX-Programmierung
Neuauflage:
Linux-UNIX-
Programmierung

bestellen
 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchtipps
Zum Rheinwerk-Shop: Linux-Server






 Linux-Server


Zum Rheinwerk-Shop: Das Komplettpaket LPIC-1 & LPIC-2






 Das Komplettpaket
 LPIC-1 & LPIC-2


Zum Rheinwerk-Shop: Linux-Hochverfügbarkeit






 Linux-
 Hochverfügbarkeit


Zum Rheinwerk-Shop: Shell-Programmierung






 Shell-
 Programmierung


Zum Rheinwerk-Shop: Linux Handbuch






 Linux Handbuch


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
Info





Copyright © Rheinwerk Verlag GmbH 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern