Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
1 Einleitung
2 Überblick über Python
3 Die Arbeit mit Python
4 Der interaktive Modus
5 Grundlegendes zu Python-Programmen
6 Kontrollstrukturen
7 Das Laufzeitmodell
8 Basisdatentypen
9 Benutzerinteraktion und Dateizugriff
10 Funktionen
11 Modularisierung
12 Objektorientierung
13 Weitere Spracheigenschaften
14 Mathematik
15 Strings
16 Datum und Zeit
17 Schnittstelle zum Betriebssystem
18 Parallele Programmierung
19 Datenspeicherung
20 Netzwerkkommunikation
21 Debugging
22 Distribution von Python-Projekten
23 Optimierung
24 Grafische Benutzeroberflächen
25 Python als serverseitige Programmiersprache im WWW mit Django
26 Anbindung an andere Programmiersprachen
27 Insiderwissen
28 Zukunft von Python
A Anhang
Stichwort

Download:
- ZIP, ca. 4,8 MB
Buch bestellen
Ihre Meinung?

Spacer
 <<   zurück
Python von Peter Kaiser, Johannes Ernesti
Das umfassende Handbuch - Aktuell zu Python 2.5
Buch: Python

Python
gebunden, mit CD
819 S., 39,90 Euro
Galileo Computing
ISBN 978-3-8362-1110-9
Pfeil 20 Netzwerkkommunikation
  Pfeil 20.1 Socket API
    Pfeil 20.1.1 Client/Server-Systeme
    Pfeil 20.1.2 UDP
    Pfeil 20.1.3 TCP
    Pfeil 20.1.4 Blockierende und nicht-blockierende Sockets
    Pfeil 20.1.5 Verwendung des Moduls
    Pfeil 20.1.6 Netzwerk-Byte-Order
    Pfeil 20.1.7 Multiplexende Server – select
    Pfeil 20.1.8 SocketServer
  Pfeil 20.2 Zugriff auf Ressourcen im Internet – urllib
    Pfeil 20.2.1 Verwendung des Moduls
  Pfeil 20.3 Einlesen einer URL – urlparse
  Pfeil 20.4 FTP – ftplib
  Pfeil 20.5 E-Mail
    Pfeil 20.5.1 SMTP – smtplib
    Pfeil 20.5.2 POP3 – poplib
    Pfeil 20.5.3 IMAP4 – imaplib
    Pfeil 20.5.4 Erstellen komplexer E-Mails – email
  Pfeil 20.6 Telnet – telnetlib
  Pfeil 20.7 XML-RPC
    Pfeil 20.7.1 Der Server
    Pfeil 20.7.2 Der Client
    Pfeil 20.7.3 Multicall
    Pfeil 20.7.4 Einschränkungen

»Alle reden von Kommunikation, aber die wenigsten haben sich etwas mitzuteilen.« – Hans Magnus Enzensberger

20 Netzwerkkommunikation

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 20.1 zeigt eine stark vereinfachte Version des OSI-Schichtenmodells, das die Hierarchie der verschiedenen Protokollebenen veranschaulicht.

Abbildung 20.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 von elektrischen Impulsen ü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 sehr interessant. In diesem Buch werden wir folgende Protokolle ausführlich behandeln:


Tabelle 20.1  Netzwerkprotokolle
Protokoll Beschreibung Modul Abschnitt

TCP

Grundlegendes verbindungsorientiertes Netzwerkprotokoll

socket

20.1.3

UDP

Grundlegendes verbindungsloses Netzwerkprotokoll

socket

20.1.2

FTP

Dateiübertragung

ftplib

20.4

SMTP

Versenden von E-Mails

smtplib

20.5.1

POP3

Abholen von E-Mails

poplib

20.5.2

IMAP4

Abholen von E-Mails

imaplib

20.5.3

Telnet

Terminalemulation

telnetlib

20.6

HTTP

Übertragen von Textdateien, beispielsweise Webseiten

urllib

20.2


Beachten Sie, dass es auch abstrakte Protokolle gibt, die auf UDP aufbauen, beispielsweise NFS (Network File System). Wir werden in diesem Buch aber ausschließlich auf TCP basierende Protokolle behandeln.

