Kapitel 4
HTTP selbst ausliefern
Die Verbindung umdrehen
In den Kapiteln 1 bis 3 hast du Anfragezeilen geschrieben und Antwortzeilen gelesen. practice-api hat die andere Hälfte übernommen, und du konntest kein Byte von dem ändern, was es zurückgesendet hat. Dieses Kapitel dreht die Verbindung um. Ein Server liest die Anfragezeilen, die ein Client geschrieben hat, entscheidet, was er antwortet, und schreibt die Antwortzeilen, die dieser Client lesen wird. Er tut diese drei Dinge einmal pro Verbindung, auf einem accept bekommen hat, so wie dein Pinnwand-Server in Networks with Python. Der Unterschied ist der Text auf dem Socket:
Dieselben Zeilen, vom anderen Ende
Folge einer Anfrage von dem Client, den du gebaut hast, durch einen Server und zurück:

Dein Client aus Kapitel 1 schreibt GET /books HTTP/1.1, Host: books.test und eine Leerzeile. Auf dem Server liest Pythons http.server die Anfragezeile und die Header vom Socket, und eine kleine Datei, die mit diesem Kapitel mitgeliefert wird, der Adapter, macht daraus ein Anfrage-handle(request, state), entscheidet über die Antwort und gibt sie als Antwort-Dict zurück. Der Adapter übergibt dieses Dict an http.server, das es als Bytes zurückschreibt: die Statuszeile HTTP/1.0 200 OK, seine eigenen Zeilen Server und Date, den Header content-length: 13, eine Leerzeile und den Body {"books": []}. Dein Client liest diese Zeilen genau so, wie er es in Kapitel 1 getan hat.
Hier sind die lesende und die schreibende Hälfte von Hand erledigt, auf mitgeschnittenen Bytes. Das läuft in deinem Browser, also ist kein Socket beteiligt; jedes Byte ist ein Literal:
Die erste Hälfte liest: Sie trennt den Kopf an der Leerzeile ab, zerlegt ihn in Zeilen und zerlegt die Anfragezeile in content-length, die ein Server davor senden würde. In diesem Kapitel erledigen http.server und die mitgelieferte Datei beide Hälften für dich. Das Entscheiden in der Mitte ist deine Sache.
Wer was tut
Drei Teile teilen sich die Arbeit:
http.serverliest die Anfrage-Bytes vom Socket und schreibt die Antwort-Bytes zurück. Es prüft, dass die Anfragezeile wohlgeformt ist, und findet die Methode.http_adapter.py, mitgeliefert in deinem Arbeitsbereich, macht aus dem, washttp.servergelesen hat, ein Anfrage-Dict, ruft deine Funktion auf und gibt das Antwort-Dict anhttp.serverzum Schreiben zurück:send_responseschreibt die Statuszeile,send_headerschreibt jede, end_headersschreibt die Leerzeile, und dann geht der Body unverändert hinaus.Deine Funktion,
handle(request, state), entscheidet. Sie bekommt ein Anfrage-Dict und ein State-Dict mit den Büchern und gibt ein Antwort-Dict zurück. Sie berührt nie einen Socket.
Die beiden Dicts sehen so aus:
request = {
"method": "GET",
"target": "/books?limit=2",
"version": "HTTP/1.1",
"headers": {"host": ["books.test"], "accept": ["application/json"]},
"body": b"",
}
response = {
"status": 200,
"headers": {"content-type": "application/json", "content-length": "13"},
"body": b'{"books": []}',
}
print(request["method"], request["target"])
print(response["status"], response["headers"]["content-length"], response["body"])
Headernamen sind in beiden Dicts kleingeschrieben. In der Anfrage zeigt jeder Name auf eine parse_head in Kapitel 1 gebaut hat, sodass ein zweimal gesendeter Header beide Werte behält. Der Body besteht aus Bytes, weil Bytes das sind, was übertragen wird.
Was auf die Leitung geht
Der Adapter schreibt genau das, was dein Antwort-Dict sagt. http.server fügt direkt nach der Statuszeile zwei eigene Headerzeilen hinzu: Server, die die Bibliothek und die Python-Version nennt, und Date, den Zeitpunkt der Antwort. Sonst wird nichts hinzugefügt und nichts entfernt. Wenn dein Dict einen Body hat und kein content-length, geht die Antwort ohne Länge hinaus. Unter HTTP/1.0 markiert dann das Schließen der Verbindung durch den Server das Ende des Bodys, aber ein Client, der die Länge braucht, etwa das wire.py, das du in Kapitel 1 geschrieben hast, lehnt diese Antwort ab. Die Bytes des Bodys zu zählen ist also deine Aufgabe.
Der Adapter spricht bis Kapitel 9 HTTP/1.0. Seine Statuszeile beginnt mit HTTP/1.0, auch wenn der Client HTTP/1.1 geschrieben hat, und der Server schließt die Verbindung nach einer Antwort, also trägt jede Verbindung genau eine Anfrage.
Den Adapter lesen
Hier ist die Mitte von make_handler_class in http_adapter.py:
class Handler(http.server.BaseHTTPRequestHandler):
timeout = SOCKET_TIMEOUT
def respond(self):
request: dict = request_record(self)
if read_body is not None:
error = read_body(request, self.rfile.read)
if error is not None:
write_response(self, error)
return
write_response(self, handle(request, state))
def do_GET(self):
self.respond()
def do_HEAD(self):
self.respond()
def do_POST(self):
self.respond()
def do_PUT(self):
self.respond()
def do_DELETE(self):
self.respond()
Er benutzt drei Konstrukte, die deine bisherigen Kurse nicht unterrichtet haben. Jedes braucht ein, zwei Sätze, um es zu lesen.
class Handler(http.server.BaseHTTPRequestHandler): definiert eine neue Art von Handler, aufgebaut auf BaseHTTPRequestHandler aus http.server. Weil es darauf aufgebaut ist, weiß http.server, wie es benutzt wird: Für jede Verbindung erzeugt es einen frischen Handler und ruft seine Methoden auf, etwa do_GET für eine Anfrage GET. Eine Anfrage, deren Methode hier keine do_-Methode hat, etwa PATCH, bekommt eine Antwort 501 von http.server selbst, und deine Funktion läuft nie.
Jedes def innerhalb der Klasse nimmt zuerst self. self ist der Handler für die eine Anfrage, die gerade beantwortet wird, also ist self.command die Methode dieser Anfrage (request_record liest sie als handler.command), und self.respond() ruft eine andere seiner Methoden auf.
Das : dict nach request ist eine Annotation, ein Hinweis an den Leser, dass dieser Name ein Dict enthält. Python prüft sie nicht; sie zu löschen würde nichts ändern.
Python Fundamentals II unterrichtet Klassen, self und Annotationen vollständig. Dieser Kurs verlangt nie, dass du eines davon schreibst: Alles, was du schreibst, ist eine einfache Funktion. make_handler_class baut diese Klasse um dein handle und deinen state herum und gibt sie zurück; in Lektion 3 übergibst du diese Klasse an http.server.HTTPServer. Die Zeilen mit read_body bleiben bis Kapitel 5 unbenutzt.
Welcher Schritt ist bei dem Server dieses Kurses deiner?
Dein Antwort-Dict hat einen Body, aber kein content-length. Was erreicht den Client?
Ein Client sendet PATCH /books. Was passiert?
Der Adapter schreibt eine Zeile pro Header. Was gibt das aus?
response = {"status": 200, "headers": {"content-type": "application/json", "content-length": "2"}, "body": b"[]"}
lines = []
for name in response["headers"]:
lines.append(name + ": " + response["headers"][name])
print(len(lines), lines[1])
Noch beantwortet nichts in deinem Arbeitsbereich eine Anfrage: handle löst noch NotImplementedError aus. Die nächste Lektion schreibt es.