Kapitel 9 · Übung
Eine Verbindung, viele Anfragen
Die Verbindung offen halten
Bisher hat jeder Austausch in diesem Kurs eine Verbindung geöffnet, eine Anfrage und eine Antwort transportiert und die Verbindung geschlossen. Dein Server spricht
python pl_http.py keep-alive
Sein erster Teil ist der eigene Client von pl_http. Er erstellt eine einzige http.client.HTTPConnection und sendet drei Anfragen darüber: GET /books, HEAD /books und GET /books/1, die letzte mit dem Header Connection: close. Die Mitte des Berichts sagt, was passiert ist:
--- pl_http's own http.client connection ---
replies: ['HTTP/1.0 200 OK', 'HTTP/1.0 200 OK', 'HTTP/1.0 200 OK']
connections: 3
Ein Verbindungsobjekt, drei Verbindungen: Nach jeder HTTP/1.0-Antwort hat der Server geschlossen, und http.client hat sich stillschweigend neu verbunden.
Ein Argument
Öffne noch einmal http_adapter.py. make_handler_class hat schon lange einen
def make_handler_class(handle, state, *, read_body=None, protocol_version="HTTP/1.0"):
...
Handler.protocol_version = protocol_version
return Handler
http.server liest protocol_version aus der Handler-Klasse. Mit "HTTP/1.0" beantwortet er eine Anfrage und schließt. Mit "HTTP/1.1" schreibt er HTTP/1.1 in jede Statuszeile und liest nach jeder Antwort die nächste Anfrage von derselben Verbindung. Das ist die offene Verbindung (Keep-alive), der Standard in HTTP/1.1: Eine Verbindung bleibt für weitere Anfragen offen, bis eine Seite sie beendet.
Die Änderung ist also ein einziges Schlüsselwortargument in make_server: protocol_version="HTTP/1.1". Nimm sie vor und führe python pl_http.py keep-alive erneut aus:
request 1 GET /books -> 200 OK
request 2 HEAD /books -> 200 OK
request 3 GET /books/1 -> 200 OK
--- pl_http's own http.client connection ---
replies: ['HTTP/1.1 200 OK', 'HTTP/1.1 200 OK', 'HTTP/1.1 200 OK']
connections: 1
Drei Anfragen, eine Verbindung. Das Log deines Servers zeigt drei Anfragezeilen, aber deine serve-handle_request()-Aufruf gemacht: Dieser Aufruf hat die Verbindung angenommen und ist zurückgekehrt, als sie endete. Das Szenario gibt serve vier Verbindungen. Die drei Anfragen haben eine davon genutzt, ein zweiter Teil des Laufs eine weitere, und im Ergebnisblock steht pl_http opened 2 empty connection(s) so serve() could stop waiting. Deine Schleife hat nie Anfragen gezählt. Seit Kapitel 4 zählt sie Verbindungen, und bis jetzt hat jede Verbindung zufällig genau eine Anfrage transportiert.
serve(server, max_connections=2) läuft unter HTTP/1.1. Ein Client sendet fünf Anfragen über eine Verbindung und schließt sie dann. Wie viele Anfragen werden beantwortet, bevor serve auf einen weiteren Client wartet?
Wer sie beendet
Eine offen gehaltene Verbindung endet auf eine von drei Arten:
Der Client sendet
Connection: closemit einer Anfrage. Der Server beantwortet sie und schließt. Die dritte Anfrage von pl_http hat genau das getan.Eine Antwort enthält
connection: close.http.serverbemerkt diesen Header, während er ihn schreibt, und schließt nach der Antwort.Niemand sendet etwas. Der
SOCKET_TIMEOUTdes Adapters beträgt 2 Sekunden: Kommt so lange keine neue Anfrage, schreibt der ServerRequest timed out: TimeoutError('timed out')in sein Log und schließt.
Ein Client sieht die dritte Art nicht kommen. http.client merkt es erst, wenn seine nächste Anfrage über diese Verbindung mit einem ConnectionResetError fehlschlägt, oder mit dessen Unterklasse RemoteDisconnected, wenn der Client das Schließen des Servers schon gelesen hatte. Welche der beiden kommt, hängt vom Timing ab. Auch die erste Art hat eine Eigenheit: http.server fügt der Antwort, nach der er schließt, keine Connection-Zeile hinzu, und http.client entscheidet anhand der Header der Antwort. Er hält diese Verbindung also weiter für offen, bis er versucht, sie wieder zu benutzen.
Die offene Verbindung hat auf einem Server wie deinem ihren Preis: Solange ein Client seine Verbindung offen hält, wartet der nächste, wie hinter dem langsamen Client in Networks with Python. http.server.ThreadingHTTPServer gibt stattdessen jeder Verbindung einen eigenen Thread. Dieser Kurs bleibt bei einer Verbindung nach der anderen, damit pl_http und Check my work sie zählen können.
Deine Längen stimmten schon
Auf einer offen gehaltenen Verbindung beginnt die nächste Antwort direkt nach dem letzten Byte dieser Antwort, also muss ein Client wissen, wo jeder Body endet. Unter HTTP/1.0 hat das Schließen das markiert, jetzt muss content-length es angeben. Dein Server sendet diesen Header seit Kapitel 4 bei jeder Antwort mit Body: json_response zählt die codierten Bytes, eine 0 an, und eine 204 oder eine 304 hat gar keinen Body.
Ein abgelehnter Body ist trotzdem da
Der zweite Teil des keep-alive-Laufs ist eine rohe Verbindung. pl_http sendet POST /books mit Transfer-Encoding: chunked und ohne content-length, und sein Body ist zufällig selbst eine vollständige Anfrage:
client -> server b'POST /books HTTP/1.1\r\nHost: books.test\r\nTransfer-Encoding: chunked\r\n\r\nGET /books/2 HTTP/1.1\r\nHost: books.test\r\n\r\n' (113 bytes)
server -> client b'HTTP/1.1 411 Length Required\r\nServer: ...
(N bytes followed the reply: b'HTTP/1.1 200 OK\r\nServer: ...
...
127.0.0.1 - - [the date and time] "GET /books/2 HTTP/1.1" 200 -
Zeilen, die auf ... enden, sind hier gekürzt, N hängt von der Server-Zeile ab, und die letzte Zeile stammt aus dem Log deines Servers. Dein read_body hat korrekt mit 411 geantwortet und nichts vom Body gelesen. Aber die Verbindung blieb offen, also hat der Server dort weitergelesen, wo er aufgehört hatte, und die Bytes des Bodys für die nächste Anfrage gehalten. Das Log zeigt ein GET /books/2, das der Client nie als Anfrage gesendet hat, und pl_http zeigt dessen 200 nach der 411. Hier ist diese Verbindung, vor und nach der Korrektur:

Hätte der Body aus gewöhnlichen Chunks wie 5\r\nhello bestanden, hätte der Server die Zeile 5 als Anfragezeile gelesen, Bad request syntax ('5') geloggt und eine HTML-Fehlerseite ganz ohne Statuszeile geschrieben.
Der Server beantwortet ein POST mit 411 und hält die Verbindung offen, ohne die 40 Bytes des Bodys zu lesen. Was liest er als Nächstes?
Die Regel stammt vom Protokoll. RFC 9112, Abschnitt 9.3, sagt, ein Server muss “read the entire request message body or close the connection after sending its response”, also den gesamten Body der Anfrage lesen oder die Verbindung nach dem Senden seiner Antwort schließen. Dein read_body lehnt absichtlich ab, bevor er liest, also muss er schließen: Jede Antwort, die er zurückgibt (die 411, beide 400er und die 413), enthält den Header connection: close. Antworten, die gebaut werden, nachdem der Body vollständig gelesen wurde, etwa 415 oder 422, lassen die Verbindung nutzbar und brauchen nichts.
Zu dieser 411: Ein HTTP/1.1-Server ist verpflichtet, Anfrage-Bodys mit Chunked Encoding zu decodieren (RFC 9112, Abschnitt 7.1). Deiner antwortet stattdessen mit 411. RFC 9112, Abschnitt 6.3, merkt an, dass echte Dienste das auch tun, und ein Client kann darauf reagieren, indem er den Body noch einmal mit einer Länge sendet. Chunks decodierst du in Lektion 3 selbst, auf der Client-Seite.
Welche Antwort bringt http.server dazu, die Verbindung zu schließen, nachdem er sie geschrieben hat?
Deine Aufgabe
Übergib in
make_serverprotocol_version="HTTP/1.1"anhttp_adapter.make_handler_class.Sorge dafür, dass jede Antwort, mit der
read_bodyablehnt,connection: closeenthält. Eine kleine Hilfsfunktion, diejson_erroraufruft und den Header hinzufügt, hält die vier Ablehnungen kurz.
Führe dann noch einmal python pl_http.py keep-alive aus. Die 411, hier ohne die Server und Date gezeigt, beendet jetzt den Lauf:
server -> client b'HTTP/1.1 411 Length Required\r\nServer: ...\r\nDate: ...\r\ncontent-type: application/json\r\ncontent-length: 56\r\nconnection: close\r\n\r\n...
Danach folgt nichts mehr, und das Log enthält kein GET /books/2. Klicke auf Check my work. Lektion 2 stellt als Nächstes die Frage, die diese Lektion aufgeworfen hat, von der Client-Seite aus: Woher weiß ein Leser, wo ein Body endet?
Aufgabe
Stelle deinen Server auf
In reading_server.py:
Übergib in
make_serverprotocol_version="HTTP/1.1"anhttp_adapter.make_handler_class. Behaltehttp.server.HTTPServer, die Adresse("127.0.0.1", 0)undserver.timeoutbei.Jede Antwort, mit der
read_bodyablehnt (die 411 für eine fehlende Länge, die 400 für eine fehlerhafte Länge, die 413 und die 400 für einen Body, der zu früh endet), enthält den Headerconnection: close. Behalte ihreund die Reihenfolge der Prüfungen aus Kapitel 5 bei.
Sonst ändert sich nichts: Die Antworten von handle, auch seine Fehler, lassen die Verbindung offen.
Im python pl_http.py keep-alive drei Anfragen über eine Verbindung, dann ein POST mit Chunked Encoding, dessen ungelesener Body eine komplette GET-Anfrage ist, und zeigt, was zurückkam.
Klicke dann auf Check my work. Es baut deinen Server mit make_server, ohne einen read_body mit Anfrage-connection: close enthält. Dann schickt es mehrere Anfragen auf einmal durch diese Handler-Klasse, als eine Verbindung, mit Büchern, Benutzern und Tokens, die du noch nicht gesehen hast, und trennt die Antworten anhand ihrer content-length: Drei Lesezugriffe müssen drei Antworten bekommen, ein Schreibzugriff mit anschließendem Lesezugriff zwei, und ein abgelehnter Body, ob verstecktes GET oder Chunks, genau eine Antwort, die Ablehnung. Außerdem sendet es elf Anfragen aus den Kapiteln 4 bis 8 über eine Verbindung, um zu sehen, dass jede frühere Regel noch gilt. Nichts aus deinem Terminal wird bewertet.