Wir werden im ersten Unterkapitel zunächst eine ganz grundlegende Einführung in das systemnahe Modul socket bringen. Es lohnt sich absolut, 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.


Galileo Computing - Zum Seitenanfang

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

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

Die Idee, die hinter der Socket API steht, ist die, 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 Zielsocket 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 sogenannte 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önnten 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 kann. 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.


Galileo Computing - Zum Seitenanfang

20.1.1 Client/Server-Systeme  Zur nächsten ÜberschriftZur vorigen Ü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 sogenannten 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 gegenüber stehen die Konzepte der parallelen Server und der multiplexenden Server, 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, sollen durch das Flussdiagramm aus Abbildung 20.2 verdeutlicht werden. Sie können es als eine Art Bauplan für einen seriellen Server und den dazu gehö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 mittels 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. Beachten Sie, dass ein Kommunikationssocket immer nur für einen verbundenen Client zuständig ist.

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

Abbildung 20.2  Das Client/Server-Modell

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.

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 finden Sie im folgenden Abschnitt.


Galileo Computing - Zum Seitenanfang

20.1.2 UDP  Zur nächsten ÜberschriftZur vorigen Ü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 = raw_input("IP-Adresse: ") nachricht = raw_input("Nachricht: ")
s.sendto(nachricht, (ip, 50000)) s.close()

Zunächst wird durch Aufruf der Funktion socket eine Socket-Instanz erzeugt. 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 zu schicken ist, 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, und der Socket mittels close geschlossen.

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 20.1.1 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 "[%s] %s" % (addr[0], daten) finally: s.close()

Auch hier wird zunächst eine Socket-Instanz erstellt. In der darauf folgenden 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 schlicht ein Tupel mit zwei Elementen gemeint: einer IP-Adresse als String und einer Portnummer als ganze 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 also auch über 127.0.0.1 oder localhost.

Nachdem der Socket an eine Adresse gebunden worden ist, 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. 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 mittels close geschlossen werden.


Galileo Computing - Zum Seitenanfang

20.1.3 TCP  Zur nächsten ÜberschriftZur vorigen Ü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 bestimmte 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 intern, 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 man gerade dort die Performance von UDP und die Verbindungsqualität von TCP gegeneinander abwägen sollte.

Im Folgenden soll auch die Verwendung von TCP anhand eines kleinen Beispielprojekts erläutert werden: Es soll ein rudimentäres Chatprogramm 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 "[%s] %s" % (addr[0], data) nachricht = raw_input("Antwort: ") komm.send(nachricht) 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 darauf folgenden Endlosschleife wartet die aufgerufene Methode accept des Verbindungssockets nun 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, so 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 = raw_input("IP-Adresse: ") s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ip, 50000))
try: while True: nachricht = raw_input("Nachricht: ") s.send(nachricht) antwort = s.recv(1024) print "[%s] %s" % (ip,antwort) 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 nachfolgende Endlosschleife funktioniert ähnlich wie die des Servers, mit dem Unterschied, dass zuerst eine Nachricht eingegeben und abgeschickt wird und danach auf eine Antwort des Servers gewartet wird. Damit wären 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.

Genau dieser Rhythmus ist aber auch der größte Knackpunkt des Beispielprojekts, denn es ist für einen der Kommunikationspartner schlicht unmöglich, zwei Nachrichten direkt hintereinander abzusetzen. Für den praktischen Einsatz hätte das Programm also allenfalls Unterhaltungswert. Das Ziel war es auch nicht, eine möglichst perfekte Chat-Applikation zu schreiben, sondern eine einfache und kurze Beispielimplementation einer Client/Server-Kommunikation über TCP zu erstellen.

Betrachten Sie es also als Herausforderung, Client und Server, beispielsweise durch Threads, 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 ermöglicht, dass die Benutzer Nachrichten per raw_input eingeben, und diese dann verschickt.


Galileo Computing - Zum Seitenanfang

