Wissen: Einführung in die Netzwerkprogrammierung mit der Socket-API

Inhalt dieses Artikels sind die Grundlagen der Programmierung von Client-Server-Anwendungen mit BSD-Sockets. Es wird ein Überblick über die wichtigsten Funktionen der API gegeben. Anhand eines Beispiels, das im Verlauf des Artikels immer weiter verfeinert wird, werden verschiedene Aspekte aufgezeigt, die bei der Entwicklung eine Rolle spielen. Nach der Lektüre dieses Artikels sollte der Leser in der Lage sein, eigene Anwendungen zu entwickeln und damit über ein TCP-Netzwerk Daten auszutauschen. Es muss hierbei allerdings davon ausgegangen werden, dass beim Leser bereits ausreichende Kenntnisse in ANSI-C vorhanden sind, um dem zur Verfügung stehenden Platz Rechnung zu tragen.

Was unterscheidet Client-Server-Anwendungen von normalen Programmen?

Bevor wir in die Programmierung einsteigen, soll an dieser Stelle ein Gefühl für die Umgebung vermittelt werden, in der wir uns demnächst bewegen. Der Begriff Client-Server ist nämlich etwas weiter gefasst als man auf den ersten Blick vermuten mag und nicht nur auf die Verteilung einer einzelnen Anwendung auf mehrere Rechner beschränkt.

Der Begriff „Anwendung“ kann im Netzwerkkontext auf zweierlei Arten interpretiert werden.

Zum einen bezieht er sich auf die siebte Schicht des OSI-Referenzmodells, die durch „Anwendungsprotokolle“ wie HTTP, SMTP und dergleichen eingenommen wird. Aus Sicht von TCP ist die Anwendung somit ein weiteres Protokoll, das dessen Dienste in Anspruch nimmt. Darüber hinaus kann eine Anwendung aber auch ein Prozess sein, der oberhalb dieser Schicht-7-Protokolle liegt und auf deren Dienste zugreift. Dabei kann es sich beispielsweise um einen Webshop oder -browser handeln, der die Dienste von HTTP oder HTTPS in Anspruch nimmt oder ein Mailprogramm, das mit Hilfe von SMTP und POP E-Mail senden und empfangen kann. Die hier vorgestellten Anwendungen und Beispielprogramme sind im allgemeinen der Schicht 7 zuzuordnen, da sie direkt auf Dienste der Transportschicht zugreifen.

Das, was einen Server über seine Implementierung hinaus definiert, wird durch die drei Begriffe Dienst, Protokoll und Schnittstelle beschrieben. Der Dienst drückt aus, was der Server macht. Er stellt einer Applikation eine Reihe von Operationen zur Verfügung, enthält jedoch keine Hinweise auf eine spezielle Implementierung. Das Protokoll legt hingegen den Ablauf (also das wie) und das Verhalten von Client und Server fest. Es beschreibt damit die Regeln, die zwischen beiden beteiligten Instanzen gelten. Teil der Protokolldefinition sind die Syntax und Semantik der übermittelten Nachrichten.

Schließlich wird mit der Schnittstelle beschrieben, wo man den zugehörigen Dienst erreicht.

Ein Server implementiert also ein Protokoll, das einen Dienst erbringt, der wiederum nur durch eine definierte Schnittstelle angesprochen werden kann. Diese Trennung ist erreicht eine möglichst lose Kopplung zwischen diesen drei Teilen. Ziel ist es, etwa die Schnittstelle, also die Sicht des Clients auf den Server, nicht verändert zu müssen, wenn die Implementierung des Servers aktualisiert wird.

Die Idee, eine Anwendung auf diese Weise in mehrere Teile aufzuspalten, ist nicht auf die Netzwerkwelt beschränkt. Auch auf einem lokalen System kann es sinnvoll sein, spezialisierte Prozesse für die Ausführung eines bestimmten Dienstes zu etablieren. Ein Beispiel hierfür ist der Syslog-Daemon, der anderen Prozessen eine Möglichkeit zur Ausgabe von Fehlermeldungen bietet. Man kann hier sagen, dass Prozesse ohne eigenes Kontrollterminal die Ausführung von Fehlerausgaben an diesen Daemon abtritt. Im Gegensatz zu Netzwerkanwendungen ist man hierbei jedoch nicht auf Sockets als Kommunikationsmedium beschränkt.

Hinter dem Client-Server-Paradigma verbirgt sich also die Idee, allgemein verwendbare Dienste von der eigentlichen Anwendung zu trennen und an andere Prozesse zu delegieren.

Zum einen wird hierdurch eine gewisse Form von Modularisierung erreicht. Durch das Protokoll und die definierte Schnittstelle eines Dienstes sind die Bindeglieder zwischen Client und Server vollständig beschrieben. Beide Teile haben darüber hinaus keine Kenntnis voneinander und können dadurch auch unabhängig voneinander verändert werden.

