Rheinwerk Computing < openbook >

 
Inhaltsverzeichnis
1 Einleitung
2 Die Programmiersprache Python
Teil I Einstieg in Python
3 Erste Schritte im interaktiven Modus
4 Der Weg zum ersten Programm
5 Kontrollstrukturen
6 Dateien
7 Das Laufzeitmodell
8 Funktionen, Methoden und Attribute
9 Informationsquellen zu Python
Teil II Datentypen
10 Das Nichts – NoneType
11 Operatoren
12 Numerische Datentypen
13 Sequenzielle Datentypen
14 Zuordnungen
15 Mengen
16 Collections
17 Datum und Zeit
18 Aufzählungstypen – Enum
Teil III Fortgeschrittene Programmiertechniken
19 Funktionen
20 Modularisierung
21 Objektorientierung
22 Ausnahmebehandlung
23 Iteratoren
24 Kontextobjekte
25 Manipulation von Funktionen und Methoden
Teil IV Die Standardbibliothek
26 Mathematik
27 Kryptografie
28 Reguläre Ausdrücke
29 Schnittstelle zu Betriebssystem und Laufzeitumgebung
30 Kommandozeilenparameter
31 Dateisystem
32 Parallele Programmierung
33 Datenspeicherung
34 Netzwerkkommunikation
35 Debugging und Qualitätssicherung
36 Dokumentation
Teil V Weiterführende Themen
37 Anbindung an andere Programmiersprachen
38 Distribution von Python-Projekten
39 Grafische Benutzeroberflächen
40 Python als serverseitige Programmiersprache im WWW – ein Einstieg in Django
41 Wissenschaftliches Rechnen
42 Insiderwissen
43 Von Python 2 nach Python 3
A Anhang
Stichwortverzeichnis

Download:
- Beispielprogramme, ca. 464 KB

Jetzt Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Python 3 von Johannes Ernesti, Peter Kaiser
Das umfassende Handbuch
Buch: Python 3

Python 3
Pfeil 34 Netzwerkkommunikation
Pfeil 34.1 Socket API
Pfeil 34.1.1 Client-Server-Systeme
Pfeil 34.1.2 UDP
Pfeil 34.1.3 TCP
Pfeil 34.1.4 Blockierende und nicht-blockierende Sockets
Pfeil 34.1.5 Erzeugen eines Sockets
Pfeil 34.1.6 Die Socket-Klasse
Pfeil 34.1.7 Netzwerk-Byte-Order
Pfeil 34.1.8 Multiplexende Server – selectors
Pfeil 34.1.9 Objektorientierte Serverentwicklung – socketserver
Pfeil 34.2 URLs – urllib
Pfeil 34.2.1 Zugriff auf entfernte Ressourcen – urllib.request
Pfeil 34.2.2 Einlesen und Verarbeiten von URLs – urllib.parse
Pfeil 34.3 FTP – ftplib
Pfeil 34.3.1 Mit einem FTP-Server verbinden
Pfeil 34.3.2 FTP-Kommandos ausführen
Pfeil 34.3.3 Mit Dateien und Verzeichnissen arbeiten
Pfeil 34.3.4 Übertragen von Dateien
Pfeil 34.4 E‐Mail
Pfeil 34.4.1 SMTP – smtplib
Pfeil 34.4.2 POP3 – poplib
Pfeil 34.4.3 IMAP4 – imaplib
Pfeil 34.4.4 Erstellen komplexer E‐Mails – email
Pfeil 34.5 Telnet – telnetlib
Pfeil 34.5.1 Die Klasse Telnet
Pfeil 34.5.2 Beispiel
Pfeil 34.6 XML-RPC
Pfeil 34.6.1 Der Server
Pfeil 34.6.2 Der Client
Pfeil 34.6.3 Multicall
Pfeil 34.6.4 Einschränkungen
 
Zum Seitenanfang

34    Netzwerkkommunikation Zur vorigen ÜberschriftZur nächsten Überschrift

Nachdem wir uns ausführlich mit der Speicherung von Daten in Dateien verschiedener Formate oder Datenbanken beschäftigt haben, folgt nun ein Kapitel, das sich mit einer weiteren interessanten Programmierdisziplin beschäftigt: mit der Netzwerkprogrammierung.

Grundsätzlich lässt sich das Themenfeld der Netzwerkkommunikation in mehrere sogenannte Protokollebenen (engl. layer) aufteilen. Abbildung 34.1 zeigt eine stark vereinfachte Version des OSI-Schichtenmodells[ 145 ](Das OSI-Modell wurde 1983 von der Internationalen Organisation für Normung (ISO) standardisiert und spezifiziert auch, was die Protokolle der einzelnen Schichten zu leisten haben. ), das die Hierarchie der verschiedenen Protokollebenen veranschaulicht.

Netzwerkprotokolle

Abbildung 34.1    Netzwerkprotokolle

Das rudimentärste Protokoll steht in der Grafik ganz unten. Dabei handelt es sich um die blanke Leitung, über die die Daten in Form elektrischer Impulse übermittelt werden. Darauf aufbauend existieren etwas abstraktere Protokolle wie Ethernet und IP. Doch der für Anwendungsprogrammierer eigentlich interessante Teil fängt erst oberhalb des IP-Protokolls an, nämlich bei den Transportprotokollen TCP und UDP. Beide Protokolle werden wir ausführlich im Zusammenhang mit Sockets im nächsten Abschnitt besprechen.

Die Protokolle, die auf TCP aufbauen, sind am weitesten abstrahiert und deshalb für uns ebenfalls interessant. In diesem Buch werden wir folgende Protokolle behandeln:

Protokoll Beschreibung Modul Abschnitt
UDP grundlegendes verbindungsloses Netzwerkprotokoll socket 34.1.2
TCP grundlegendes verbindungsorientiertes Netzwerkprotokoll socket 34.1.3
HTTP Übertragen von Textdateien, beispielsweise Webseiten urllib 34.2
FTP Dateiübertragung ftplib 34.3
SMTP Versenden von E‐Mails smtplib 34.4.1
POP3 Abholen von E‐Mails poplib 34.4.2
IMAP4 Abholen und Verwalten von E‐Mails imaplib 34.4.3
Telnet Terminalemulation telnetlib 34.5

Tabelle 34.1    Netzwerkprotokolle

Es gibt auch abstrakte Protokolle, die auf UDP aufbauen, beispielsweise NFS (Network File System). Wir werden in diesem Buch aber ausschließlich auf TCP basierende Protokolle behandeln, da diese die am häufigsten verwendeten sind.

