Verwenden der Serverklassen

(Auszug aus "Python & XML" von Christopher A. Jones & Fred L. Drake, Jr.)

Nachdem Sie nun über Sockets Bescheid wissen, sind Sie bereit, Ihre eigenen Netzwerkserver zu codieren. Aber bevor Sie jetzt gleich versuchen, einen HTTP-Server zu implementieren, sollten Sie erst einmal sehen, was Python bereits an solchen Modulen zu bieten hat.

Python bietet einen Basis-HTTP-Server, einen einfachen HTTP-Server und einen CGI-HTTP-Server. Was den einfachen Server angeht, liegt seine Hauptaufgabe darin, Dateien aus dem lokalen Verzeichnis an Clients zu übermitteln, die über HTTP danach fragen. Der CGI-Server ist damit vergleichbar, ist jedoch dafür vorgesehen, CGI-Skripten auszuführen und sie mit den Umgebungsvariablen auszustatten, die sie brauchen, um interaktiv mit ihren Browser-Clients zu kommunizieren. Aber der interessanteste von allen ist der BaseHTTPServer.

Das BaseHTTPServer-Modul enthält einen einfachen Socket-Server (die Klasse HTTP-Server), der HTTP-Anfragen akzeptiert. Sie können einige Methoden von Base-HTTPRequestHandler implementieren, um mit der Anfrage zu arbeiten. Der Vorteil, diese zwei Klassen zu benutzen, ist die Power, die Sie im Umgang mit Clients erhalten. Anstatt einfach nur Dateien zurückzugeben oder externe CGI-Programme zu starten, haben Sie die Möglichkeit, URLs zu interpretieren, wie immer Sie es wünschen, und dem Client alles, was Sie wollen, zurückzugeben. Falls Sie jemals mit Anwendungsservern gearbeitet haben, die ihre eigene Inline-Skriptsprache anbieten (z. B. PHP, Cold Fusion, Active Server Pages usw.), dann haben Sie Basis-HTTP-Server in der Praxis gesehen. Wenn ein Anwendungsserver, z. B. Cold Fusion, mit Ihrem Webserver betrieben wird, übergibt Ihr Webserver alle HTTP-Anfragen mit der Erweiterung .cfm an den Cold Fusion-Prozeß. Das heißt, der Server tut absolut nichts mit der Anfrage, wenn sie als eine Cold Fusion-Anfrage gekennzeichnet ist. Cold Fusion untersucht dann die Anfrage und sendet die passenden Daten zurück, nachdem eventuell in der Datei befindlicher Inline-Code ausgeführt worden ist.

In diesem Abschnitt implementieren wir einen Basis-HTTP-Server, der sowohl GET- als auch POST-Anfragen bearbeiten kann. Da er auf Port 2112 läuft, wird der gesamte an diesen Port geleitete HTTP-Verkehr vom Server übernommen, egal welcher Dateiname verlangt wird. Eigentlich werden überhaupt keine Dateien zurückgegeben, sondern es wird eine Standardantwort geben, die alle Browser erhalten, unabhängig davon, welche Informationen die URL enthält.

Klassen im Modul BaseHTTPServer

Das Modul BaseHTTPServer enthält die folgenden zwei Klassen: HTTPServer und BaseHTTPRequestHandler. Sie werden wahrscheinlich nur die vier Methoden zwischen diesen Klassen kennenlernen, die in diesem Abschnitt dargelegt werden.

Der Konstruktor der Klasse HTTPServer hat zwei Argumente: ein Adressen-Tupel (Rechnername und Port) und eine Referenz auf einen Anfrage-Handler. Nachdem Sie einmal instanziiert wurde, verfügt die HTTPServer-Klasse über zwei Operationsmodi. Sie können serve_forever aufrufen, um eine unendliche Anzahl von Clients zu bedienen, oder Sie können handle_request aufrufen, um einen einzelnen Client zu bedienen.

