WSGI: Die Server-Application-Schnittstelle für Python

Veröffentlicht: 2022-03-11

1993 steckte das Internet mit rund 14 Millionen Nutzern und 100 Websites noch in den Kinderschuhen. Die Seiten waren statisch, aber es bestand bereits die Notwendigkeit, dynamische Inhalte wie aktuelle Nachrichten und Daten zu erstellen. Als Reaktion darauf implementierten Rob McCool und andere Mitwirkende das Common Gateway Interface (CGI) im HTTPd-Webserver des National Center for Supercomputing Applications (NCSA) (dem Vorläufer von Apache). Dies war der erste Webserver, der Inhalte bereitstellen konnte, die von einer separaten Anwendung generiert wurden.

Seitdem ist die Zahl der Nutzer im Internet explodiert und dynamische Websites sind allgegenwärtig. Wenn Entwickler zum ersten Mal eine neue Sprache lernen oder sogar zum ersten Mal Programmieren lernen, möchten sie bald wissen, wie sie ihren Code in das Web einbinden können.

Python im Web und der Aufstieg von WSGI

Seit der Gründung von CGI hat sich viel verändert. Der CGI-Ansatz wurde unpraktisch, da er bei jeder Anforderung die Erstellung eines neuen Prozesses erforderte, wodurch Speicher und CPU verschwendet wurden. Es entstanden einige andere Low-Level-Ansätze wie FastCGI](http://www.fastcgi.com/) (1996) und mod_python (2000), die verschiedene Schnittstellen zwischen Python-Webframeworks und dem Webserver bereitstellen. Als sich verschiedene Ansätze vermehrten, schränkte die Wahl des Frameworks durch den Entwickler die Auswahl an Webservern ein und umgekehrt.

Um dieses Problem anzugehen, schlug Phillip J. Eby 2003 PEP-0333 vor, das Python Web Server Gateway Interface (WSGI). Die Idee war, eine universelle Schnittstelle auf hoher Ebene zwischen Python-Anwendungen und Webservern bereitzustellen.

Im Jahr 2003 aktualisierte PEP-3333 die WSGI-Schnittstelle, um Unterstützung für Python 3 hinzuzufügen. Heutzutage verwenden fast alle Python-Frameworks WSGI als Mittel, wenn nicht das einzige Mittel, um mit ihren Webservern zu kommunizieren. So machen es Django, Flask und viele andere beliebte Frameworks.

Dieser Artikel soll dem Leser einen Einblick in die Funktionsweise von WSGI geben und ihm ermöglichen, eine einfache WSGI-Anwendung oder einen einfachen Server zu erstellen. Sie erhebt jedoch keinen Anspruch auf Vollständigkeit, und Entwickler, die beabsichtigen, produktionsreife Server oder Anwendungen zu implementieren, sollten sich die WSGI-Spezifikation genauer ansehen.

Die Python-WSGI-Schnittstelle

WSGI legt einfache Regeln fest, denen der Server und die Anwendung entsprechen müssen. Lassen Sie uns damit beginnen, dieses allgemeine Muster zu überprüfen.

Die Python-WSGI-Serveranwendungsschnittstelle.

Anwendungsschnittstelle

In Python 3.5 sehen die Anwendungsschnittstellen so aus:

 def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body]

In Python 2.7 wäre diese Schnittstelle nicht viel anders; Die einzige Änderung wäre, dass der Körper durch ein str Objekt anstelle eines bytes -Objekts dargestellt wird.

Obwohl wir in diesem Fall eine Funktion verwendet haben, reicht jede aufrufbare Funktion aus. Die Regeln für das Anwendungsobjekt hier sind:

  • Muss ein Callable mit den Parametern environ und start_response .
  • Muss den Rückruf start_response bevor der Text gesendet wird.
  • Muss ein Iterable mit Teilen des Dokumentkörpers zurückgeben.

Ein weiteres Beispiel für ein Objekt, das diese Regeln erfüllt und denselben Effekt hervorrufen würde, ist:

 class Application: def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] self.start_response(status, headers) yield body

Server-Schnittstelle

Ein WSGI-Server könnte wie folgt mit dieser Anwendung kommunizieren:

 def write(chunk): '''Write data back to client''' ... def send_status(status): '''Send HTTP status code''' ... def send_headers(headers): '''Send HTTP headers''' ... def start_response(status, headers): '''WSGI start_response callable''' send_status(status) send_headers(headers) return write # Make request to application response = application(environ, start_response) try: for chunk in response: write(chunk) finally: if hasattr(response, 'close'): response.close()

