Fachkonzept - Client-Server-Kommunikation mit Sockets
Bevor du diese Seite liest, solltest du das Fachkonzept - Client-Server-System kennen.
Die folgende Abbildung verdeutlicht die Aktionen von Client und Server bei einer Client-Server-Kommunikation:
Der Server muss hierzu zunächst einen sog. Verbindungssocket erzeugen und diesen Socket an IP-Adresse und Port eines Prozesses binden. Abschließend beginnt der Socket mit dem Lauschen auf Verbindungsanfragen.
Ein Client muss ebenfalls einen Socket erzeugen, der hier Kommunikationsssocket genannt wird. Anschließend kann ein solcher Kommunikationssocket mit dem Verbindungssocket des Servers Kontakt aufnehmen und einen Verbindungswunsch anfragen. Hier läuft (unter TCP) dann ein Drei-Wege-Handschlagprotokoll ab.
Wenn die Verbindsanfrage vom Verbindungssocket des Servers akzeptiert wird, dann erzeugt der Verbindungssocket einen zusätzlichen Kommunikationssocket. Der Verbindungssocket übernimmt also nicht den Austausch der Daten, sondern überlässt das dem neu erzeugten Kommunikationssocket, um selbst auf weitere Verbindungsanfragen reagieren zu können.
Die Kommunikationssockets von Client und Server können jetzt Daten senden und empfangen.
Wenn der Nachrichtenaustausch beendet ist, werden die Kommunikationssockets geschlossen. Der Verbindungssocket des Servers wird in der Regel erst dann geschlossen, wenn der Server beendet wird.
Operationen zur Umesetzung einer Client-Server-Kommunikation in Python
Die Socket-Schnittstelle (Socket-API) legt Operationen fest, um den oben beschriebenen Client-Server-Kommunikation zu realisieren:
Den Server für die Kommunikation vorbereiten
Wenn ein Client mit einem Server kommunizieren möchte, muss er eine Verbindung bei dem Server anfragen. Um die Verbindung annehmen zu können, muss der Server darauf vorbereitet sein. Dazu benötigt man einen Verbindungssocket. Ein Verbindungssocket wird, genau wie ein Kommunikationssocket, in Python durch ein Software-Objekt dargestellt. In der Regel wird der Verbindungssocket nur ein einziges mal erzeugt und bleibt während der gesamten Laufzeit des Servers bestehen.
Das folgende Beispiel zeigt, wie ein Verbindungssocket erzeugt wird:
import socket
# Verbindungssocket erzeugen
verbindungs_s = socket.socket()
# Verbindungssocket an einen Port "binden".
# Der Server wird also über Port 5000 erreichbar sein.
verbindungs_s.bind(('', 5000))
# mit dem Lauschen beginnen
# (d.h. nach diesem Befehl werden höchstens 20 Verbindungsanfragen
# in eine Warteschlange eingereiht)
verbindungs_s.listen(20)
Der Client fragt eine Verbindung an
Mit der Methode komm_s.connect( (ip_adresse, port) )
kann ein Client eine Verbindung bei einem Server anfragen.
Als Parameter erwartet diese Methode ein Tupel, in dem die IP-Adresse und der Port des Servers gespeichert ist:
import socket
komm_s = socket.socket()
adresse = ('127.0.0.1', 5000)
komm_s.connect(adresse)
Der Server nimmt eine Verbindung an
Server, die wir programmieren, können zu einem Zeitpunkt immer nur mit einem Client kommunizieren. Falls mehrere Verbindungsanfragen gleichzeitig eingehen, werden sie in eine Warteschlange eingereiht. Die Methodeverbindungs_s.accept()
entnimmt eine Verbindsanfrage aus der Warteschlange und baut die Verbindung auf.
Falls es gerade keine Verbindungsanfrage gibt, wartet dieser Befehl auf eine Anfrage.
verbindungs_s.accept()
gibt zwei Werte zurück:
Einen Kommunikationssocket und die ip-Adresse des Clients, zu dem gerade eine Verbindung aufgebaut wurde.
Das folgende Beispiel zeigt, wie man diese beiden Rückgabewerte in je einer Variablen speichert:
komm_s, client_adresse = verbindungs_s.accept()
Mit dem Kommunikationssocket kann anschließend genau so gearbeitet werden, wie mit einem Kommunikationssocket, der auf einem Client erzeugt wurde (vgl. Kapitel Client zu einem Geburtstagsserver).
Man kann auch Server programmieren, die mit mehreren Clients gleichzeitig kommunizieren können. Dazu ist jedoch ein größer Aufwand nötig.
Anzeigen, dass (im Moment) keine weiteren Daten gesendet werden
Der Geburtstagsserver aus Client zu einem Geburtstagsserver benötigt das Geburtsdatum des Benuters. Deshalb muss der Client zunächst das Geburtsdatum an den Server schicken. Woran erkennt der Server, dass der Client das gesamte Geburtsdatum übertragen wurde? Es gibt mehrere Möglichkeiten, wie man dieses Problem lösen könnte. Bei unseren Programm schicken wir immer das Byte 0 um anzuzeigen, dass wir im Moment keine weiteren Daten schicken möchten:
# Sende das Geburtsdatum '14.3.'
tag = '14.'
tagBytesObj = byte(tag, 'utf-8')
komm_s.sendall(tagBytesObj)
monat = '3.'
monatAlsBytesObj = bytes(monat, 'utf-8')
komm_s.sendall(monatAlsBytesObj)
# Bytes-Objekt erzeugen, das nur das Byte 0 enthält:
trennByte = bytes([0])
# Dem Empfänger anzeigen,
# dass wir im Moment keine weiteren Daten schicken:
komm_s.sendall(trennByte)
Der Empfänger muss also so lange einzelne Bytes empfangen, bis er das Byte 0 empfängt. Diese Aufgaben erledigen für uns folgende Funktionen, die sich in der Datei socketLib.py befinden:
-
empfangeStr(komm_s)
- Liest Daten, bis der Sender anzeigt, dass er keine weiteren Daten schicken wird.
- Parameter
komm_s
: Socket, von dem gelesen werden soll - Rückgabewert: gelesene Daten als String (Kodierung utf-8)
-
sendeStr(komm_s, datenStr)
- Wandelt den String datenStr in ein Bytes-Objekt um (Kodierung utf-8) und versendet diesen String über den Kommunikationssocket komm_s
- Parameter
komm_s
: Socket, über den gesendet werden soll - Parameter
datenStr
: String, der umgewandelt und versendet werden soll
-
sendeTrennByte(komm_s)
- Sendet das Byte 0. Daran erkennt der Empfänger, das der Sender zunächst keine weiteren Daten sende.
- Parameter
komm_s
: Socket, über den gesendet werden soll