Wir werden Ihnen im ersten Abschnitt dieses Kapitels zunächst eine grundlegende Einführung in das systemnahe Modul socket geben. Es lohnt sich, einen Blick in dieses Modul zu riskieren, denn es bietet viele Möglichkeiten der Netzwerkprogrammierung, die bei den anderen, abstrakteren Modulen verloren gehen. Außerdem lernen Sie den Komfort, den die abstrakten Schnittstellen bieten, erst wirklich zu schätzen, wenn Sie das socket-Modul kennengelernt haben.

Nachdem wir uns mit der Socket API beschäftigt haben, folgen einige spezielle Module, die beispielsweise mit bestimmten Protokollen wie HTTP oder FTP umgehen können.

 
Zum Seitenanfang

34.1    Socket API Zur vorigen ÜberschriftZur nächsten Überschrift

Das Modul socket der Standardbibliothek bietet grundlegende Funktionalität zur Netzwerkkommunikation. Es bildet dabei die standardisierte Socket API ab, die so oder in ähnlicher Form auch für viele andere Programmiersprachen implementiert ist.

Hinter der Socket API steht die Idee, dass das Programm, das Daten über die Netzwerkschnittstelle senden oder empfangen möchte, dies beim Betriebssystem anmeldet und von diesem einen sogenannten Socket (dt. »Steckdose«) bekommt. Über diesen Socket kann das Programm jetzt eine Netzwerkverbindung zu einem anderen Socket aufbauen. Dabei spielt es keine Rolle, ob sich der Ziel-Socket auf demselben Rechner, einem Rechner im lokalen Netzwerk oder einem Rechner im Internet befindet.

Zunächst ein paar Worte dazu, wie ein Rechner in der komplexen Welt eines Netzwerks adressiert werden kann. Jeder Rechner besitzt in einem Netzwerk, auch dem Internet, eine eindeutige IP-Adresse, über die er angesprochen werden kann. Eine IP-Adresse ist ein String der folgenden Struktur:

"192.168.1.23"

Dabei repräsentiert jeder der vier Zahlenwerte ein Byte und kann somit zwischen 0 und 255 liegen. In diesem Fall handelt es sich um eine IP-Adresse eines lokalen Netzwerks, was an der Anfangssequenz 192.168 zu erkennen ist.

Damit ist es jedoch noch nicht getan, denn auf einem einzelnen Rechner können mehrere Programme laufen, die gleichzeitig Daten über die Netzwerkschnittstelle senden und empfangen möchten. Aus diesem Grund wird eine Netzwerkverbindung zusätzlich an einen sogenannten Port gebunden. Der Port ermöglicht es, ein bestimmtes Programm anzusprechen, das auf einem Rechner mit einer bestimmten IP-Adresse läuft.

Bei einem Port handelt es sich um eine 16-Bit-Zahl – grundsätzlich sind also 65.535 verschiedene Ports verfügbar. Allerdings sind viele dieser Ports für Protokolle und Anwendungen registriert und sollten nicht verwendet werden. Beispielsweise sind für HTTP- und FTP-Server die Ports 80 bzw. 21 registriert. Grundsätzlich können Sie Ports ab 49152 bedenkenlos verwenden.

Beachten Sie, dass beispielsweise eine Firewall oder ein Router bestimmte Ports blockieren können. Sollten Sie also auf Ihrem Rechner einen Server betreiben wollen, zu dem sich Clients über einen bestimmten Port verbinden können, müssen Sie diesen Port gegebenenfalls mit der entsprechenden Software freischalten.

 
Zum Seitenanfang

34.1.1    Client-Server-Systeme Zur vorigen ÜberschriftZur nächsten Überschrift

Die beiden Kommunikationspartner einer Netzwerkkommunikation haben in der Regel verschiedene Aufgaben. So existiert zum einen ein Server (dt. »Diener«), der bestimmte Dienstleistungen anbietet, und zum anderen ein Client (dt. »Kunde«), der diese Dienstleistungen in Anspruch nimmt.

Ein Server ist unter einer bekannten Adresse im Netzwerk erreichbar und operiert passiv, das heißt, er wartet auf eingehende Verbindungen. Sobald eine Verbindungsanfrage eines Clients eintrifft, wird, sofern der Server die Anfrage akzeptiert, ein neuer Socket erzeugt, über den die Kommunikation mit diesem speziellen Client läuft. Wir werden uns zunächst mit seriellen Servern befassen, das sind Server, bei denen die Kommunikation mit dem vorherigen Client abgeschlossen sein muss, bevor eine neue Verbindung akzeptiert werden kann. Dem stehen die Konzepte der parallelen Server und der multiplexenden Server gegenüber, auf die wir auch noch zu sprechen kommen werden.

Der Client stellt den aktiven Kommunikationspartner dar. Das heißt, er sendet eine Verbindungsanfrage an den Server und nimmt dann aktiv dessen Dienstleistungen in Anspruch.

Die Stadien, in denen sich ein serieller Server und ein Client vor, während und nach der Kommunikation befinden, verdeutlicht das Flussdiagramm in Abbildung 34.2. Sie können es als eine Art Bauplan für einen seriellen Server und den dazugehörigen Client auffassen.

Zunächst wird im Serverprogramm der sogenannte Verbindungssocket erzeugt. Das ist ein Socket, der ausschließlich dazu gedacht ist, auf eingehende Verbindungen zu horchen und diese gegebenenfalls zu akzeptieren. Über den Verbindungssocket läuft keine Kommunikation. Durch Aufruf der Methoden bind und listen wird der Verbindungssocket an eine Netzwerkadresse gebunden und dazu instruiert, nach einkommenden Verbindungsanfragen zu lauschen.

Nachdem eine Verbindungsanfrage eingetroffen ist und mit accept akzeptiert wurde, wird ein neuer Socket, der sogenannte Kommunikationssocket, erzeugt. Über einen solchen Kommunikationssocket wird die vollständige Kommunikation zwischen Server und Client über Methoden wie send oder recv abgewickelt. Ein Kommunikationssocket ist immer nur für einen verbundenen Client zuständig.

Sobald die Kommunikation beendet ist, wird das Kommunikationsobjekt geschlossen und eventuell eine weitere Verbindung eingegangen. Verbindungsanfragen, die nicht sofort akzeptiert werden, sind keineswegs verloren, sondern werden gepuffert. Sie befinden sich in der sogenannten Queue und können nacheinander abgearbeitet werden. Zum Schluss wird auch der Verbindungssocket geschlossen.