Wie Sie vielleicht bemerkt haben, hat das aufrufbare start_response ein aufrufbares write zurückgegeben, das die Anwendung verwenden kann, um Daten an den Client zurückzusenden, aber das wurde von unserem Anwendungscodebeispiel nicht verwendet. Diese write ist veraltet und wir können sie vorerst ignorieren. Es wird später im Artikel kurz besprochen.

Eine weitere Besonderheit in der Verantwortung des Servers besteht darin, die optionale close -Methode auf dem Response-Iterator aufzurufen, falls vorhanden. Wie in Graham Dumpletons Artikel hier erwähnt, handelt es sich um ein oft übersehenes Merkmal von WSGI. Das Aufrufen dieser Methode, sofern vorhanden , ermöglicht es der Anwendung, alle Ressourcen freizugeben, die sie möglicherweise noch enthält.

Das environ -Argument der aufrufbaren Anwendung

Der environ sollte ein Wörterbuchobjekt sein. Es wird verwendet, um Anfrage- und Serverinformationen an die Anwendung zu übergeben, ähnlich wie es CGI tut. Tatsächlich sind alle CGI-Umgebungsvariablen in WSGI gültig, und der Server sollte alles weitergeben, was für die Anwendung gilt.

Während viele optionale Schlüssel übergeben werden können, sind einige obligatorisch. Nehmen wir als Beispiel die folgende GET -Anfrage:

 $ curl 'http://localhost:8000/auth?user=obiwan&token=123'

Dies sind die Schlüssel, die der Server bereitstellen muss , und die Werte, die sie annehmen würden:

Taste Wert Bemerkungen
REQUEST_METHOD "GET"
SCRIPT_NAME "" Server-Setup abhängig
PATH_INFO "/auth"
QUERY_STRING "token=123"
CONTENT_TYPE ""
CONTENT_LENGTH ""
SERVER_NAME "127.0.0.1" Server-Setup abhängig
SERVER_PORT "8000"
SERVER_PROTOCOL "HTTP/1.1"
HTTP_(...) Vom Client bereitgestellte HTTP-Header
wsgi.version (1, 0) Tupel mit WSGI-Version
wsgi.url_scheme "http"
wsgi.input Dateiähnliches Objekt
wsgi.errors Dateiähnliches Objekt
wsgi.multithread False True , wenn der Server Multithreading ist
wsgi.multiprocess False True , wenn der Server mehrere Prozesse ausführt
wsgi.run_once False True , wenn der Server erwartet, dass dieses Skript nur einmal ausgeführt wird (zB: in einer CGI-Umgebung)

Die Ausnahme von dieser Regel besteht darin, dass, wenn einer dieser Schlüssel leer wäre (wie CONTENT_TYPE in der obigen Tabelle), sie aus dem Wörterbuch weggelassen werden können und angenommen wird, dass sie der leeren Zeichenfolge entsprechen.

wsgi.input und wsgi.errors

Die meisten environ -Schlüssel sind unkompliziert, aber zwei von ihnen verdienen etwas mehr Erläuterung: wsgi.input , das einen Stream mit dem Anforderungstext des Clients enthalten muss, und wsgi.errors , wo die Anwendung alle Fehler meldet, auf die sie stößt. Fehler, die von der Anwendung an wsgi.errors gesendet werden, werden normalerweise an das Fehlerprotokoll des Servers gesendet.

Diese beiden Schlüssel müssen dateiähnliche Objekte enthalten; Das heißt, Objekte, die Schnittstellen zum Lesen oder Schreiben als Streams bereitstellen, genau wie das Objekt, das wir erhalten, wenn wir eine Datei oder einen Socket in Python öffnen. Das mag zunächst schwierig erscheinen, aber zum Glück gibt uns Python gute Werkzeuge, um damit umzugehen.

Erstens, über welche Art von Streams sprechen wir? Gemäß der WSGI-Definition müssen wsgi.input und wsgi.errors bytes -Objekte in Python 3 und str -Objekte in Python 2 handhaben Schnittstelle können wir die Klasse io.BytesIO .