20.1.4 Blockierende und nicht-blockierende Sockets  Zur nächsten ÜberschriftZur vorigen Ü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 würde ein Aufruf der Methode recv eines Sockets so lange das komplette Programm blockieren, 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 sogenannten 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 zu warten hätte, wirft sie eine socket.error–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 socket.error-Exception geworfen und der Kontrollfluss damit an das Programm zurückgegeben.
  • Die Methode connect sendet eine Verbindungsanfrage an den Zielsocket und wartet nicht, bis diese Verbindung zustande kommt. Wenn connect aufgerufen wird und die Verbindungsanfrage noch läuft, wird eine socket.error-Exception mit der Fehlermeldung »Operation now in progress« geworfen. Durch mehrmaligen Aufruf von connect lässt sich feststellen, ob die Operation immer noch durchgeführt wird.
    • Alternativ kann im nicht-blockierenden Modus die Methode connect_ex für Verbindungsanfragen verwendet werden. Diese wirft keine socket.error-Exception, sondern zeigt eine erfolgreiche Verbindung anhand eines Rückgabewertes von 0 an. Bei echten Fehlern, die bei der Verbindung auftreten, wird auch bei connect_ex eine Exception geworfen.

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

s.setblocking(0)

In diesem Fall würden sich Methodenaufrufe des Sockets s wie oben beschrieben verhalten. Ein Parameter von 1 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.

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


Galileo Computing - Zum Seitenanfang

20.1.5 Verwendung des Moduls  Zur nächsten ÜberschriftZur vorigen Überschrift

Da die Funktionen des Moduls oder die Methoden des Socket-Objekts in den beiden vorherigen Abschnitten vielleicht etwas zu kurz gekommen sind, möchten wir in diesem Abschnitt noch einmal eine Auflistung der wichtigsten dieser Funktionen und Methoden bringen. Wir beginnen mit den Funktionen und Konstanten des Moduls socket.

Funktionen

socket.getfqdn([name])

Gibt den vollständigen Domainnamen (FQDN, Fully qualified domain name) der Domain name zurück. Wenn name weggelassen wird, wird der vollständige Domainname des lokalen Hosts zurückgegeben.

>>> socket.getfqdn() 
'HOSTNAME.localdomain'
socket.gethostbyname(hostname)

Gibt die IPv4-Adresse des Hosts hostname als String zurück.

>>> socket.gethostbyname("HOSTNAME") 
'192.168.1.23'
socket.gethostname()

Gibt den Hostnamen des Systems als String zurück.

>>> socket.gethostname() 
'HOSTNAME'
socket.getservbyname(servicename[, protocolname])

Gibt den Port für den Service servicename mit dem Netzwerkprotokoll protocolname zurück. Bekannte Services wären beispielsweise "http" oder "ftp" mit den Portnummern 80 bzw. 21. Der Parameter protocolname sollte entweder "tcp" oder "udp" sein.

>>> socket.getservbyname("http", "tcp") 
80
socket.getservbyport(port[, protocolname])

Diese Funktion ist das Gegenstück zu getservbyname.

>>> socket.getservbyport(21) 
'ftp'
socket.socket([family[, type]])

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.

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.

socket.getdefaulttimeout(), socket.setdefaulttimeout(timeout)

Gibt in Form einer Gleitkommazahl die maximale Anzahl an Sekunden zurück, die beispielsweise die Methode recv eines Socket-Objekts auf ein eingehendes Paket wartet. Durch die Funktion setdefaulttimeout kann dieser Wert für alle neu erzeugten Socket-Instanzen verändert werden.

Die Socket-Klasse

Nachdem durch die Funktion socket des Moduls socket eine neue Instanz der Klasse Socket erzeugt wurde, stellt diese natürlich weitere Funktionalität bereit, um sich mit einem zweiten Socket zu verbinden oder Daten an den Verbindungspartner zu übermitteln. Die Methoden der Socket-Klasse sollen im Folgenden beschrieben werden.

Beachten Sie, dass sich das Verhalten der Methoden im blockierenden und nicht-blockierenden Modus unterscheidet. Näheres dazu finden Sie in Abschnitt 20.1.4, »Blockierende und nicht-blockierende Sockets«. Im Folgenden sei s eine Instanz der Klasse socket.Socket.