Die Struktur des Clients ist vergleichsweise einfach. So gibt es beispielsweise nur einen Kommunikationssocket, über den mithilfe der Methode connect eine Verbindungsanfrage an einen bestimmten Server gesendet werden kann. Danach erfolgt, ähnlich wie beim Server, die tatsächliche Kommunikation über Methoden wie send oder recv. Nach dem Ende der Kommunikation wird der Verbindungssocket geschlossen.

Das Client-Server-Modell

Abbildung 34.2    Das Client-Server-Modell

Grundsätzlich kann für die Datenübertragung zwischen Server und Client aus zwei verfügbaren Netzwerkprotokollen gewählt werden: UDP und TCP. In den folgenden beiden Abschnitten sollen kleine Beispielserver und -clients für beide dieser Protokolle implementiert werden.

Beachten Sie, dass sich das hier vorgestellte Flussdiagramm auf das verbindungsbehaftete und üblichere TCP-Protokoll bezieht. Die Handhabung des verbindungslosen UDP-Protokolls unterscheidet sich davon in einigen wesentlichen Punkten. Näheres dazu erfahren Sie im folgenden Abschnitt.

 
Zum Seitenanfang

34.1.2    UDP Zur vorigen ÜberschriftZur nächsten Überschrift

Das Netzwerkprotokoll UDP (User Datagram Protocol) wurde 1977 als Alternative zu TCP für die Übertragung menschlicher Sprache entwickelt. Charakteristisch ist, dass UDP verbindungslos und nicht zuverlässig ist. Diese beiden Begriffe gehen miteinander einher und bedeuten zum einen, dass keine explizite Verbindung zwischen den Kommunikationspartnern aufgebaut wird, und zum anderen, dass UDP weder garantiert, dass gesendete Pakete in der Reihenfolge ankommen, in der sie gesendet wurden, noch dass sie überhaupt ankommen. Aufgrund dieser Einschränkungen können mit UDP jedoch vergleichsweise schnelle Übertragungen stattfinden, da beispielsweise keine Pakete neu angefordert oder gepuffert werden müssen.

Damit eignet sich UDP insbesondere für Multimedia-Anwendungen wie VoIP, Audio- oder Videostreaming, bei denen es auf eine schnelle Übertragung der Daten ankommt und kleinere Übertragungsfehler toleriert werden können.

Das im Folgenden entwickelte Beispielprojekt besteht aus einem Server- und einem Clientprogramm. Der Client schickt eine Textnachricht per UDP an eine bestimmte Adresse. Das dort laufende Serverprogramm nimmt die Nachricht entgegen und zeigt sie an. Betrachten wir zunächst den Quellcode des Clients:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ip = input("IP-Adresse: ")
nachricht = input("Nachricht: ")
s.sendto(nachricht.encode(), (ip, 50000))
s.close()

Zunächst erzeugt der Aufruf der Funktion socket eine Socket-Instanz. Dabei können zwei Parameter übergeben werden: zum einen der zu verwendende Adresstyp und zum anderen das zu verwendende Netzwerkprotokoll. Die Konstanten AF_INET und SOCK_DGRAM stehen dabei für Internet/IPv4 und UDP.

Danach werden zwei Angaben vom Benutzer eingelesen: die IP-Adresse, an die die Nachricht geschickt werden soll, und die Nachricht selbst.

Zum Schluss wird die Nachricht unter Verwendung der Socket-Methode sendto zur angegebenen IP-Adresse geschickt, wozu der Port 50000 verwendet wird. Da die zu versendende Nachricht nicht unbedingt ein String sein muss, sondern beliebige Byte-Folgen enthalten darf, wird an der Schnittstelle von s.sendto kein String erwartet, sondern eine bytes- oder bytearray-Instanz. Im Beispiel muss der eingelesene String also zuvor codiert werden.

Das Clientprogramm allein ist so gut wie wertlos, solange es kein dazu passendes Serverprogramm auf der anderen Seite gibt, das die Nachricht entgegennehmen und verwerten kann. Beachten Sie, dass UDP verbindungslos ist und sich die Implementation daher etwas vom Flussdiagramm eines Servers aus Abschnitt 34.1.1, »Client-Server-Systeme«, unterscheidet. Der Quelltext des Servers sieht folgendermaßen aus:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.bind(("", 50000))
while True:
daten, addr = s.recvfrom(1024)
print("[{}] {}".format(addr[0], daten.decode()))
finally:
s.close()

Auch hier wird zunächst eine Socket-Instanz erstellt. In der darauffolgenden try/ finally-Anweisung wird dieser Socket durch Aufruf der Methode bind an eine Adresse gebunden. Beachten Sie, dass diese Methode ein Adressobjekt als Parameter übergeben bekommt. Immer wenn im Zusammenhang mit Sockets von einem Adressobjekt die Rede ist, ist damit ein Tupel mit zwei Elementen gemeint: einer IP-Adresse als String und einer Portnummer als ganzer Zahl.

Das Binden eines Sockets an eine Adresse legt fest, über welche interne Schnittstelle der Socket Pakete empfangen kann. Wenn keine IP-Adresse angegeben wurde, bedeutet dies, dass Pakete über alle dem Server zugeordneten Adressen empfangen werden können, beispielsweise auch über 127.0.0.1 oder localhost.

Nachdem der Socket an eine Adresse gebunden wurde, können Daten empfangen werden. Dazu wird die Methode recvfrom (für receive from) in einer Endlosschleife aufgerufen. Die Methode wartet so lange, bis ein Paket eingegangen ist, und gibt die gelesenen Daten mitsamt den Absenderinformationen als Tupel zurück. Beachten Sie, dass die empfangenen Daten ebenfalls in Form einer bytes-Instanz zurückgegeben werden.

Der Parameter von recvfrom kennzeichnet die maximale Paketgröße und sollte eine Zweierpotenz sein.

An dieser Stelle wird auch der Sinn der try/finally-Anweisung deutlich: Das Programm wartet in einer Endlosschleife auf eintreffende Pakete und kann daher nur mit einem Programmabbruch durch Tastenkombination, also durch eine KeyboardInterrupt-Exception, beendet werden. In einem solchen Fall muss der Socket trotzdem noch mit close geschlossen werden.

 
Zum Seitenanfang

34.1.3    TCP Zur vorigen ÜberschriftZur nächsten Überschrift

TCP (Transmission Control Protocol) ist kein Konkurrenzprodukt zu UDP, sondern füllt mit seinen Möglichkeiten die Lücken auf, die UDP offen lässt. So ist TCP vor allem verbindungsorientiert und zuverlässig. Verbindungsorientiert bedeutet, dass nicht wie bei UDP einfach Datenpakete an IP-Adressen geschickt werden, sondern dass zuvor eine Verbindung aufgebaut wird und auf Basis dieser Verbindung weitere Operationen durchgeführt werden. Zuverlässig bedeutet, dass es mit TCP nicht wie bei UDP vorkommen kann, dass Pakete verloren gehen, fehlerhaft oder in falscher Reihenfolge ankommen. Solche Vorkommnisse korrigiert das TCP-Protokoll automatisch, indem es beispielsweise unvollständige oder fehlerhafte Pakete neu anfordert.

