25.5 Erstellen einer Server-Anwendung 

Eine Server-Anwendung zu erstellen ist nicht viel schwieriger als das Programmieren 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 des Clients. Allerdings ist es die Aufgabe des Servers, Verbindungswünsche anzunehmen. Und um dies zu realisieren, müssen Sie den Server in einen Wartezustand versetzen.
25.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 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.
Die Syntax zur Funktion bind() lautet bei Linux/UNIX:
#include <sys/types.h> #include <sys/socket.h> int bind( int s, const struct sockaddr name, int namelen );
Bei MS-Windows lautet die Syntax ähnlich:
#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 in Network Byte Order). Ansonsten geben Sie die IP-Adresse wie gewöhnlich mit der Funktion inet_addr() an.
Es ist außerdem auch möglich, neben der IP-Adresse eine beliebige Portnummer zuzulassen. Hierfür müssen Sie lediglich 0 als Portnummer (in 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 (beispielsweise der 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.
Hier sehen Sie einen kurzen Codeausschnitt, der zeigt, 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() }
25.5.2 »listen()« – Warteschlange für eingehende Verbindungen einrichten 

Im nächsten Schritt müssen Sie eine Warteschlange einrichten, die auf eingehende Verbindungswünsche eines Clients wartet – man spricht auch gerne vom »Horchen« auf Verbindungen, die am Socket eingehen. Eine solche Warteschlange wird mit der Funktion listen() eingerichtet. Dabei wird die Programmausführung des Servers so lange 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 (gleichbedeutend unter MS-Windows mit SOCKET_ERROR). Den Fehlercode selbst können Sie wieder wie gehabt mit 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() }
25.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 sehen Sie die Syntax dazu unter Linux/UNIX:
#include <sys/types.h> #include <sys/socket.h> int accept( int s, struct sockaddr *addr, socklen_t addrlen );
Die Syntax unter MS-Windows ist ähnlich:
#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 ein neues Socket zurückgibt. Hierbei handelt es sich um das gleiche Socket mit denselben Eigenschaften wie vom ersten Parameter s. Über dieses neue 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. Das heißt, accept() blockiert den aufrufenden (Server-)Prozess so lange, 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. |
Mit dem 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 (gleichbedeutend 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 sehen Sie ein Beispiel für eine solche 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. }
Abbildung 25.2 verdeutlicht alle Socket-Funktionen für eine TCP-Verbindung zwischen dem Server und dem Client anhand einer Grafik.
Abbildung 25.2 Kompletter Vorgang einer TCP-Client/Server-Verbindung
Für eine UDP-Verbindung zwischen Server und Client sieht der Vorgang hingegen so aus wie in Abbildung 25.3.
Abbildung 25.3 Kompletter Vorgang einer UDP-Client/Server-Verbindung
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.