s.accept()

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 geschehen kann. Das zweite Element des Tupels ist ein weiteres Tupel, das IP-Adresse und Port des verbundenen Sockets enthält.

Diese Methode ist für TCP gedacht.

s.bind(address)

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 oder, im Falle von UDP, direkt Datenpakete empfangen.

s.close()

Schließt den Socket. Das bedeutet, dass keine Daten mehr über ihn gesendet oder empfangen werden können.

s.connect(address)

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.

Diese Methode ist für TCP gedacht.

s.connect_ex(address)

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 Zielsocket nicht erreicht werden konnte.

Diese Methode ist für TCP gedacht.

s.getpeername()

Gibt das Adressobjekt des mit diesem Socket verbundenen Sockets zurück. Das Adressobjekt ist ein Tupel, das IP-Adresse und Portnummer enthält.

Diese Methode ist für TCP gedacht.

s.getsockname()

Gibt das Adressobjekt zurück, über das dieser Socket mit dem verbundenen Socket kommuniziert.

s.listen(backlog)

Versetzt einen Serversocket in den sogenannten Listen-Modus, das heißt, der Socket achtet auf Sockets, die sich mit ihm verbinden wollen. Nachdem diese Methode aufgerufen worden ist, 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.

Diese Methode ist für TCP gedacht.

s.recv(bufsize)

Liest beim Socket eingegangene Daten. Durch den Parameter bufsize wird die maximale Anzahl von zu lesenden Bytes festgelegt. Die gelesenen Daten werden in Form eines Strings zurückgegeben.

Diese Methode ist für TCP gedacht.

s.recvfrom(bufsize)

Unterscheidet sich von recv in Bezug auf den Rückgabewert. Dieser ist bei recvfrom ein Tupel, das als erstes Element die gelesenen Daten als String und als zweites Element das Adressobjekt des Verbindungspartners enthält.

Diese Methode ist für UDP gedacht.

s.send(string)

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

Diese Methode ist für TCP gedacht.

s.sendall(string)

Unterscheidet sich von send darin, dass sendall so lange versucht, die Daten zu senden, bis entweder der vollständige Datensatz string versendet wurde oder ein Fehler aufgetreten ist. Im Fehlerfall wird eine entsprechende Exception geworfen.

Diese Methode ist für TCP gedacht.

s.sendto(string, address)

Versendet den Datensatz string an einen Socket mit der Adresse address. Da der Verbindungspartner explizit angegeben wird, brauchen die beiden Sockets nicht untereinander verbunden zu sein. Der Parameter address muss ein Adressobjekt sein.

Diese Methode ist für UDP gedacht.

s.setblocking(flag)

Wenn für flag 0 übergeben wird, wird der Socket in den nicht-blockierenden Modus versetzt, sonst in den blockierenden Modus.

Im blockierenden Modus warten Methoden wie send oder recv, bis Daten versendet bzw. gelesen werden konnten. Im nicht-blockierenden Modus würde ein Aufruf von recv beispielsweise eine Exception verursachen, wenn keine Daten eingegangen sind, die gelesen werden könnten.

s.settimeout(value), gettimeout()

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 kann für value die Anzahl an Sekunden in Form einer Gleitkommazahl oder None übergeben werden.

Ü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.


Galileo Computing - Zum Seitenanfang

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

Das Schöne an standardisierten Protokollen wir 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 sogenannte 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 für Binärdaten im Netzwerk zu verwenden ist. Um diese Netzwerk-Byte-Order sinnvoll umzusetzen, sind im Modul socket vier Funktionen enthalten, die entweder Daten von der Host-Byte-Order in die Netzwerk-Byte-Order (»hton«) oder umgekehrt (»ntoh«) konvertieren. Die folgende Tabelle listet diese Funktionen auf und erläutert ihre Bedeutung:


Tabelle 20.2  Konvertierung von Binärdaten
Alias Bedeutung
socket.ntohl(x)

Konvertiert eine 32-Bit-Zahl von der Netzwerk- in die Host-Byte-Order.

socket.ntohs(x)

Konvertiert eine 16-Bit-Zahl von der Netzwerk- in die Host-Byte-Order.