Aus diesem Grund ist TCP zumeist die erste Wahl, wenn es um eine Netzwerkschnittstelle geht. Bedenken Sie aber unbedingt, dass jedes Paket, das neu angefordert werden muss, Zeit kostet und die Latenz der Verbindung somit steigen kann. Außerdem sind fehlerhafte Übertragungen in einem LAN äußerst selten, weswegen Sie gerade dort die Performance von UDP und die Verbindungsqualität von TCP gegeneinander abwägen sollten.

Im Folgenden wird die Verwendung von TCP anhand eines kleinen Beispielprojekts erläutert: Es soll ein rudimentäres Chat-Programm entstehen, bei dem der Client eine Nachricht an den Server sendet, auf die der Server wieder antworten kann. Die Kommunikation soll also immer abwechselnd erfolgen. Der Quelltext des Servers sieht folgendermaßen aus:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("", 50000))
s.listen(1)
try:
while True:
komm, addr = s.accept()
while True:
data = komm.recv(1024)
if not data:
komm.close()
break
print("[{}] {}".format(addr[0], data.decode()))
nachricht = input("Antwort: ")
komm.send(nachricht.encode())
finally:
s.close()

Bei der Erzeugung des Verbindungssockets unterscheidet sich TCP von UDP nur in den zu übergebenden Werten. In diesem Fall wird AF_INET für das IPv4-Protokoll und SOCK_STREAM für die Verwendung von TCP übergeben. Damit ist allerdings nur der Socket in seiner Rohform instanziiert. Auch bei TCP muss der Socket an eine IP-Adresse und einen Port gebunden werden. Beachten Sie, dass bind ein Adressobjekt als Parameter erwartet, die Angaben von IP-Adresse und Port also noch in ein Tupel gefasst sind. Auch hier werden wieder alle IP-Adressen des Servers genutzt.

Danach wird der Server durch Aufruf der Methode listen in den passiven Modus geschaltet und instruiert, nach Verbindungsanfragen zu horchen. Beachten Sie, dass diese Methode noch keine Verbindung herstellt. Der übergebene Parameter bestimmt die maximale Anzahl von zu puffernden Verbindungsversuchen und sollte mindestens 1 sein.

In der darauffolgenden Endlosschleife wartet die aufgerufene Methode accept des Verbindungssockets auf eine eingehende Verbindungsanfrage und akzeptiert diese. Zurückgegeben wird ein Tupel, dessen erstes Element der Kommunikationssocket ist, der zur Kommunikation mit dem verbundenen Client verwendet werden kann. Das zweite Element des Tupels ist das Adressobjekt des Verbindungspartners.

Nachdem eine Verbindung hergestellt wurde, wird eine zweite Endlosschleife eingeleitet, deren Schleifenkörper im Prinzip aus zwei Teilen besteht: Zunächst wird immer eine Nachricht per komm.recv vom Verbindungspartner erwartet und ausgegeben. Sollte von komm.recv ein leerer String zurückgegeben werden, bedeutet dies, dass der Verbindungspartner die Verbindung beendet hat. In einem solchen Fall wird die innere Schleife abgebrochen. Wenn eine wirkliche Nachricht angekommen ist, erlaubt es der Server dem Benutzer, eine Antwort einzugeben, und verschickt diese per komm.send.

Jetzt soll der Quelltext des Clients besprochen werden:

import socket
ip = input("IP-Adresse: ")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, 50000))
try:
while True:
nachricht = input("Nachricht: ")
s.send(nachricht.encode())
antwort = s.recv(1024)
print("[{}] {}".format(ip, antwort.decode()))
finally:
s.close()

Auf der Clientseite wird der instanziierte Socket s durch Aufruf der Methode connect mit dem Verbindungspartner verbunden. Die Methode connect verschickt genau die Verbindungsanfrage, die beim Server durch accept akzeptiert werden kann. Wenn die Verbindung abgelehnt wurde, wird eine Exception geworfen.

Die folgende Endlosschleife funktioniert ähnlich wie die des Servers, mit dem Unterschied, dass zuerst eine Nachricht eingegeben und abgeschickt und danach auf eine Antwort des Servers gewartet wird. Damit sind Client und Server in einen Rhythmus gebracht, bei dem der Server immer dann auf eine Nachricht wartet, wenn beim Client eine eingegeben wird und umgekehrt.

Betrachten Sie es als Herausforderung, Client und Server beispielsweise durch Threads[ 146 ](siehe Kapitel 32, »Parallele Programmierung« ) zu einem brauchbaren Chat-Programm zu erweitern. Das könnte so aussehen, dass ein Thread jeweils s.recv abhört und eingehende Nachrichten anzeigt und ein zweiter Thread es dem Benutzer ermöglicht, Nachrichten per input einzugeben und zu verschicken.

 
Zum Seitenanfang

34.1.4    Blockierende und nicht-blockierende Sockets Zur vorigen ÜberschriftZur nächsten Überschrift

Wenn ein Socket erstellt wird, befindet er sich standardmäßig im sogenannten blockierenden Modus (engl. blocking mode). Das bedeutet, dass alle Methodenaufrufe warten, bis die von ihnen angestoßene Operation durchgeführt wurde. So blockiert ein Aufruf der Methode recv eines Sockets so lange das komplette Programm, bis tatsächlich Daten eingegangen sind und aus dem internen Puffer des Sockets gelesen werden können.