Wenn wir beispielsweise einen WSGI-Server schreiben, könnten wir der Anwendung den Anfragetext wie folgt zur Verfügung stellen:

  • Für Python 2.7
 import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
  • Für Python 3.5
 import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)

Wenn wir auf der Anwendungsseite eine Stream-Eingabe, die wir erhalten haben, in eine Zeichenfolge umwandeln möchten, möchten wir so etwas schreiben:

  • Für Python 2.7
 readstr = environ['wsgi.input'].read() # returns str object
  • Für Python 3.5
 readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object

Der Stream wsgi.errors sollte verwendet werden, um Anwendungsfehler an den Server zu melden, und Zeilen sollten mit einem \n enden. Der Webserver sollte die systembedingte Umstellung auf ein anderes Zeilenende übernehmen.

Das start_response-Argument des Application start_response

Das Argument start_response muss ein Callable mit zwei erforderlichen Argumenten sein, nämlich status und headers , und einem optionalen Argument, exc_info . Es muss von der Anwendung aufgerufen werden, bevor ein Teil des Körpers an den Webserver zurückgesendet wird.

Im ersten Anwendungsbeispiel am Anfang dieses Artikels haben wir den Text der Antwort als Liste zurückgegeben und haben daher keine Kontrolle darüber, wann die Liste durchlaufen wird. Aus diesem Grund mussten wir start_response aufrufen, bevor wir die Liste zurückgeben konnten.

Im zweiten haben wir start_response aufgerufen, kurz bevor wir den ersten (und in diesem Fall einzigen) Teil des Antworttexts erhalten haben. Beide Wege sind innerhalb der WSGI-Spezifikation gültig.

Von der Seite des Webservers aus sollte der Aufruf von start_response die Header nicht wirklich an den Client senden, sondern ihn verzögern, bis der Antworttext mindestens eine nicht leere Bytezeichenfolge enthält, die an den Client zurückgesendet wird. Diese Architektur ermöglicht die korrekte Meldung von Fehlern bis zum letztmöglichen Moment der Ausführung der Anwendung.

Das status -Argument von start_response

Das an den status -Callback übergebene start_response muss eine Zeichenfolge sein, die aus einem HTTP-Statuscode und einer Beschreibung besteht, die durch ein einzelnes Leerzeichen getrennt sind. Gültige Beispiele sind: '200 OK' oder '404 Not Found' .

Das headers -Argument von start_response

Das headers -Argument, das an den start_response Callback übergeben wird, muss eine Python- list von Tupeln sein, wobei jedes tuple wie folgt zusammengesetzt ist: (header_name, header_value) . Sowohl der Name als auch der Wert jedes Headers müssen Zeichenfolgen sein (unabhängig von der Python-Version). Dies ist ein seltenes Beispiel, bei dem der Typ eine Rolle spielt, da dies tatsächlich von der WSGI-Spezifikation gefordert wird.

Hier ist ein gültiges Beispiel dafür, wie ein header Argument aussehen könnte:

 response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]

Bei HTTP-Headern wird die Groß- und Kleinschreibung nicht beachtet, und wenn wir einen WSGI-kompatiblen Webserver schreiben, sollten Sie dies bei der Überprüfung dieser Header beachten. Außerdem soll die Liste der von der Anwendung bereitgestellten Header nicht vollständig sein. Es liegt in der Verantwortung des Servers, sicherzustellen, dass alle erforderlichen HTTP-Header vorhanden sind, bevor er die Antwort an den Client zurücksendet und alle Header ausfüllt, die nicht von der Anwendung bereitgestellt werden.

Das exc_info Argument von start_response

Der Callback start_response sollte ein drittes Argument exc_info , das für die Fehlerbehandlung verwendet wird. Die korrekte Verwendung und Implementierung dieses Arguments ist für Produktionswebserver und -anwendungen von größter Bedeutung, würde jedoch den Rahmen dieses Artikels sprengen.

Weitere Informationen dazu finden Sie in der WSGI-Spezifikation hier.

Der start_response Rückgabewert – Der write -Callback

Aus Gründen der Abwärtskompatibilität sollten Webserver, die WSGI implementieren, ein write Callable zurückgeben. Dieser Rückruf sollte es der Anwendung ermöglichen, Body-Response-Daten direkt an den Client zurückzuschreiben, anstatt sie über einen Iterator an den Server zu übergeben.

Trotz seines Vorhandenseins ist dies eine veraltete Schnittstelle, und neue Anwendungen sollten davon absehen, sie zu verwenden.

