Einführung in die Netzwerkprogrammierung
aus PUG, der Penguin User Group
Inhaltsverzeichnis |
Allgemeines
In diesem Artikel möchte ich eine kleine Einführung geben, wie man sowohl TCP- als auch UDP-Client-/Server-Programme erstellt. Dazu stelle ich die nötigen System-Calls vor und zeige am Ende des Artikels mehrere kleine Beispiele.
Um diese Einführung so kurz wie möglich zu halten, habe ich im folgenden auf einiges verzichtet:
- OSI-Modell: Eine ausführliche Erklärung des OSI-Schichtenmodells ist bei Wikipedia zu finden.
- Protokolle: Beschreibungen von TCP und UDP finden sich ebenfalls bei Wikipedia:
- IPv6: Diese Einführung benutzt nur IPv4-Funktionsaufrufe
- Beschreibung der System-Calls: Die genaue Beschreibung ist in den Manpages Abschnitt 2 und 3 zu finden: man 2 <system-call>
- fork()/Threads: Teilweise ist es nötig einen Sub-Prozess zu erzeugen. Hier benutze ich ein einfaches fork(). Andere Methoden werden hier nicht näher beschrieben.
fork()
switch(fork()) {
case -1:
//fork fehlgeschlagen
perror("fork error");
exit(errno);
case 0:
//Code des Sub-Prozesses
default:
//Code des Super-Prozesses
}
struct sockaddr_in
Zunächst ist es wichtig die struct sockaddr_in kennen zu lernen. Mit ihr werden die Daten "Protokollfamilie" (AF_INET), Port-Nummer und IP-Adresse definiert und die Funktionsaufrufe bind, connect, accept bekommen Argumente in dieser Form übergeben. Diese struct ist in <netinet/in.h> folgendermaßen definiert:
struct in_addr {
inn_addr_t s_addr; /* 32-bit IPv4 address */
/* network byte ordered */
};
struct sockaddr_in {
sa_familiy_t sin_familiy; /* AF_INET */
in_port_t sin_port; /* 16-bit TCP or UDP port number */
/* network byte ordered */
struct sin_addr; /* 32-bit IPv4 address */
/* network byte ordered */
char sin_zero[8];
};
Diese Struktur wird folgendermaßen benutzt:
#include <netinet/in.h> struct sockaddr_in server_address; server_address.sin_family=AF_INET; server_address.sin_port=htons(port); server_address.sin_addr.s_addr=htonl(IPv4_address); memset(&server_address.sin_zero, 0, sizeof(server_address.sin_zero));
Man kann einem Server auch angeben, daß er auf allen (verfügbaren) IP-Adressen ansprechbar sein soll (INADDR_ANY):
server_address.sin_addr.s_addr=htonl(INADDR_ANY);
socket
Der Anfang ist überall gleich, egal ob wir einen Client oder einen Server programmieren wollen und egal ob er TCP oder UDP sprechen soll. Erste Amtshandlung ist immer das öffnen eines Sockets.
#include <sys/socket.h> int socket (int family, int type, int protocol);
Als Socket-Family benutzen wir AF_INET (IPv4 Protokollfamilie). Für den Protokolltyp beschränken wir uns hier auf zwei Typen:
- TCP: SOCK_STREAM
- UDP: SOCK_DGRAM
Das Protokoll selbst ist damit vorgegeben, so daß wir den dritten Parameter auf 0 setzen können.
Beispiel für TCP:
#include <sys/socket.h>
int fd; /* file descriptor */
if ((fd=socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket error");
exit(errno);
}
TCP- und UDP-Client
Als TCP-Client müssen wir ein connect ausführen. Wir können danach mit read und write Daten empfangen und senden. Ein UDP-Client muß kein connect ausführen. In diesem Fall arbeitet er mit recvfrom und sendto. Die Angabe von connect ermöglicht einem UDP-Client jedoch mit read und write zu arbeiten.
connect
Als Client öffnen wir also jetzt auf dem oben definierten Socket eine Verbindung zu dem in struct sockaddr_in angegebenen Server.
#include <sys/socket.h> int connect (int filedescriptor, const struct sockaddr *server_address, socklen_t address_length);
Beispiel:
#include <sys/socket.h>
.
.
.
if (connect(fd, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
perror("connect error");
exit(errno);
}
TCP-Server
listen
Als TCP-Server hingegen würden wir auf dem angegebenen Port auf einen Verbindungswunsch "lauschen".
#include <sys/socket.h> int listen (int filedescriptor, int backlog);
Die Funktion des Parameters backlog ist als "Maximum actual number of queued connections" angegeben, ist auf jedem UNIX-System unterschiedlich und die Definition füllt mehrere Seiten. Der Einfachheit halber setzten wir sie so hoch wie möglich.
Beispiel:
#include <sys/socket.h> . . . listen (fd, 15);
accept
Nun wollen wir eine eingehende Verbindung "Akzeptieren":
#include <sys/socket.h> int accept (int filedescriptor, struct sockaddr *client_address, socklen_t *address_length);
Beispiel:
#include <sys/socket.h>
int fdclient; /* file descriptor for client connection */
int rlen;
struct sockaddr_in remote_address;
.
.
.
rlen=sizeof(remote_address);
if ((fdclient=accept(fd, (struct sockaddr*)&remote_address, &rlen)) < 0) {
perror("accept error");
exit(errno);
}
read
Die Funktionen read und write sollten allgemein bekannt sein. Mit ihnen können wir nun Daten senden und empfangen:
#include <unistd.h> ssize_t read(int filedescriptor, const void *buffer, size_t buffer_length);
write
#include <unistd.h> ssize_t write(int filedescriptor, const void *buffer, size_t buffer_length);
UDP-Server
Ein UDP-Server macht kein listen und accept, da keine direkte "Verbindung" aufgebaut wird. Er holt seine Daten einfach per recvfrom aus einer Art Queue.
recvfrom und recv
Um UDP-Daten zu lesen benutzt man recvfrom. Dieses wird genau so benutzt wie ein read, nur das dabei zusätzliche Flags angegeben werden können. Auf eine Erklärung der Flags wird hier verzichtet. Als weiteren Parameter bekommt man in struct sockaddr *from die Adresse des Senders mitgeteilt.
#include <sys/socket.h> ssize_t recvfrom(int filedescriptor, void *buffer, size_t buffer_length, int flags, struct sockaddr *from, socklen_t *from_length);
Ein recv ist ein recvfrom mit vorherigem connect. Daher fällt die Socketadresse hier weg.
#include <sys/socket.h> ssize_t recv(int filedescriptor, void *buffer, size_t buffer_length, int flags);
sendto und send
Um UDP-Daten zu lesen benutzt man sendto. Dieses wird genau so benutzt wie ein send, nur das dabei zusätzliche Flags und die Zieladresse angegeben werden müssen.
#include <sys/socket.h> ssize_t sendto(int filedescriptor, const void *buffer, size_t buffer_length, int flags, const struct sockaddr *to, socklen_t to_length);
Ein vorher ausgeführtes connect macht die Angabe der Zieladresse überflüssig, daher kann in diesem Fall ein send benutzt werden.
#include <sys/socket.h> ssize_t send(int filedescriptor, const void *buffer, size_t buffer_length, int flags);
close
Natürlich dürfen wir nicht vergessen, den File-Deskriptor am Ende der Verbindung wieder zu schließen:
#include <unistd.h> int close (int <file descriptor>);
Hilfsfunktionen
Konvertierung zwischen host- und network byte order
Da auf den verschiedenen Architekturen und Betriebssystemen die byte order unterschiedlich ist, benutzt man für das Internet die einheitliche network byte order. Um IP-Adressen und Ports von der lokalen host byte order in die network byte order umzuwandeln gibt es Konvertierungsfunktionen:
#include <netinet/in.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
- hton bedeutet host-to-network
- ntoh beteutet network-to-host
- beide Funktionen gibt es für short int (z.B. Ports) und long int (z.B. IP-Adressen).
IP-Adresse in Binärdaten
Um die IP-Adressen von der bekannten numbers-and-dots-Notation in Binärdaten umzurechnen, gibt es weitere Konvertierungsfunktionen. Ich stelle hier nur diejenigen vor, die direkt von/nach network byte order umzuwandeln:
#include <netinet/in.h> in_addr_t inet_addr(const char *cp); char *inet_ntoa(struct in_addr in);
Beispiel:
217.19.187.24 -> inet_addr -> 0x18bb13d9 217.19.187.24 <- inet_ntoa <- 0x18bb13d9
gethostbyname und gethostbyaddr
Um die IP-Adresse zu einem hostname zu bekommen - und umgekehrt - benutzt man die Funktionen gethostbyname und gethostbyaddr:
#include <netdb.h> extern int h_errno; struct hostent *gethostbyname(const char *name); #include <sys/socket.h> /* for AF_INET */ struct hostent *gethostbyaddr(const char *addr, int len, int type);
gethostbyname gibt seine Fehlercodes nicht über errno aus. Daher muß das externe h_errno deklariert und abgefragt werden.
Beispiel:
if ((server_entry=gethostbyname(server_addr)) == NULL) {
printf("gethostbyname failed\n");
exit(h_errno);
}
strcpy(server_ip, inet_ntoa(*(struct in_addr*)server_entry->h_addr_list[0]));
printf("connecting to %s (%s) on port %s\n", server_addr, server_ip, server_port);
weitere geplante Kapitel
- select()
Beispiele
Einige Bemerkungen zu den Beispielen:
- Clients: Der TCP-Client in den Beispielen unterscheidet sich vom UDP-Client dabei nur in der entsprechenden Option im socket-Aufruf. Die Clients nehmen lediglich eine Textfolge von stdin entgegen und senden sie an den Server. Danach warten sie auf eine Antwort vom Server.
- Server: Was der Server mit den Daten macht, die er vom Client gesendet bekommt, hängt natürlich vom Anwendungsfall ab. Um in unseren Beispielen überhaupt irgendetwas zu machen wandeln wir hier die Textfolge vom Client einfach in Groß-/Kleinbuchstaben um und senden sie zurück.