In vielen Fällen ist dieses Verhalten durchaus gewünscht, doch möchte man bei einem Programm, in dem viele verbundene Sockets verwaltet werden, beispielsweise nicht, dass einer dieser Sockets mit seiner recv-Methode das komplette Programm blockiert, nur weil noch keine Daten eingegangen sind, während an einem anderen Socket Daten zum Lesen bereitstehen. Um solche Probleme zu umgehen, lässt sich der Socket in den nicht-blockierenden Modus (engl. non-blocking mode) versetzen. Dies wirkt sich folgendermaßen auf diverse Socket-Operationen aus:

  • Die Methoden recv und recvfrom des Socket-Objekts geben nur noch ankommende Daten zurück, wenn sich diese bereits im internen Puffer des Sockets befinden. Sobald die Methode auf weitere Daten warten muss, wirft sie eine OSError-Exception und gibt damit den Kontrollfluss wieder an das Programm ab.
  • Die Methoden send und sendto versenden die angegebenen Daten nur, wenn sie direkt in den Ausgangspuffer des Sockets geschrieben werden können. Gelegentlich kommt es vor, dass dieser Puffer voll ist und send bzw. sendto zu warten hätten, bis der Puffer weitere Daten aufnehmen kann. In einem solchen Fall wird im nicht-blockierenden Modus eine OSError-Exception geworfen und der Kontrollfluss damit an das Programm zurückgegeben.
  • Die Methode connect sendet eine Verbindungsanfrage an den Ziel-Socket und wartet nicht, bis diese Verbindung zustande kommt. Durch mehrmaligen Aufruf von connect lässt sich feststellen, ob die Operation immer noch durchgeführt wird. Wenn connect aufgerufen wird und die Verbindungsanfrage noch läuft, wird eine OSError-Exception mit der Fehlermeldung »Operation now in progress« geworfen.
    Alternativ kann im nicht-blockierenden Modus die Methode connect_ex für Verbindungsanfragen verwendet werden. Diese wirft keine OSError-Exception, sondern zeigt eine erfolgreiche Verbindung mit einem Rückgabewert von 0 an. Bei Fehlern, die bei der Verbindung auftreten, wirft auch connect_ex eine Exception.

Ein Socket lässt sich durch Aufruf seiner Methode setblocking in den nicht-blockierenden Zustand versetzen:

s.setblocking(False)

In diesem Fall würden sich Methodenaufrufe des Sockets s wie oben beschrieben verhalten. Der Parameter True versetzt den Socket wieder in den ursprünglichen blockierenden Modus.

Socket-Operationen werden im Falle des blockierenden Modus auch synchrone Operationen und im Falle des nicht-blockierenden Modus asynchrone Operationen genannt.

[»]  Hinweis

Es ist möglich, auch während des Betriebs zwischen dem blockierenden und dem nicht-blockierenden Modus eines Sockets umzuschalten. So können Sie beispielsweise die Methode connect blockierend und anschließend die Methode read nicht-blockierend verwenden.

 
Zum Seitenanfang

34.1.5    Erzeugen eines Sockets Zur vorigen ÜberschriftZur nächsten Überschrift

Dieser Abschnitt behandelt die zwei wesentlichen Wege, einen Socket zu erzeugen. Neben der bereits in Beispielen verwendeten Funktion socket ist dies die Funktion create_connection.

socket([family, type])

Diese Funktion erzeugt einen neuen Socket. Der erste Parameter family kennzeichnet dabei die Adressfamilie und sollte entweder socket.AF_INET für den IPv4-Namensraum oder socket.AF_INET6 für den IPv6-Namensraum sein.

[»]  Hinweis

IPv6 (Internet Protocol version 6) ist der Nachfolger des weitverbreiteten IPv4-Protokolls, dessen Adressraum inzwischen nahezu erschöpft ist.

Python bietet eine grundlegende IPv6-Unterstützung, für die es in der Regel ausreicht, bei der Socket-Instanziierung den Wert AF_INET6 anstelle von AF_INET zu übergeben. Einige Funktionen des socket-Moduls sind inkompatibel zu IPv6, was bei der Besprechung der jeweiligen Funktion erwähnt wird.

IPv6 muss vom Betriebssystem unterstützt werden. Ob dies der Fall ist, können Sie anhand der booleschen Variablen socket.has_ipv6 ablesen.

Der zweite Parameter type kennzeichnet das zu verwendende Netzwerkprotokoll und sollte entweder socket.SOCK_STREAM für TCP oder socket.SOCK_DGRAM für UDP sein.

create_connection(address, [timeout, source_address])

Diese Funktion verbindet sich über TCP mit der über das Adressobjekt address identifizierten Gegenstelle und gibt das zum Verbindungsaufbau verwendete Socket-Objekt zurück. Für den Parameter timeout kann ein Timeout-Wert übergeben werden, der beim Verbindungsaufbau berücksichtigt wird.

Wenn für den Parameter source_address ein Adressobjekt übergeben wird, wird das Socket-Objekt vor dem Verbindungsaufbau an diese Adresse gebunden. Der Aufruf

import socket
s = socket.create_connection((ip1,port1), timeout, (ip2, port2))

ist damit äquivalent zu:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
s.bind((ip2, port2))
s.connect((ip1, port1))

Die Funktion create_connection kann im Zusammenhang mit der with-Anweisung verwendet werden:

with socket.create_connection(("ip", port)) as s:
s.send(b"Hallo Welt")
 
Zum Seitenanfang

34.1.6    Die Socket-Klasse Zur vorigen ÜberschriftZur nächsten Überschrift

Nachdem durch Aufruf der Funktionen socket oder create_connection eine neue Instanz der Klasse Socket erzeugt wurde, stellt diese weitere Funktionen bereit, um sich mit einem zweiten Socket zu verbinden oder Daten an den Verbindungspartner zu übermitteln. Die wichtigsten Methoden der Socket-Klasse werden Tabelle 34.2 beschrieben.

Beachten Sie, dass sich das Verhalten der Methoden im blockierenden und im nicht-blockierenden Modus unterscheidet. Näheres dazu finden Sie in Abschnitt 34.1.4, »Blockierende und nicht-blockierende Sockets«.

Methode Beschreibung Protokoll
accept() Wartet auf eine eingehende Verbindungsanfrage und akzeptiert diese. TCP
bind(address) Bindet den Socket an die Adresse address.
close() Schließt den Socket. Das bedeutet, dass keine Daten mehr über ihn gesendet oder empfangen werden können.
connect(address)
connect_ex(address)
Verbindet zu einem Server mit der Adresse address. TCP
getpeername() Gibt das Adressobjekt des verbundenen Sockets zurück. Das Adressobjekt ist ein Tupel aus IP-Adresse und Portnummer. TCP
getsockname() Gibt das Adressobjekt des Sockets selbst zurück.
listen() Lässt den Socket auf Verbindungsanfragen achten. TCP
recv(bufsize) Liest maximal bufsize Byte der beim Socket eingegangenen Daten und gibt sie als String zurück. TCP
recv_into(buffer,
[nbytes])
Wie recv, schreibt die gelesenen Daten aber in buffer, anstatt sie als bytes-String zurückzugeben. Für buffer kann beispielsweise eine bytearray-Instanz übergeben werden. TCP
recvfrom(bufsize) Wie recv, gibt aber zusätzlich das Adressobjekt des Verbindungspartners zurück. UDP
recvfrom_into(buffer,
[nbytes])
Wie recvfrom, schreibt die gelesenen Daten aber in buffer, anstatt sie als bytes-String zurückzugeben. Für buffer kann beispielsweise eine bytearray-Instanz übergeben werden. UDP
send(bytes)
sendall(bytes)
Sendet die Daten bytes an den verbundenen Socket. TCP
sendto(bytes,
address)
Sendet die Daten bytes an einen Socket mit dem Adressobjekt address. UDP
setblocking(flag) Versetzt den Socket in den blockierenden bzw. nicht-blockierenden Modus.
settimeout(value)
gettimeout()
Schreibt bzw. liest den Timeout-Wert des Sockets.