socket.htonl(x)

Konvertiert eine 32-Bit-Zahl von der Host- in die Netzwerk-Byte-Order.

socket.htons(x)

Konvertiert eine 16-Bit-Zahl von der Host- in die Netzwerk-Byte-Order.


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 übrigens nicht die Netzwerk-Byte-Order.

An dieser Stelle möchten wir noch einmal darauf hinweisen, dass eine Konvertierung von Binärdaten in einem professionellen Programm selbstverständlich dazugehört. Solange Sie jedoch im privaten Umfeld kleinere Netzwerkanwendungen schreiben, die Binärdaten ausschließlich zwischen x86er-PCs austauschen, brauchen Sie sich über die Byte-Order keine Gedanken zu machen. Zudem können ASCII-Zeichen, wie gesagt, problemlos auch zwischen Systemen mit verschiedener Byte-Order ausgetauscht werden, sodass auch in diesem Fall keine explizite Konvertierung nötig ist.


Galileo Computing - Zum Seitenanfang

20.1.7 Multiplexende Server – select  Zur nächsten ÜberschriftZur vorigen Ü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 von nicht-blockierenden 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 select ermöglicht es, im gleichen Prozess mehrere blockierende Sockets zu verwalten, sodass die Vorteile blockierender Sockets erhalten bleiben. Ein Server, der select verwendet, wird multiplexender Server genannt. Im Modul ist im Wesentlichen die Funktion select enthalten, die im Folgenden besprochen werden soll.

select.select(iwtd, owtd, ewtd[, timeout])

Im einfachsten Fall bekommt die Funktion select als ersten Parameter iwtd eine Liste von Sockets übergeben, mit denen eine Leseoperation durchgeführt werden soll. Nehmen wir einmal an, für die weiteren Parameter owtd und ewtd würde jeweils eine leere Liste übergeben. In diesem Fall würde die Funktion select das Programm so lange schlafen legen, bis an mindestens einem der übergebenen Sockets Daten vorliegen, die ausgelesen werden können.

Ähnlich verhält es sich mit dem zweiten Parameter, owtd. Hier wird eine Liste von Sockets übergeben, mit denen eine Schreiboperation durchgeführt werden soll. Die Funktion select weckt das Programm auf, sobald einer der hier übergebenen Sockets zum Schreiben bereit ist.

Für den dritten Parameter, ewtd, wird eine Liste von Sockets übergeben, bei denen möglicherweise sogenannte out-of-band data eingegangen sind. Das sind TCP-Pakete, die als besonders wichtig (engl. urgent) eingestuft sind und somit privilegiert übertragen werden. Mithilfe solcher Nachrichten kann ein Programm wichtige Ausnahmefälle signalisieren. Dennoch werden wir hier nicht näher darauf eingehen, da solche OOB-Pakete so gut wie nie verwendet werden.

Als vierter, optionaler und letzter Parameter kann ein Timeout-Wert in Sekunden angegeben werden. Dieser veranlasst die Funktion select, das Programm nach einer gewissen Zeit aufzuwecken, auch wenn sich bei keinem der übergebenen Sockets etwas getan hat. Wenn ein Timeout-Wert von 0.0 übergeben wird, gibt select nur die Sockets zurück, die beim Aufruf bereits bereit zum Lesen bzw. Schreiben sind.

Es ist möglich, für iwtd, owtd oder ewtd leere Listen zu übergeben, vor allem, weil in der Regel nur der erste dieser Parameter benötigt wird, denn es ist das klassische Anwendungsgebiet von select, auf eintreffende Daten zu warten. Der zweite Parameter ist deshalb weniger wichtig, weil ein Socket in der Regel zu jeder Zeit zum Versenden von Daten bereitsteht. Und in den seltenen Fällen, bei denen dies nicht der Fall ist, ist die »Verstopfung« des Ausgangspuffers nur von kurzer Dauer und ein blockierender Aufruf von send somit nicht weiter tragisch.