Generieren des Antworttexts

Anwendungen, die WSGI implementieren, sollten den Antworttext generieren, indem sie ein iterierbares Objekt zurückgeben. Bei den meisten Anwendungen ist der Antworttext nicht sehr groß und passt problemlos in den Speicher des Servers. In diesem Fall ist die effizienteste Art, alles auf einmal zu senden, mit einem Iterable mit einem Element. In besonderen Fällen, in denen es nicht möglich ist, den gesamten Körper in den Speicher zu laden, kann die Anwendung ihn teilweise über diese iterierbare Schnittstelle zurückgeben.

Hier gibt es nur einen kleinen Unterschied zwischen dem WSGI von Python 2 und Python 3: In Python 3 wird der Response-Body durch bytes -Objekte repräsentiert; in Python 2 ist der richtige Typ dafür str .

Das Konvertieren von UTF-8-Strings in bytes oder str ist eine einfache Aufgabe:

  • Python 3.5:
 body = 'unicode stuff'.encode('utf-8')
  • Python 2.7:
 body = u'unicode stuff'.encode('utf-8')

Wenn Sie mehr über die Unicode- und Bytestring-Behandlung von Python 2 erfahren möchten, gibt es ein nettes Tutorial auf YouTube.

Webserver, die WSGI implementieren, sollten auch den write -Callback für die Abwärtskompatibilität unterstützen, wie oben beschrieben.

Testen Ihrer Anwendung ohne Webserver

Mit einem Verständnis dieser einfachen Schnittstelle können wir leicht Skripte erstellen, um unsere Anwendungen zu testen, ohne tatsächlich einen Server starten zu müssen.

Nehmen Sie zum Beispiel dieses kleine Skript:

 from io import BytesIO def get(app, path = '/', query = ''): response_status = [] response_headers = [] def start_response(status, headers): status = status.split(' ', 1) response_status.append((int(status[0]), status[1])) response_headers.append(dict(headers)) environ = { 'HTTP_ACCEPT': '*/*', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_USER_AGENT': 'TestAgent/1.0', 'PATH_INFO': path, 'QUERY_STRING': query, 'REQUEST_METHOD': 'GET', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'TestServer/1.0', 'wsgi.errors': BytesIO(b''), 'wsgi.input': BytesIO(b''), 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0), } response_body = app(environ, start_response) merged_body = ''.join((x.decode('utf-8') for x in response_body)) if hasattr(response_body, 'close'): response_body.close() return {'status': response_status[0], 'headers': response_headers[0], 'body': merged_body}

Auf diese Weise könnten wir beispielsweise einige Testdaten und Mock-Module in unserer App initialisieren und GET -Aufrufe durchführen, um zu testen, ob sie entsprechend antwortet. Wir können sehen, dass es sich nicht um einen echten Webserver handelt, sondern auf vergleichbare Weise mit unserer App interagiert, indem er der Anwendung einen start_response Callback und ein Wörterbuch mit unseren Umgebungsvariablen zur Verfügung stellt. Am Ende der Anfrage verbraucht es den Response-Body-Iterator und gibt einen String mit seinem gesamten Inhalt zurück. Ähnliche Methoden (oder eine allgemeine) können für verschiedene Arten von HTTP-Anforderungen erstellt werden.

Einpacken

WSGI ist ein wichtiger Bestandteil fast aller Python-Webframeworks.

In diesem Artikel haben wir uns nicht damit befasst, wie WSGI mit Datei-Uploads umgeht, da dies als eine „fortgeschrittenere“ Funktion angesehen werden könnte, die nicht für einen Einführungsartikel geeignet ist. Wenn Sie mehr darüber wissen möchten, werfen Sie einen Blick auf den PEP-3333-Abschnitt, der sich auf die Dateiverwaltung bezieht.

Ich hoffe, dass dieser Artikel hilfreich ist, um ein besseres Verständnis dafür zu vermitteln, wie Python mit Webservern kommuniziert, und Entwicklern ermöglicht, diese Schnittstelle auf interessante und kreative Weise zu verwenden.

Danksagungen

Ich möchte meinem Lektor Nick McCrea dafür danken, dass er mir bei diesem Artikel geholfen hat. Durch seine Arbeit wurde der Originaltext viel klarer und einige Fehler blieben nicht unkorrigiert.