Tabelle 34.2    Methoden der Socket-Klasse

accept()

Diese Methode wartet auf eine eingehende Verbindungsanfrage und akzeptiert diese. Die Socket-Instanz muss zuvor durch Aufruf der Methode bind an eine bestimmte Adresse und einen Port gebunden worden sein und Verbindungsanfragen erwarten. Letzteres geschieht durch Aufruf der Methode listen.

Die Methode accept gibt ein Tupel zurück, dessen erstes Element eine neue Socket-Instanz, auch Connection-Objekt genannt, ist, über die die Kommunikation mit dem Verbindungspartner erfolgen kann. Das zweite Element des Tupels ist ein weiteres Tupel, das IP-Adresse und Port des verbundenen Sockets enthält.

bind(address)

Diese Methode bindet den Socket an die Adresse address. Der Parameter address muss ein Tupel der Form sein, wie es accept zurückgibt.

Nachdem ein Socket an eine bestimmte Adresse gebunden wurde, kann er, im Falle von TCP, in den passiven Modus geschaltet werden und auf Verbindungsanfragen warten oder, im Falle von UDP, direkt Datenpakete empfangen.

connect(address), connect_ex(address)

Diese Methode verbindet zu einem Server mit der Adresse address. Beachten Sie, dass dort ein Socket existieren muss, der auf dem gleichen Port auf Verbindungsanfragen wartet, damit die Verbindung zustande kommen kann. Der Parameter address muss im Falle des IPv4-Protokolls ein Tupel sein, das aus der IP-Adresse und der Portnummer besteht.

Die Methode connect_ex unterscheidet sich von connect nur darin, dass im nicht-blockierenden Modus keine Exception geworfen wird, wenn die Verbindung nicht sofort zustande kommt. Der Verbindungsstatus wird über einen ganzzahligen Rückgabewert angezeigt. Ein Rückgabewert von 0 bedeutet, dass der Verbindungsversuch erfolgreich durchgeführt wurde.

Beachten Sie, dass bei echten Fehlern, die beim Verbindungsversuch auftreten, weiterhin Exceptions geworfen werden, beispielsweise wenn der Ziel-Socket nicht erreicht werden konnte.

listen(backlog)

Diese Methode versetzt einen Server-Socket in den sogenannten Listen-Modus, das heißt, der Socket achtet auf Sockets, die sich mit ihm verbinden wollen. Nachdem diese Methode aufgerufen wurde, können eingehende Verbindungswünsche mit accept akzeptiert werden.

Der Parameter backlog legt die maximale Anzahl an gepufferten Verbindungsanfragen fest und sollte mindestens 1 sein. Den größtmöglichen Wert für backlog legt das Betriebssystem fest, meistens liegt er bei 5.

send(bytes), sendall(bytes)

Diese Methode sendet den bytes-String bytes zum verbundenen Socket. Die Anzahl der gesendeten Bytes wird zurückgegeben. Beachten Sie, dass unter Umständen die Daten nicht vollständig gesendet wurden. In einem solchen Fall ist die Anwendung dafür verantwortlich, die verbleibenden Daten erneut zu senden.

Im Gegensatz dazu versucht die Methode sendall so lange, die Daten zu senden, bis entweder der vollständige Datensatz versendet wurde oder ein Fehler aufgetreten ist. Im Fehlerfall wird eine entsprechende Exception geworfen.

settimeout(value), gettimeout()

Diese Methode setzt einen Timeout-Wert für diesen Socket. Dieser Wert bestimmt im blockierenden Modus, wie lange auf das Eintreffen bzw. Versenden von Daten gewartet werden soll. Dabei können Sie für value die Anzahl an Sekunden in Form einer Gleitkommazahl oder None übergeben.

Über die Methode gettimeout kann der Timeout-Wert ausgelesen werden.

Wenn ein Aufruf von beispielsweise send oder recv die maximale Wartezeit überschreitet, wird eine socket.timeout-Exception geworfen.

 
Zum Seitenanfang

34.1.7    Netzwerk-Byte-Order Zur vorigen ÜberschriftZur nächsten Überschrift

Das Schöne an standardisierten Protokollen wie TCP oder UDP ist, dass Computer verschiedenster Bauart eine gemeinsame Schnittstelle haben, über die sie miteinander kommunizieren können. Allerdings hören diese Gemeinsamkeiten hinter der Schnittstelle unter Umständen wieder auf. So ist beispielsweise die Byte-Order ein signifikanter Unterschied zwischen diversen Systemen. Diese Byte-Order legt die Speicherreihenfolge von Zahlen fest, die mehr als ein Byte Speicher benötigen.

Bei der Übertragung von Binärdaten führt es zu Problemen, wenn diese ohne Konvertierung zwischen zwei Systemen mit verschiedener Byte-Order ausgetauscht werden. Das Protokoll TCP garantiert dabei nur, dass die gesendeten Bytes in der Reihenfolge ankommen, in der sie abgeschickt wurden.

Solange Sie sich bei der Netzwerkkommunikation auf reine ASCII-Strings beschränken, können keine Probleme auftreten, da ASCII-Zeichen nie mehr als ein Byte Speicher benötigen. Außerdem sind Verbindungen zwischen zwei Computern derselben Plattform problemlos. So können beispielsweise Binärdaten zwischen zwei x86er-PCs übertragen werden, ohne Probleme befürchten zu müssen.

Allerdings möchte man bei einer Netzwerkverbindung in der Regel Daten übertragen, ohne sich über die Plattform des verbundenen Rechners Gedanken zu machen. Dazu hat man die sogenannte Netzwerk-Byte-Order definiert. Das ist die Byte-Order, die Sie für Binärdaten im Netzwerk verwenden müssen. Um diese Netzwerk-Byte-Order sinnvoll umzusetzen, enthält das Modul socket vier Funktionen, die entweder Daten von der Host-Byte-Order in die Netzwerk-Byte-Order (»hton«) oder umgekehrt (»ntoh«) konvertieren.