Die Funktion select gibt in jedem Fall ein Tupel zurück, das aus drei Listen besteht. Diese Listen enthalten jeweils die Sockets, bei denen entweder Daten gelesen oder geschrieben werden können oder, wie erwähnt, dringende Pakete vorliegen. Beachten Sie, dass dieselbe Socket-Instanz beim Aufruf von select durchaus in mehreren der übergebenen Listen vorkommen darf.

Im folgenden Beispiel soll ein Server geschrieben werden, 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.

import socket 
import select
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(("", 50000)) server.listen(1)
clients = []
try: while True: lesen, schreiben, oob = select.select([server] + clients, [], [])
for sock in lesen: if sock is server: client, addr = server.accept() clients.append(client) print "+++ Client %s verbunden" % addr[0] else: nachricht = sock.recv(1024) ip = sock.getpeername()[0] if nachricht: print "[%s] %s" % (ip, nachricht) else: print "+++ Verbindung zu %s beendet" % ip sock.close() clients.remove(sock) finally: for c in clients: c.close() server.close()

Zunächst wird ein Socket server erzeugt, der dazu gedacht ist, eingehende Verbindungsanfragen zu akzeptieren. Zudem wird die leere Liste clients angelegt, die später alle verbundenen Client-Sockets enthalten soll. Die darauf folgende try/except-Anweisung hat die Aufgabe, alle verbundenen Sockets ordnungsgemäß durch Aufruf von close zu schließen, wenn das Programm beendet wird.

Viel interessanter ist aber die Endlosschleife innerhalb des try-Zweiges, in der zunächst die Funktion select aufgerufen wird. Dabei werden alle geöffneten Sockets, inklusive des Server-Sockets, als erster Parameter übergeben. Die von select zurückgegebenen Listen werden von lesen, schreiben und oob referenziert, wobei wir uns nur für die Liste lesen näher interessieren.

Nach dem Aufruf von select wird über die zurückgegebene Liste lesen iteriert und in jedem Iterationsschritt überprüft, ob es sich bei dem betrachteten Socket um den Server-Socket handelt. Wenn das der Fall ist, wenn also beim Server-Socket Daten zum Einlesen bereitstehen, bedeutet dies, dass eine Verbindungsanfrage vorliegt. Wir akzeptieren die Verbindung, fügen den neuen Socket in die Liste clients ein und geben eine entsprechende Meldung aus.

Wenn Daten bei einem Client-Socket eingegangen sind, bedeutet dies, dass entweder eine Nachricht von diesem eingetroffen ist oder dass die Verbindung beendet wurde. Um zu testen, welcher der beiden Fälle eingetreten ist, lesen wir die vorhandenen Daten mittels recv aus. Wenn die Verbindung seitens des Clients beendet wurde, gibt recv einen leeren String zurück. In diesem Fall löschen wir diesen Socket aus der Liste clients und geben eine entsprechende Meldung aus.

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

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

Dabei handelt es sich tatsächlich um reine Socket-Programmierung, wie wir sie bereits in den vorherigen Kapiteln behandelt haben. Beachten Sie, dass der Client, abgesehen von eventuell auftretenden Latenzen, nicht bemerkt, ob er von einem seriellen oder einem multiplexenden Server bedient wird.


Galileo Computing - Zum Seitenanfang

20.1.8 SocketServer  topZur vorigen Überschrift

Sie werden festgestellt haben, dass das Schreiben eines Servers unter Verwendung des Moduls socket mitunter eine sehr komplexe Aufgabe sein 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 vorherigen 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 "[%s] Verbindung hergestellt" % addr while True: s = self.request.recv(1024) if s: print "[%s] %s" % (addr, s) else: print "[%s] Verbindung geschlossen" % addr break

Hier wurde die Klasse ChatRequestHandler erzeugt, die von BaseRequestHand ler erbt. Später wird von der SocketServer-Instanz bei jeder hergestellten Verbindung eine neue Instanz dieser Klasse erzeugt und die Methode handle aufgerufen. 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 (setup) oder nach (finish) dem Aufruf von handle aufgerufen werden.

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 erzeugen, wird eine Instanz der Klasse Threa dingTCPServer erzeugt. Dem Konstruktor wird dabei ein Adress-Tupel und die soeben erstellte Request Handler-Klasse ChatRequestHandler übergeben. Durch Aufruf der Methode serve_forever der ThreadingTCPServer-Instanz wird der Server dazu instruiert, eine unbestimmte Anzahl an Verbindungen einzugehen.

