28.5 Erstellen einer Server-Anwendung
 
Die Erstellung der Server-Anwendung gestaltet sich nicht viel schwieriger, als die der Client-Anwendung. Der Datenaustausch erfolgt genauso wie bei der Client-Anwendung via send()/recv() (TCP) bzw. sendto()/recvfrom() (UDP). Der Server muss allerdings keine Verbindung herstellen – dies ist die Aufgabe vom Client. Allerdings ist es die Aufgabe des Servers, Verbindungswünsche anzunehmen. Und um dies zu realisieren, müssen Sie den Server in einen Wartezustand versetzen.
28.5.1 bind() – Festlegen einer Adresse aus dem Namensraum
 
Nachdem Sie auch auf der Serverseite mit der Funktion socket() eine »Steckdose« bereitgestellt haben, müssen Sie zunächst die Portnummer der Server-Anwendung festlegen. Sie wissen ja bereits von der Clientanwendung, dass mittels connect() auf eine bestimmte IP-Adresse und eine Portnummer des Servers zugegriffen wird. Unter welcher IP-Adresse und Portnummer, der Server denn nun auf Anfragen der Clients wartet, müssen Sie mit der Funktion bind() festlegen. Somit weisen Sie praktisch einem Socket eine Adresse zu – schließlich ist es durchaus gängig, dass eine Serveranwendung mehrere Sockets verwendet. Dass hierbei meistens die IP-Adresse die gleiche ist, dürfte klar sein, aber es ist durchaus möglich die Datenübertragung über mehrere Ports zuzulassen. Die Funktion bind() wiederum teilt dem Betriebssystem mit, welchen Socket es mit einem bestimmten Port verknüpfen soll. Sobald dann ein Datenpaket eingeht, erkennt das Betriebssystem anhand der Portnummer, für welchen Socket das Paket ist.
Hierzu die Syntax zur Funktion bind() bei Linux/UNIX:
#include <sys/types.h>
#include <sys/socket.h>
int bind( int s, const struct sockaddr name, int namelen );
Und die ähnliche Syntax bei MS-Windows:
#include <winsock.h>
int bind(SOCKET s, const struct sockaddr FAR* name, int namelen);
Als ersten Parameter übergeben Sie, wie immer, den Socket-Deskriptor, den Sie mit socket() angelegt haben. Mit dem zweiten Parameter geben Sie einen Zeiger auf eine Adresse und Portnummer an. Damit teilen Sie dem System mit, welche Datenpakete für welches Socket gedacht sind. Die Struktur sockaddr bzw. (einfacher) sockaddr_in und deren Mitglieder wurde bereits ausführlich im Abschnitt zur Funktion connect() beschrieben. Allerdings sollte hier noch erwähnt werden, dass ein Rechner häufig über verschiedene Rechner (unter mehreren Adressen) und auch verschiedenste Netze (Internet, Intranet, lokales Netzwerk, etc.) erreichbar ist bzw. sein muss. Damit ein Server über alle Netze und IP-Adressen eine Verbindung annimmt, setzt man die IP-Adresse auf INADDR_ANY (natürlich im Network Byte Order). Ansonsten geben Sie die IP-Adresse wie gewöhnlich mit der Funktion inet_addr() an.
Neben der IP-Adresse ist es außerdem auch möglich, eine beliebige Portnummer zuzulassen. Hierfür müssen Sie lediglich 0 als Portnummer (im Network Byte Order) verwenden. Welchen Port Sie dann erhalten haben, können Sie mit der Funktion getsockname() im Nachhinein abfragen. Mehr zu dieser Funktion können Sie aus der entsprechenden Dokumentation entnehmen (bspw. Manual-Page).
Mit dem letzten Parameter geben Sie wiederum die Länge der Struktur (zweiter Parameter) in Bytes mit sizeof() an. bind() liefert im Falle eines Fehlers –1 (gleichwertig mit dem Fehlercode SOCKET_ERROR unter MS-Windows). Welcher Fehler aufgetreten ist, können Sie wiederum mit errno (Linux/UNIX) bzw. WSAGetLastError() (MS-Windows) in Erfahrung bringen.
Hierzu ein kurzer Codeausschnitt, wie die Zuweisung einer Adresse auf der Serverseite in der Praxis realisiert wird:
struct sockaddr_in server;
memset( &server, 0, sizeof (server));
// IPv4-Adresse
server.sin_family = AF_INET;
// Jede IP-Adresse ist gültig
server.sin_addr.s_addr = htonl( INADDR_ANY );
// Portnummer 1234
server.sin_port = htons( 1234 );
if(bind( sock, (struct sockaddr*)&server, sizeof( server)) < 0) {
//Fehler bei bind()
}
28.5.2 listen() – Warteschlange für eingehende Verbindungen einrichten
 