Tabelle 34.3 listet diese Funktionen auf und erläutert ihre Bedeutung:

Alias Bedeutung
ntohl(x) Konvertiert eine 32-Bit-Zahl von der Netzwerk- in die Host-Byte-Order.
ntohs(x) Konvertiert eine 16-Bit-Zahl von der Netzwerk- in die Host-Byte-Order.
htonl(x) Konvertiert eine 32-Bit-Zahl von der Host- in die Netzwerk-Byte-Order.
htons(x) Konvertiert eine 16-Bit-Zahl von der Host- in die Netzwerk-Byte-Order.

Tabelle 34.3    Konvertierung von Binärdaten

Der Aufruf dieser Funktionen ist möglicherweise überflüssig, wenn das entsprechende System bereits die Netzwerk-Byte-Order verwendet. Der gebräuchliche x86er-PC verwendet diese übrigens nicht.

 
Zum Seitenanfang

34.1.8    Multiplexende Server – selectors Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Server ist in den meisten Fällen nicht dazu gedacht, immer nur einen Client zu bedienen, wie es in den bisherigen Beispielen vereinfacht angenommen wurde. In der Regel muss ein Server eine ganze Reihe von verbundenen Clients verwalten, die sich in verschiedenen Phasen der Kommunikation befinden. Es stellt sich die Frage, wie so etwas sinnvoll in einem Prozess, also ohne den Einsatz von Threads, durchgeführt werden kann.

Selbstverständlich könnte man alle verwendeten Sockets in den nicht-blockierenden Modus schalten und die Verwaltung selbst in die Hand nehmen. Das ist aber nur auf den ersten Blick eine Lösung, denn der blockierende Modus besitzt einen unschätzbaren Vorteil: Ein blockierender Socket veranlasst, dass das Programm bei einer Netzwerkoperation so lange schlafen gelegt wird, bis die Operation durchgeführt werden kann. Auf diese Weise kann die Prozessorauslastung reduziert werden.

Im Gegensatz dazu müssten wir beim Einsatz nicht-blockierender Sockets in einer Schleife ständig über alle verbundenen Sockets iterieren und prüfen, ob sich etwas getan hat, also ob beispielsweise Daten zum Auslesen bereitstehen. Dieser Ansatz, auch Busy Waiting genannt, ermöglicht uns zwar das quasi-parallele Auslesen mehrerer Sockets, das Programm lastet den Prozessor aber wesentlich mehr aus, da es über den gesamten Zeitraum aktiv ist.

Das Modul selectors ermöglicht es, im gleichen Prozess mehrere Sockets zu verwalten und auf bei diesen Sockets eingehende Ereignisse zu warten. Ein solcher Server wird multiplexender Server genannt. Das Modul definiert die Klasse DefaultSelector, bei der Sockets registriert werden können.

Im folgenden Beispiel wird ein Server geschrieben, der Verbindungen von beliebig vielen Clients akzeptiert und verwaltet. Diese Clients sollen dazu in der Lage sein, dem Server mehrere Nachrichten zu schicken, die von diesem dann am Bildschirm angezeigt werden. Aus Gründen der Einfachheit verzichten wir auf eine Antwortmöglichkeit des Servers.

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("", 50000))
server.setblocking(False)
server.listen(1)

selector = selectors.DefaultSelector()
selector.register(server, selectors.EVENT_READ, accept)

Zunächst erzeugen wir in gewohnter Weise einen nicht-blockierenden Server-Socket. Dieser wird dann bei einer DefaultSelector-Instanz gemeinsam mit einem Ereignis registriert. Mögliche Ereignisse sind EVENT_READ bzw. EVENT_WRITE, die auftreten, wenn Daten bei einem Socket zum Lesen bereitliegen bzw. wenn ein Socket bereit ist zum Schreiben. Als dritter Parameter wird der Methode register ein Callback-Handler übergeben, den wir im Falle des Ereignisses aufrufen werden. In diesem Fall verknüpfen wir ein READ-Ereignis beim Verbindungssocket mit der Handler-Funktion accept, die eine eingehende Verbindung akzeptieren soll:

def accept(selector, sock):
connection, addr = sock.accept()
connection.setblocking(False)
selector.register(connection, selectors.EVENT_READ, message)

Der Callback-Handler accept bekommt den Selector und den Socket übergeben, bei dem das Ereignis aufgetreten ist, in diesem Fall handelt es sich dabei um den Verbindungssocket. Er akzeptiert daraufhin die Verbindung und knüpft seinerseits das Ereignis beim entstandenen Client-Socket eingehender Daten an die Handler-Funktion message:

def message(selector, client):
nachricht = client.recv(1024)
ip = client.getpeername()[0]
if nachricht:
print("[{}] {}".format(ip, nachricht.decode()))
else:
print("+++ Verbindung zu {} beendet".format(ip))
selector.unregister(client)
client.close()

Die Handler-Funktion message bekommt ebenfalls den Selector sowie den betreffenden Socket, in diesem Fall den Client-Socket, übergeben. Sie liest daraufhin die eingegangenen Daten und gibt sie als Nachricht auf dem Bildschirm aus. Aus Gründen der Einfachheit verzichten wir auf eine Antwortmöglichkeit des Servers.

Wenn die Verbindung seitens des Clients beendet wurde, gibt recv einen leeren String zurück. In diesem Fall müssen wir die Methode unregister des Selectors aufrufen, um diesen Client-Socket auszutragen. Danach kann die Verbindung geschlossen werden.

Zum Schluss muss der multiplexende Server noch in Betrieb genommen werden. Dazu rufen wir in einer Endlosschleife die Methode select des Selectors auf, die so lange blockiert, bis eines der registrierten Ereignisse aufgetreten ist:

while True:
for key, mask in selector.select():
key.data(selector, key.fileobj)

Im Falle eines aufgetretenen Ereignisses gibt select ein Tupel (key, mask) zurück. Über die Instanz key können wir auf die mit dem Ereignis verknüpften Daten zugreifen, in diesem Fall die entsprechende Handler-Funktion. Außerdem kann über key.fileobj auf den Socket zugegriffen werden, bei dem das Ereignis aufgetreten ist. Die Instanz mask spezifiziert, welches Ereignis konkret aufgetreten ist. Mithilfe des binären UND lassen sich hier die Ereignistypen EVENT_READ und EVENT_WRITE prüfen.

Der Vollständigkeit halber folgt hier noch der Quelltext des zu diesem Server passenden Clients:

import socket
ip = input("IP-Adresse: ")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, 50000))
try:
while True:
nachricht = input("Nachricht: ")
s.send(nachricht.encode())
finally:
s.close()