Beachten Sie, dass der Programmierer selbst Verantwortung für eventuell von mehreren Threads gemeinsam genutzte Ressourcen trägt. Diese müssen gegebenenfalls durch Critical Sections abgesichert werden.

Neben der Klasse ThreadingTCPServer können auch andere Klassen instanziiert werden, je nachdem, wie sich der Server verhalten soll. Die Schnittstelle der Konstruktoren ist immer dieselbe.

SocketServer.TCPServer, SocketServer.UDPServer

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.

SocketServer.ThreadingTCPServer, SocketServer.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 ThreadingTCPSer ver ideal für unser obiges Beispiel.

SocketServer.ForkingTCPServer, SocketServer.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. Beachten Sie dabei, dass die Methode handle des Request Handlers in einem eigenen Prozess ausgeführt wird, also nicht auf Instanzen des Hauptprozesses zugreifen kann.

Die Klasse SocketServer

An dieser Stelle sollen die wichtigsten Attribute und Methoden der Klasse SoketServer besprochen werden. Im Folgenden sei s eine Instanz der Klasse SocketServer.

s.address_family

Dieses Attribut referenziert ein Adress-Tupel, das die IP-Adresse und die Portnummer enthält, auf denen der Server s nach eingehenden Verbindungsanfragen horcht.

s.socket

Dieses Attribut referenziert die von dem Server verwendete Socket-Instanz.

s.fileno()

Gibt den Dateideskriptor des Server-Sockets zurück.

s.handle_request()

Instruiert den Server, genau eine Verbindungsanfrage zu akzeptieren und zu behandeln.

s.serve_forever()

Instruiert den Server, eine unbestimmte Anzahl von Verbindungsanfragen zu akzeptieren und zu behandeln.

Die Klasse BaseRequestHandler

Die Klasse BaseRequestHandler bietet einige Methoden und Attribute, die überschrieben oder verwendet werden können. Beachten Sie, dass eine Instanz der Klasse BaseRequestHandler immer für einen verbundenen Client zuständig ist. Im Folgenden sei rh eine Instanz der Klasse BaseRequestHandler.

rh.request

Über das Attribut request können Informationen über die aktuelle Anfrage eines Clients herausgefunden werden. Bei einem TCP-Server referenziert request die Socket-Instanz, die zur Kommunikation mit dem Client verwendet wird. Mit dieser können Daten gesendet oder empfangen werden. Bei Verwendung des verbindungslosen UDP-Protokolls referenziert request einen String, der die gesendeten Daten enthält.

rh.client_address

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

rh.server

Das Attribut server referenziert den verwendeten Server, also eine Instanz der Klassen TCPServer, UDPServer, ThreadingTCPServer, ThreadingUDPServer, ForkingTCPServer oder ForkingUDPServer.

rh.handle()

Diese Methode sollte überschrieben werden. Wenn der Server eine Verbindungsanfrage eines Clients akzeptiert hat, wird eine neue Instanz der Request-Handler-Klasse erzeugt und diese Methode aufgerufen.

rh.setup()

Diese Methode kann überschrieben werden und wird stets vor dem Aufruf von handle aufgerufen.

rh.finish()

Diese Methode kann überschrieben werden und wird stets nach dem Aufruf von handle aufgerufen.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.






 <<   zurück
  
  Zum Katalog
Zum Katalog: Python






Python
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Linux






 Linux


Zum Katalog: Ubuntu GNU/Linux






 Ubuntu GNU/Linux


Zum Katalog: Praxisbuch Web 2.0






 Praxisbuch Web 2.0


Zum Katalog: UML 2.0






 UML 2.0


Zum Katalog: Praxisbuch Objektorientierung






 Praxisbuch Objektorientierung


Zum Katalog: Einstieg in SQL






 Einstieg in SQL


Zum Katalog: IT-Handbuch für Fachinformatiker






 IT-Handbuch für Fachinformatiker


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Galileo Press 2008
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.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de