Die Klasse BaseHTTPRequestHandler ist eine Platzhalterklasse, die Sie für spezielle Funktionalität überladen können. Wahrscheinlich werden Sie nur die Methoden do_GET und do_POST überladen. Diese werden immer dann aufgerufen, wenn eine neue HTTP-Anfrage angekommen ist und entweder nach einer GET- oder POST-Operation verlangt. Ein Anfrage-Handler (rh, Request-Handler) verfügt über einige Attribute, die von großer Bedeutung sind:

rh.headers
Ein Dictionary von Header/Wert-Paaren.

rh.rfile
Der »request«- oder »read«-Dateideskriptor des Sockets. Er ermöglicht es Ihnen, Daten über den Header hinaus in den Server einzulesen, wie Sie es während einer POST-Operation täten.

rh.wfile
Der »write«-Dateideskriptor des Sockets. Dieses Attribut ermöglicht es Ihnen, Daten an die Quelle der HTTP-Anfrage zu schicken (zurück zum Client oder Browser).

rh.path
Der Anfrage- und Pfadteil des Abfrage-Strings.

rh.command
Der angeforderte Typ der HTTP-Anfrage (GET, POST, HEAD oder PUT).

rh.request_version
Ein String mit der momentan verwendeten HTTP-Version.

rh.client_address
Ein Tupel, das die Client-Adresse darstellt.

Kernkonzepte von Servern

Die Architektur der Basis-Server-Klasse ist ziemlich einfach. Die HTTP-Header werden Ihnen als Dictionary übergeben, und darüber hinaus existieren ein paar einfache Funktionen, die es Ihnen ermöglichen, verschiedene Aspekte einer HTTP-Anfrage an den Sender zurückzuschreiben. Zum Kern der Klassen gehören die Variablen rfile und wfile der Klasse BaseHTTPRequestHandler. Mit diesen zwei Objekten können Sie direkt vom Client-Socket lesen und dorthin schreiben.

Instanziierung einer Server-Klasse

Um einen Server zu instanziieren und ihn in Betrieb zu nehmen, können Sie den HTTPServer-Konstruktor verwenden. Beispiel:

myServer = HTTPServer(('', 2112), myRequestHandler)
for lp in range(5):
   myServer.handle_request( )

Dieser Code teilt dem Server mit, daß er sich mit localhost verbinden (mit dem Tupel aus Leerzeile und Port-Nummer) und einen speziellen Anfrage-Handler annehmen soll. Zusätzlich zu handle_request verfügt HTTPServer über die Methode serve_forever, die nie ein Ergebnis zurückliefert. Welche Methode Sie wählen, ist allein Ihre Sache. Später in diesem Abschnitt werden im Beispiel HTTPServer.py fünf Anfragen bearbeitet, bevor es beendet wird. Bei der Wahl von serve_forever gehen Sie davon aus, daß der Server für eine lange Zeit laufen wird. Sie werden ihn dann nicht von der Kommandozeile aus unterbrechen können. Wenn Sie sich für serve_forever entschieden haben, müssen Sie im Prinzip eine Vorrichtung fest einbauen, um Ihren Server damit anzuhalten – entweder, indem Sie ihm über das Netzwerk einen speziellen Befehl senden, oder indem Sie eine Sperrdatei oder einen anderen Schalter im Dateisystem des Servers umlegen. Natürlich können Sie immer auch den Prozeß ausfindig machen und ihn abschießen (unter Unix mit kill -9 oder unter Windows mit einem kurzen Ausflug zum Task Manager).

Bedienen eines GET

Wenn Sie tatsächlich einen Anfrage-Handler implementieren, liegt es an Ihnen, do_GET und do_POST zu implementieren. Eine GET-Operation ist am einfachsten, da das meiste an Informationen, die Sie möglicherweise brauchen, in den Headern steckt.

class myRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):

Für Neulinge: Sie müssen Ihren Anfrage-Handler (myRequestHandler) von der Klasse BaseHTTPServer.BaseHTTPRequestHandler ableiten. Nach diesem ersten Schritt können Sie einige der Methoden von HTTPServer benutzen, um eine Antwort an den Benutzer zurückzuschicken.