Ein weiterer Aspekt der Trennung ist die Möglichkeit der Aufteilung von Rechenlast oder die Zuteilung von Aufgaben an Hosts mit spezialisierter Architektur. So ist eine Applikation denkbar, zu deren Aufgabe es gehört, Multiplikationen von Matrizen durchzuführen. Hier kann eine Verteilung der Gesamtlast auf mehrere Rechner gewinnbringend sein, da die Matrixmultiplikation aus voneinenander unabhängigen Teilen besteht. Eine zusätzliche Leistungssteigerung ist denkbar, wenn ein Vektorrechner zur Verfügung steht und die Applikation so verteilt werden kann, dass Matrixoperationen gezielt an diesen gereicht werden, während beispielsweise die Benutzerschnittstelle auf einem herkömmlichen PC abläuft. Andere sinnvolle Anwendungen sind Datenbankzugriffe, die mit einer Netzwerkverbindung einhergehen können, wenn ein zentraler Datenbankserver die Datenhaltung für mehrere Arbeitsplätze übernimmt.

Eine solche Aufteilung einer Applikation bringt natürlich eine veränderte Situation für die Entwicklung mit sich. Durch die Tatsache, dass kein gemeinsamer Adressraum für die gesamte Anwendung existiert, gestaltet sich die Suche nach Fehlern aufwändiger. Häufig kann hier nur mit Timeouts gearbeitet werden, um einen Ausfall eines entfernten Prozesses zu erkennen. Die Übertragung von Daten über öffentliche Netzwerke kann eine Verschlüsselung notwendig machen. Außerdem kann es angebracht sein, zwischen verschiedenen Benutzern zu unterscheiden. In diesem Fall können Mechanismen zur Authentifizierung und Authorisierung von Anwendern, sowie eine Rechteverwaltung integriert werden. Eine der größten Herausforderung bei der Konzeption und Entwicklung verteilter Anwendungen ist jedoch die massive Nebenläufigkeit, die durch die Verwendung mehrerer Rechner entsteht. Als Entwickler hat man sich in diesem Fall mit Maßnahmen zur Synchronisation mehrerer Prozesse, bzw. der Vermeidung von Wettbewerbsbedingungen auseinanderzusetzen. Diese Problematik wird nicht zuletzt durch einen Mehrbenutzerbetrieb, wie ihn beispielsweise Datenbankserver bieten, verstärkt. Eine genaue Vorstellung verschiedener konkurrierender Synchronisations- und Schutzmechanismen wie Semaphore, Monitore und Mutexe kann hier allerdings nicht erfolgen. In den vorgestellten Beispielen wird die Kenntnis dieser Strukturen jedoch nicht vorausgesetzt, um den Blick auf das Wesentliche zu fokussieren. An den entsprechenden Stellen wird auf die Problematik hingewiesen.

Ablauf von Client-Server-Anwendungen

Den Kommunikationsablauf zwischen Client und Server kann man sich als aus vier Schritten bestehend denken. Der Request ist die Anfrage des Client. In Analogie zu einem eingehenden Telefonanruf wäre dieser Schritt mit dem Wählen einer Telefonnummer zu vergleichen. Im nächsten Moment geht diese Anfrage beim Server ein und dieser wird davon in Kenntnis gesetzt. Diese Indication ist beim Telefonieren mit dem Klingeln des Telefons gleichzusetzen. Erfolgt die Kommunikation in beide Richtungen, sendet der Server seine Antwort nun an den Client zurück. Dieser Response genannte Schritt entspricht dem Abnehmen des Telefonhörers. Trifft die Serverantwort beim Client ein, spricht man von Confirm. Im Falle des Telefonats bemerkt der Anrufer durch das Freizeichen das Abnehmen des Telefonhörers durch den Angerufenen.

Im allgemeinen hat der Server bei dieser Aufteilung keine Kenntnis über vorangegangene Aufrufe durch den Client. Der Server arbeitet also zustandslos und in den meisten Fällen ist das auch völlig ausreichend. Es existieren allerdings auch Anwendungen, bei denen der Server in der Lage sein muß, eingehende Anfragen bestimmten Clients zuzuordnen. Beispiele hierfür sind HTTP-Server. Das HTTP-Protokoll arbeitet von Haus aus ohne interne Zustände. Setzt eine Anwendung wie ein Online-Shop auf HTTP auf, wird jedoch genau diese Eigenschaft von HTTP zu einem Problem. Aus Sicht des Servers wird jede eingehende Anfrage unabhängig von vorangehenden behandelt. Fordert nun ein und derselbe Benutzer verschiedene Seiten des Shops an, sieht der Server hier nur eine Reihe unzusammenhängender Anfragen. Da der Server kein Gedächtnis hat, ist die Folge, dass eine eingangs durchgeführte Authentifizierung des Benutzers bei jeder Nachfrage erneuert werden muss. Das ist natürlich nicht wünschenswert.

Es muss daher eine Möglichkeit gefunden werden, zwischen Client und Server Informationen zu auszutauschen, die für die Dauer der Verbindung Anfragen einem bestimmten Client zuordnen oder dem Server ermöglichen, einen Client als noch nicht angemeldet zu identifizieren.

Der vormals zustandslose Server wird hierdurch um interne Zustände erweitert. Im Zusammenhang mit HTTP spricht man hier von einer Sitzung (HTTP-Session).

In einem der folgenden Abschnitte werden wir noch verschiedene Realisierungen für diesen Mechanismus betrachten.

Was ist ein Socket?