Im nächsten Schritt müssen Sie eine Warteschlange einrichten, welche auf eingehende Verbindungswünsche eines Clients wartet – man spricht auch gerne vom »horchen« am Socket auf eingehende Verbindungen. Eine solche Warteschlange wird mit der Funktion listen() eingerichtet. Dabei wird die Programmausführung des Servers solange unterbrochen, bis ein Verbindungswunsch eintrifft. Mit listen() lassen sich durchaus mehrere Verbindungswünsche »gleichzeitig« einrichten. Die Syntax dieser Funktion sieht unter Linux/UNIX wie folgt aus:
#include <sys/types.h>
#include <sys/socket.h>
int listen( int s, int backlog );
Unter MS-Windows hingegen sieht die Syntax wie folgt aus:
#include <winsock.h>
int listen( SOCKET s, int backlog );
Mit dem ersten Parameter geben Sie wie immer den Socket-Deskriptor an und mit dem zweiten Parameter die Länge der Warteschlange. Die Länge der Warteschlange ist die maximale Anzahl von Verbindungsanfragen, die in eine Warteschlange gestellt werden, wenn keine Verbindungen mehr angenommen werden können.
Der Rückgabewert ist bei Erfolg 0 und auch hier bei einem Fehler –1 (gleich bedeutend unter MS-Windows mit SOCKET_ERROR). Den Fehlercode selbst können Sie wieder wie gehabt mit den errno (Linux/UNIX) bzw. WSAGetLastError() (MS-Windows) auswerten.
In der Praxis sieht die Verwendung von listen() wie folgt aus:
if( listen( sock, 5 ) == –1 ) {
// Fehler bei listen()
}
28.5.3 accept() und die Serverhauptschleife
 
Sobald nun ein oder mehrere Clients Verbindung mit dem Server aufnehmen wollen, können Sie sich darauf verlassen, dass die Funktion accept() immer die nächste Verbindung aus der Warteschlange holt (die Sie mit listen() eingerichtet haben). Hier die Syntax dazu unter Linux/UNIX:
#include <sys/types.h>
#include <sys/socket.h>
int accept( int s, struct sockaddr *addr, socklen_t addrlen );
Und eine ähnliche Syntax unter MS-Windows:
#include <winsock.h>
SOCKET accept( SOCKET s,
struct sockaddr FAR* addr,
int FAR* addrlen );
An der Syntax unter MS-Window lässt sich gleich erkennen, dass die Funktion accept() als Rückgabewert einen neuen Socket zurückgibt. Hierbei handelt es sich um den gleichen Socket mit denselben Eigenschaften, wie vom ersten Parameter s. Über diesen neuen Socket wird anschließend die Datenübertragung der Verbindung abgewickelt. Ein so akzeptiertes Socket kann allerdings nicht mehr für weitere Verbindungen verwendet werden. Das Orginalsocket s hingegen bleibt weiterhin für weitere Verbindungswünsche offen.
Hinweis accept() ist eine blockierende Funktion – dass heißt, accept() blockiert den aufrufenden (Server-)Prozess solange, bis eine Verbindung vorhanden ist. Sofern Sie die Eigenschaften des Socket-Deskriptors auf nicht-blockierend ändern, gibt accept() einen Fehler zurück, wenn beim Aufruf keine Verbindungen vorhanden sind.
|
In den zweiten Parameter schreibt accept() Informationen (IP-Adresse und Port) über den Verbindungspartner in die Struktur sockaddr bzw. sockaddr_in. Dies ist logischerweise nötig, damit Sie wissen, mit wem Sie es zu tun haben. addrlen wiederum ist die Größe der Struktur sockaddr bzw. sockaddr_in – allerdings wird diesmal ein Zeiger auf die Größe der Adresse erwartet!
Bei einem Fehler wird –1 (gleich bedeutend mit SOCKET_ERROR unter MS-Windows) zurückgegeben. Die genaue Ursache des Fehlers können Sie wieder mit errno (Linux/UNIX) bzw. WSAGetLastError() (MS-Windows) ermitteln. Bei erfolgreicher Ausführung von accept() wird, wie bereits beschrieben, ein neuer Socket-Deskriptor zurückgegeben.
Ein wichtiger Teil der Serverprogrammierung ist außerdem die Serverhauptschleife. In dieser Schleife wird gewöhnlich die Funktion accept() aufgerufen und darin findet auch gewöhnlich der Datentransfer zwischen Client und Server statt. Hier ein Beispiel einer solchen Serverhauptschleife:
struct sockaddr_in client;
int sock, sock2;
socklen_t len;
...
for (;;) {
len = sizeof( client );
sock2 = accept( sock, (struct sockaddr*)&client, &len);
if (sock2 < 0) {
//Fehler bei accept()
}
// Hier beginnt der Datenaustausch
}
Hierzu nochmals der bildliche Vorgang aller Socket-Funktionen für eine TCP-Verbindung zwischen dem Server und dem Client (siehe Abbildung 28.2).
Selbiger bildlicher Vorgang aller Socket-Funktionen für eine UDP-Verbindung zwischen Server und Client sieht hingegen so aus (siehe Abbildung 28.3).
 Hier klicken, um das Bild zu Vergrößern
Abbildung 28.2
Kompletter Vorgang einer TCP-Client/Server-Verbindung
 Hier klicken, um das Bild zu Vergrößern
Abbildung 28.3
Kompletter Vorgang einer UDP-Client/Server-Verbindung
|