self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers( )

Nach dem Beenden der Header mit einem Aufruf von end_headers können Sie die write-Datei benutzen, um Daten an den Browser zu senden.

self.wfile.write("<html><body>")
self.wfile.write("<b>Hallo</b>")
self.wfile.write("</html></body>")

Der tatsächlich angeforderte URI wäre im Pfad-Attribut des Anfrage-Handlers verfügbar (self.path im vorherigen Beispiel). Sie könnten diese Pfad-Information dazu benutzen, um eine spezielle Datei zurückzugeben oder um eine spezielle Methode eines bestimmten Objekts aufzurufen. Die restlichen Client-Informationen der Header sind im Header-Dictionary untergebracht.

Bedienen eines POST

Eine POST-Operation zu implementieren ist ein wenig komplizierter, und man muß dazu wissen, wie das HTTP-Protokoll während einer POST-Operation aussieht. Der wichtigste Unterschied ist der, daß der Server nach dem Lesen der Browser- (oder Client-) Header herausfinden muß, wie viele Bytes er vom Client-Socket lesen muß, die eine POST-Operation ausmachen. Dazu geben alle POST-Operationen eines Browsers einen Header namens content-length an, aus dem die Größe der Datenmenge entnommen wird.

Die grundlegenden Methodenaufrufe in Zusammenhang mit POST sind hier angegeben:

def do_POST(self):    

    self.send_response(200)
    self.send_header("Content-type", "text/html")
    self.end_headers( )

    self.wfile.write("<html><body>")
    self.printBrowserHeaders( )
    self.wfile.write("<b>Post-Daten:</b><br>")

    if self.headers.dict.has_key("content-length"):

        content_length = string.atoi(self.headers.dict["content-length"])

        raw_post_data = self.rfile.read(content_length)
        self.wfile.write(raw_post_data)

Dem können Sie entnehmen, daß die Länge der gesendeten Daten in einem Header namens content-length angegeben und mit den restlichen Client-Headern übertragen wird. Natürlich muß der Wert von content-length von einem String in eine Ganzzahl konvertiert werden, bevor er in einer read-Operation auf der Eingabedatei benutzt werden kann.

Einen kompletten Server bauen

Anhand eines Beispiels können Sie am besten lernen, welche Schritte notwendig sind, um Ihren eigenen Server zu implementieren. Im Beispiel HTTPServer.py werden das Schreiben der HTTP-Antwort und das Parsen des Eingabe-Headers zur Rückgabe in HTML in Funktionsaufrufe ausgelagert, damit sowohl die do_GET- als auch die do_POST-Methode Zugriff darauf haben. Die Funktion printBrowserHeaders iteriert einfach über die Browser-Header und formatiert sie in HTML, um sie anschließend über den Socket zurückzuschreiben:

def printBrowserHeaders(self):   
   # Iteriere durch Header-Dictionary und
   # zeige Namen und Wert jedes Paares an.
   self.wfile.write("<p>Kopf: <br>")
   header_keys = self.headers.dict.keys( )
   for key in header_keys:
      self.wfile.write("<b>" + key + "</b>: ")
      self.wfile.write(self.headers.dict[key] + "<br>")

Die Methode printCustomHTTPHeaders sendet einen Antwortcode an den Browser und beendet den HTTP-Header. Wenn Sie weitere Header hinzufügen wollen, sollten Sie das hier tun.

def printCustomHTTPResponse(self, respcode):

# Sende passenden HTTP-Code zurück
self.send_response(respcode)

# Teile mit, daß wir HTML senden
self.send_header("Content-type", "text/html")

# Beschreibe die verwendete Serversoftware!
self.send_header("Server", "myRequestHandler :-)")

# Beende Header
self.end_headers( )

Das folgende Beispiel, HTTPServer.py, gibt das komplette Listing des Servers mit den bisher vorgestellten Techniken an.

Beispiel: HTTPServer.py

"""
HTTPServer.py - eine einfache Implementierung
auf Basis des BaseHTTPServer-Moduls
"""
from BaseHTTPServer import HTTPServer
from BaseHTTPServer import BaseHTTPRequestHandler
import string
"""
Klasse myRequestHandler - Behandelt alle eingehenden Anfragen,
unabhängig von Pfad, Datei oder Anfragemethode (GET/POST)
"""
class myRequestHandler(BaseHTTPRequestHandler):
"""
do_GET wird aufgerufen, wenn der Browser eine GET-Anfrage stellt.
"""
def do_GET(self):
# Gib immer ein 200/OK zurück
self.printCustomHTTPResponse(200)
# Beginne HTML-Ausgabe
self.wfile.write("<html><body>")
self.wfile.write("<p>Hallo, ich bin eine Art Webserver.</p>")
self.wfile.write("<p>GET-String: " + self.path + "</p>")
# Zeige Browser-Header
self.printBrowserHeaders( )
# Beende HTML
self.wfile.write("</html></body>")
"""
do_POST wird aufgerufen, wenn der Browser Daten aus einem Formular POSTet
"""
def do_POST(self):
# Sende ein 200/OK zurück
self.printCustomHTTPResponse(200)

# Beginne HTML und zeige erneut Browser-Header
self.wfile.write("<html><body>")
self.printBrowserHeaders( )
self.wfile.write("<b>Post-Daten:</b><br>")

# Bestimme Länge der POST-Daten, damit Sie die korrekte Anzahl von Bytes
# einlesen können. Die Länge der POST-Daten ist im Browser-Header
# namens 'content-length' angegeben.
if self.headers.dict.has_key("content-length"):
# Konvertiere content-length von String nach Ganzzahl
content_length = string.atoi(self.headers.dict["content-length"])

# Lies die korrekte Anzahl von Bytes von der Client-Verbindung
# und sende sie zum Browser zurück
raw_post_data = self.rfile.read(content_length)
self.wfile.write(raw_post_data)

# Beende HTML
self.wfile.write("</html></body>")

"""
printBrowserHeaders - diese Methode gibt die vom Client gesendeten HTTP-Header aus
"""
def printBrowserHeaders(self):
# Iteriere durch Header-Dictionary und zeige Namen und Wert jedes Paares an.
self.wfile.write("<p>Kopf: <br>")
header_keys = self.headers.dict.keys( )
for key in header_keys:
self.wfile.write("<b>" + key + "</b>: ")
self.wfile.write(self.headers.dict[key] + "<br>")

"""
printCustomHTTPResponse - diese Methode nimmt einen Antwortcode und
sendet den Code und spezielle Header an den Browser zurück
"""
def printCustomHTTPResponse(self, respcode):

# Sende passenden HTTP-Code zurück
self.send_response(respcode)

# Teile mit, daß wir HTML senden
self.send_header("Content-type", "text/html")

# Beschreibe die verwendete Serversoftware!
self.send_header("Server", "myRequestHandler :-)")

# Beende Header
self.end_headers( )

# Um den Server auf Port 2112 zu starten, müssen die Browser-URLs

# als http://servername:2112/pathtofile adressiert sein
myServer = HTTPServer(('', 2112), myRequestHandler)

# Behandle 5 Anfragen in einer Schleife
for lp in range(5):
   myServer.handle_request( )

Ausführen einer GET-Anfrage

Um Ihren neuen Server zu starten, rufen Sie einfach Python mit dessen Namen auf:

 C:\WINDOWS\Desktop\oreilly\pythonxml\c8>python HTTPServer.py 

Die Eingabeaufforderung wird nicht wieder erscheinen, da der Server so lange läuft, bis er fünf Anfragen bedient hat. Wenn Sie den Server ausführen wollen, um die GET-Funktionalität zu testen, starten Sie einfach Ihren Browser und geben Sie den Namen Ihres Servers sowie den Port 2112 an ("http://hostname:2112"), um eine Antwort zu bekommen. Es spielt keine Rolle, welchen Pfad Sie benutzen, da er vom Server ignoriert wird. Die folgende Abbildung zeigt, wie ein Browser eine GET-Anfrage beim Server mit der URL "http://localhost:2112/this/path/and/pagename/are/made.up?its=true" durchführt.
Der einzige kritische Teil der URL ist die host:port-Kombination in Form von "http://localhost:2112".

Ein Browser, der sich über eine GET-Anfrage mit dem Server verbindet

Abbildung: Ein Browser, der sich über eine GET-Anfrage mit dem Server verbindet

Ausführen einer POST-Anfrage

Bei einer POST-Operation wird ein Webformular benötigt. Die einfache, im nächsten Beispiel testServer.html gezeigte HTML-Datei testServer.html sollte ausreichen. Sie muß nicht auf einem Webserver plaziert werden, sondern nur in Ihrem Browser geladen werden, wie Sie auch eine Datei laden würden. Wenn Sie den Abschicken-Knopf drücken, versucht das Formular, seine Daten mit POST an Ihre Datei HTTPServer.py zu schicken, die auf Port 2112 läuft. Das folgende Beispiel zeigt die Auszeichnungen für testServer.html.

Beispiel: testServer.html

<html>
  <body>
    <form action="http://localhost:2112/dieser/Teil/ist/egal" method="POST">
      <textarea rows=10 cols=40 name="textdata">Diese Beispieldaten kann man abschicken, wenn Sie wollen.</textarea><br>
      <input type="submit" value="Abschicken">
    </form>
  </body>
</html>

Die folgende Abbildung zeigt das Webformular aus dem Beispiel testServer.html, wie es von Ihrem Browser mit seinem Standardtext angezeigt wird. Sie müssen bei diesem Webformular unbedingt daran denken, daß sein action-Attribut auf Port 2112 zeigt, statt nur auf Port 80.

Ein Webformular zum Testen des Servers

Abbildung: Ein Webformular zum Testen des Servers

Wenn Sie das Formular abschicken, werden Pfad und Dateiname wie bei der GET-Anfrage vom Server ignoriert, und HTTPServer.py liest statt dessen einfach mit rfile die Daten aus Ihrer Anfrage. Der Header content-length wird vom Browser gesendet, um dem Server mitzuteilen, wie viele Bytes er vom Socket zu lesen versuchen soll. Die folgende Abbildung zeigt die Antwort des Servers. Sie ist ähnlich zur GET-Antwort, aber Ihre POST-Daten sind ebenfalls vorhanden und korrekt URL-codiert.

Eine Antwort auf ein abgeschicktes Formular

Abbildung: Eine Antwort auf ein abgeschicktes Formular

In diesem Abschnitt haben wir viele der Internet-Subsysteme miteinander verknüpft, die Ihnen in Python zur Verfügung stehen. Wenn Sie Anwendungen schreiben, die mit XML in verteilten Systemen arbeiten, ist es wichtig zu verstehen, wie die verschiedenen Subsysteme zusammenarbeiten. Ein Verständnis davon, wie Webserver operieren, wie URLs codiert und decodiert werden, ebenso wie eine Vorstellung vom Wesen von Threads und Sockets und ihrer Rolle bei der Netzwerkprogrammierung geben Ihnen die Möglichkeit, verteilte Systeme und XML mit einer robusteren Werkzeugsammlung zusammenzubauen. XML ist noch immer eine sehr neue Technologie, und sie effektiv umzusetzen verlangt ein ausgeprägtes Verständnis der verteilten Netzwerkumgebung, in der es benutzt wird, inklusive Protokollen, Sockets, Threads und anderen APIs. Auf den nächsten Seiten erkunden wir Webdienste und SOAP. Mit der aufkommenden Python-Unterstützung für SOAP wird Ihnen das in diesem Abschnitt Gelernte sehr gelegen kommen.

  

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Python & XML bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2002 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Python & XML" 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.

O’Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de