Dabei handelt es sich um reine Socket-Programmierung, wie wir sie bereits in den vorangegangenen Abschnitten behandelt haben. Der Client bemerkt abgesehen von eventuell auftretenden Latenzen nicht, ob er von einem seriellen oder einem multiplexenden Server bedient wird.

 
Zum Seitenanfang

34.1.9    Objektorientierte Serverentwicklung – socketserver Zur vorigen ÜberschriftZur nächsten Überschrift

Sie können sich vorstellen, dass die Implementierung eines komplexeren Servers unter Verwendung des Moduls socket schnell unübersichtlich und kompliziert werden kann. Aus diesem Grund enthält Pythons Standardbibliothek das Modul socketserver, das es erleichtern soll, einen Server zu schreiben, der in der Lage ist, mehrere Clients zu bedienen.

Im folgenden Beispiel soll der Chat-Server des vorangegangenen Abschnitts mit dem Modul socketserver nachgebaut werden. Dazu muss zunächst ein sogenannter Request Handler erstellt werden. Das ist eine Klasse, die von der Basisklasse socketserver.BaseRequestHandler abgeleitet wird. Im Wesentlichen muss in dieser Klasse die Methode handle überschrieben werden, in der die Kommunikation mit einem Client ablaufen soll:

import socketserver
class ChatRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
addr = self.client_address[0]
print("[{}] Verbindung hergestellt".format(addr))
while True:
s = self.request.recv(1024)
if s:
print("[{}] {}".format(addr, s.decode()))
else:
print("[{}] Verbindung geschlossen".format(addr))
break

Hier wurde die Klasse ChatRequestHandler erzeugt, die von BaseRequestHandler erbt. Später erzeugt die socketserver-Instanz bei jeder hergestellten Verbindung eine neue Instanz dieser Klasse und ruft die Methode handle auf. In dieser Methode läuft dann die Kommunikation mit dem verbundenen Client ab. Zusätzlich zur Methode handle können noch die Methoden setup und finish überschrieben werden, die entweder vor oder nach dem Aufruf von handle aufgerufen werden.

Neben den angesprochenen Methoden definiert die Basisklasse BaseRequestHandler das Attribut request, über das Informationen über die aktuelle Anfrage eines Clients zugänglich sind. Bei einem TCP-Server referenziert request die Socket-Instanz, die zur Kommunikation mit dem Client verwendet wird. Mit ihr können Daten gesendet oder empfangen werden. Bei Verwendung des verbindungslosen UDP-Protokolls referenziert request ein Tupel, das die vom Client gesendeten Daten und den Kommunikationssocket enthält, der für die Antwort verwendet werden kann.

Das Attribut client_address referenziert ein Adress-Tupel, das die IP-Adresse und die Portnummer des Clients enthält, dessen Anfrage mit dieser BaseRequestHandler-Instanz behandelt wird.

In unserem Beispiel werden innerhalb der Methode handle in einer Endlosschleife eingehende Daten eingelesen. Wenn ein leerer String eingelesen wurde, wird die Verbindung vom Kommunikationspartner geschlossen. Andernfalls wird der gelesene String ausgegeben.

Damit ist die Arbeit am Request Handler beendet. Was jetzt noch fehlt, ist der Server, der eingehende Verbindungen akzeptiert und daraufhin den Request Handler instanziiert:

server = socketserver.ThreadingTCPServer(("", 50000), ChatRequestHandler) 
server.serve_forever()

Um den tatsächlichen Server zu erstellen, erzeugen wir eine Instanz der Klasse ThreadingTCPServer. Dem Konstruktor übergeben wir dabei ein Adress-Tupel und die soeben erstellte Request-Handler-Klasse ChatRequestHandler. Durch Aufruf der Methode serve_forever der ThreadingTCPServer-Instanz instruieren wir den Server, alle von nun an eingehenden Verbindungsanfragen zu akzeptieren.

Neben der Methode serve_forever stellt eine Serverinstanz die Methode handle_request bereit, die genau eine Verbindungsanfrage akzeptiert und behandelt. Außerdem existiert die Methode shutdown zum Stoppen eines Servers.

[»]  Hinweis

Der Programmierer trägt selbst die Verantwortung für eventuell von mehreren Threads gemeinsam genutzte Ressourcen. Diese müssen gegebenenfalls durch Critical Sections abgesichert werden.

Näheres zur parallelen Programmierung erfahren Sie in Kapitel 32.

Neben der Klasse ThreadingTCPServer können auch andere Serverklassen instanziiert werden, je nachdem, wie sich der Server verhalten soll. Die Schnittstelle ist bei allen Konstruktoren gleich.

TCPServer, UDPServer

Dies ist ein einfacher TCP- bzw. UDP-Server. Beachten Sie, dass diese Server immer nur eine Verbindung gleichzeitig eingehen können. Aus diesem Grund ist die Klasse TCPServer für unser Beispielprogramm nicht einsetzbar.

ThreadingTCPServer, ThreadingUDPServer

Diese Klassen implementieren einen TCP- bzw. UDP-Server, der jede Anfrage eines Clients in einem eigenen Thread behandelt, sodass der Server mit mehreren Clients gleichzeitig in Kontakt sein kann. Damit ist die Klasse ThreadingTCPServer ideal für unser oben dargestelltes Beispiel.

ForkingTCPServer, ForkingUDPServer

Diese Klassen implementieren einen TCP- bzw. UDP-Server, der jede Anfrage eines Clients in einem eigenen Prozess behandelt, sodass der Server mit mehreren Clients gleichzeitig in Kontakt sein kann. Die Methode handle des Request Handlers wird in einem eigenen Prozess ausgeführt, kann also nicht auf Instanzen des Hauptprozesses zugreifen.

 


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.

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Python 3 Python 3
Jetzt Buch bestellen

 Buchempfehlungen
Zum Rheinwerk-Shop: Einstieg in Python
Einstieg in Python


Zum Rheinwerk-Shop: Python. Der Grundkurs
Python. Der Grundkurs


Zum Rheinwerk-Shop: Algorithmen mit Python
Algorithmen mit Python


Zum Rheinwerk-Shop: Objektorientierte Programmierung
Objektorientierte Programmierung


Zum Rheinwerk-Shop: Raspberry Pi. Das umfassende Handbuch
Raspberry Pi. Das umfassende Handbuch


Zum Rheinwerk-Shop: Roboter-Autos mit dem Raspberry Pi
Roboter-Autos mit dem Raspberry Pi


Zum Rheinwerk-Shop: Neuronale Netze programmieren mit Python
Neuronale Netze programmieren mit Python


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2020
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern