Aufbau einer Brücke zwischen Anwendungen und Servern
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Einleitung: Die unsichtbaren Helfer von Webanwendungen
Als Python-Entwickler erstellen wir oft Webanwendungen, die verschiedene Anfragen bearbeiten, von der einfachen Bereitstellung statischer Dateien bis hin zu komplexen API-Interaktionen. Hinter den Kulissen, eingebettet zwischen unserer Anwendungslogik und dem Webserver, befindet sich eine entscheidende Schicht, die für viele gängige Aufgaben zuständig ist: Middleware. Middleware fängt Anfragen und Antworten ab und ermöglicht es uns, erhebliche Funktionalitäten wie Protokollierung, Authentifizierung, Caching oder sogar Datenmanipulation hinzuzufügen, ohne unseren Kernanwendungscode zu überladen. Dieser modulare Ansatz hält unsere Anwendungen nicht nur sauber und wartbar, sondern fördert auch die Wiederverwendbarkeit über verschiedene Projekte hinweg. In diesem Artikel werden wir den Zauber lüften und die Mechanik dieses leistungsstarken Konzepts enthüllen, indem wir eine einfache WSGI- oder ASGI-Middleware von Grund auf neu erstellen.
Die Säulen verstehen: WSGI, ASGI und Middleware
Bevor wir uns dem Code zuwenden, wollen wir die Kernkonzepte, die unserer Reise zugrunde liegen, klar verstehen.
Was ist WSGI?
WSGI steht für Web Server Gateway Interface. Es ist eine Python-Spezifikation, die eine standardisierte Schnittstelle zwischen Webservern (wie Gunicorn, uWSGI) und Webanwendungen oder Frameworks (wie Flask, Django) definiert. Der Server ruft eine aufrufbare WSGI-Anwendung auf, die zwei Argumente akzeptiert:
environ: Ein Wörterbuch, das CGI-ähnliche Umgebungsvariablen, Webservervariablen und HTTP-Header enthält.start_response: Eine aufrufbare Funktion, die die Anwendung verwendet, um dem Server den HTTP-Status und die Header zuzusenden.
Die Anwendung gibt dann ein Iterable von Byte-Strings zurück, das den Antwortkörper darstellt.
Was ist ASGI?
ASGI steht für Asynchronous Server Gateway Interface. Es ist der moderne Nachfolger von WSGI, der zur Unterstützung asynchroner Operationen, Websockets und HTTP/2 entwickelt wurde. Ähnlich wie WSGI definiert ASGI eine standardisierte Schnittstelle zwischen asynchronen Webservern (wie Uvicorn, Hypercorn) und asynchronen Webanwendungen (wie FastAPI, Starlette). Eine ASGI-Anwendung ist ein asynchroner Aufrufer, der drei Argumente entgegennimmt:
scope: Ein Wörterbuch mit Informationen über die spezifische Verbindung, einschließlich Typ (z. B.'http','websocket'),method,pathund Header.receive: Eine aufrufbare Funktion, die mitawaitaufgerufen werden kann und es der Anwendung ermöglicht, Ereignisnachrichten vom Server zu empfangen (z. B. Anforderungskörperteile, Websocket-Nachrichten).send: Eine aufrufbare Funktion, die mitawaitaufgerufen werden kann und es der Anwendung ermöglicht, Ereignisnachrichten an den Server zu senden (z. B. Antwortstatus, Header, Antwortkörperteile, Websocket-Nachrichten).
Was ist Middleware?
Im Kontext von WSGI und ASGI ist Middleware im Wesentlichen eine WSGI- oder ASGI-Anwendung selbst, aber mit einem entscheidenden Unterschied: Sie umschließt eine andere WSGI- oder ASGI-Anwendung. Dieses Umschließen ermöglicht es der Middleware, Anfragen abzufangen, bevor sie die innere Anwendung erreichen, und Antworten, nachdem sie diese verlassen haben. Betrachten Sie es als einen Funktions-Decorator für Webanwendungen, der übergreifende Belange hinzufügt.
Erstellung einer einfachen WSGI-Middleware
Beginnen wir mit der Erstellung einer einfachen WSGI-Middleware, die den eingehenden Anforderungspfad protokolliert.
Die innere WSGI-Anwendung
Zuerst benötigen wir eine einfache WSGI-Anwendung, die unsere Middleware umschließen kann.
# app.py def simple_app(environ, start_response): """Eine sehr einfache WSGI-Anwendung.""" status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [b"Hello from the simple app!"] if __name__ == '__main__': from wsgiref.simple_server import make_server httpd = make_server('', 8000, simple_app) print("Serving on port 8000...") httpd.serve_forever()
Führen Sie dies mit python app.py aus und greifen Sie auf http://localhost:8000 zu, um "Hello from the simple app!" zu sehen.
Entwurf der Protokollierungs-Middleware
Nun erstellen wir unsere Protokollierungs-Middleware. Eine WSGI-Middleware muss beim Initialisieren die nächste WSGI-Anwendung in der Kette als Argument akzeptieren. Ihre __call__-Methode implementiert dann die WSGI-Schnittstelle.
# middleware.py import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') class RequestLoggerMiddleware: def __init__(self, app): """ Initialisiert die Middleware mit der nächsten WSGI-Anwendung in der Kette. """ self.app = app def __call__(self, environ, start_response): """ Die WSGI-Schnittstellenmethode für die Middleware. Fängt die Anfrage ab, protokolliert sie, gibt sie dann an die umschlossene Anwendung weiter und gibt schließlich deren Antwort zurück. """ path = environ.get('PATH_INFO', '/') method = environ.get('REQUEST_METHOD', 'GET') logging.info(f"Request received: {method} {path}") # Ruft die start_response der umschlossenen Anwendung auf und erfasst sie # Dies ist entscheidend für Middleware, die Header oder Status modifiziert _headers = [] _status = None def wrapped_start_response(status, headers, exc_info=None): nonlocal _status, _headers _status = status _headers = headers logging.info(f"Response status: {status}") return start_response(status, headers, exc_info) # Übergibt die Anfrage an die umschlossene Anwendung response_body = self.app(environ, wrapped_start_response) # Die Middleware kann hier auch die response_body inspizieren oder ändern # Für diesen einfachen Logger geben wir sie unverändert zurück. return response_body
Integration der Middleware
Schließlich können wir unsere RequestLoggerMiddleware mit unserer simple_app integrieren.
# main.py from wsgiref.simple_server import make_server from app import simple_app from middleware import RequestLoggerMiddleware if __name__ == '__main__': # Wickeln Sie unsere simple_app mit der Logger-Middleware ein application_with_middleware = RequestLoggerMiddleware(simple_app) httpd = make_server('', 8000, application_with_middleware) print("Serving application with middleware on port 8000...") httpd.serve_forever()
Wenn Sie python main.py ausführen und http://localhost:8000 aufrufen, sehen Sie eine Protokollnachricht in Ihrer Konsole, die die Anfrage anzeigt, gefolgt von der Antwort "Hello from the simple app!" in Ihrem Browser. Dies zeigt, wie die Middleware die Anfrage abfängt, ihre Protokollierungsaufgabe ausführt und die Anfrage dann an die Anwendung weiterleitet.
Erstellung einer einfachen ASGI-Middleware
Lassen Sie uns nun eine ähnliche Protokollierungsfunktionalität mithilfe von ASGI-Middleware implementieren. Die asynchrone Natur von ASGI erfordert einen etwas anderen Ansatz.
Die innere ASGI-Anwendung
Wir verwenden eine einfache ASGI-Anwendung.
# async_app.py async def simple_async_app(scope, receive, send): """Eine sehr einfache ASGI-Anwendung.""" if scope['type'] == 'http': await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ], }) await send({ 'type': 'http.response.body', 'body': b"Hello from the async app!", }) elif scope['type'] == 'websocket': # Einfacher Handler für WebSocket-Verbindungen await send({"type": "websocket.accept"}) while True: message = await receive() if message['type'] == 'websocket.disconnect': break await send({"type": "websocket.send", "text": f"Echo: {message.get('text')}"})
Um dies auszuführen, würden Sie typischerweise einen ASGI-Server wie Uvicorn verwenden:
uvicorn async_app:simple_async_app --port 8000 --reload
Entwurf der Protokollierungs-ASGI-Middleware
Eine ASGI-Middleware ist beim Initialisieren (oft __init__) ein asynchroner Aufrufer, der die nächste ASGI-Anwendung akzeptiert. Ihre __call__-Methode muss ebenfalls async def sein.
# async_middleware.py import logging import time logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') class AsyncRequestLoggerMiddleware: def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): if scope['type'] == 'http': start_time = time.monotonic() path = scope.get('path', '/') method = scope.get('method', 'GET') logging.info(f"ASGI Request Received: {method} {path}") # Definieren Sie eine benutzerdefinierte send-Funktion, um Antwortdetails abzufangen async def wrapped_send(message): if message['type'] == 'http.response.start': status_code = message['status'] logging.info(f"ASGI Response Status: {status_code}") await send(message) # Leitet die Nachricht an die ursprüngliche send-Funktion weiter await self.app(scope, receive, wrapped_send) end_time = time.monotonic() duration = (end_time - start_time) * 1000 # in Millisekunden logging.info(f"ASGI Request Processed: {method} {path} - Duration: {duration:.2f}ms") else: # Für Nicht-HTTP-Verbindungen (z. B. WebSockets) wird einfach durchgereicht await self.app(scope, receive, send)
Im Gegensatz zu WSGI start_response ist ASGI send ein asynchroner Strom von Nachrichten. Um Antwortdetails wie den Status abzufangen, umschließen wir den vom Server bereitgestellten send-Callable.
Integration der asynchronen Middleware
Nun umschließen wir unsere simple_async_app mit AsyncRequestLoggerMiddleware.
# async_main.py from async_app import simple_async_app from async_middleware import AsyncRequestLoggerMiddleware # Wickeln Sie die asynchrone App mit der Logger-Middleware ein application_with_async_middleware = AsyncRequestLoggerMiddleware(simple_async_app) if __name__ == '__main__': import uvicorn # Uvicorn erwartet direkt eine ASGI-Anwendung uvicorn.run(application_with_async_middleware, host="0.0.0.0", port=8000)
Um dies auszuführen, verwenden Sie python async_main.py. Greifen Sie in Ihrem Browser auf http://localhost:8000 zu. Sie werden in Ihrer Konsole Protokollmeldungen sowohl für die eingehende als auch für die ausgehende Antwort sowie für die Verarbeitungsdauer beobachten. Die ASGI-Middleware zeigt ihre Fähigkeit, den Anfrage-Antwort-Zyklus asynchron abzufangen, zu protokollieren und zu zeitlich zu erfassen.
Häufige Anwendungsszenarien
Middleware ist unglaublich vielseitig und bildet das Rückgrat vieler Funktionen von Webanwendungen:
- Authentifizierung/Autorisierung: Überprüfung der Anmeldedaten des Benutzers, bevor der Zugriff auf bestimmte Routen gewährt wird.
- Protokollierung: Wie gezeigt, Nachverfolgung von Anfragen, Antworten und Fehlern.
- CORS (Cross-Origin Resource Sharing): Hinzufügen geeigneter Header, um plattformübergreifende Anfragen zuzulassen oder einzuschränken.
- Komprimierung: Gzip- oder Brotli-Kodierung von Antworten zur Reduzierung der Bandbreite.
- Ratenbegrenzung: Missbrauch verhindern, indem die Anzahl der Anfragen von einer einzelnen Quelle begrenzt wird.
- Fehlerbehandlung: Abfangen von Ausnahmen und Zurückgeben benutzerfreundlicher Fehlerseiten.
- Sitzungsverwaltung: Verwaltung von Benutzersitzungen über mehrere Anfragen hinweg.
Durch das Verständnis, wie Middleware erstellt wird, erhalten Sie die Möglichkeit, diese Funktionalitäten auf eine saubere, entkoppelte und wiederverwendbare Weise zu implementieren, wodurch Ihre Python-Webanwendungen robuster und wartbarer werden.
Fazit: Webanwendungsentwicklung stärken
In diesem Artikel haben wir WSGI- und ASGI-Middleware entmystifiziert und ihre grundlegenden Rollen als Abfang- und Erweiterungsmechanismen für Webanfragen und -antworten veranschaulicht. Indem wir den Python-Schnittstellen folgten und unsere eigenen einfachen Protokollierungsbeispiele implementierten, haben wir gesehen, wie diese leistungsstarken Muster die Einspeisung übergreifender Anliegen wie Protokollierung oder Sicherheit ermöglichen, ohne die Logik über unseren Kernanwendungscode zu verteilen. Middleware fungiert als unverzichtbares Werkzeug und verbessert die Modularität, Wartbarkeit und Wiederverwendbarkeit von Python-Webanwendungen erheblich.

