0%

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 /1.0, und das schließt die Verbindung nach jeder Antwort. Ein Client, der drei Dinge möchte, verbindet sich also dreimal. Führe dieses Szenario im Tab Shell aus, bevor du etwas änderst:

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 , den du nie übergeben hast:

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- hat für alle drei nur einen 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: close mit 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.server bemerkt diesen Header, während er ihn schreibt, und schließt nach der Antwort.

  • Niemand sendet etwas. Der SOCKET_TIMEOUT des Adapters beträgt 2 Sekunden: Kommt so lange keine neue Anfrage, schreibt der Server Request 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 gibt 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:

Nach einer 411 wird der ungelesene Body als zweite Anfrage gelesen, außer connection: close beendet die Verbindung.

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

  1. Übergib in make_server protocol_version="HTTP/1.1" an http_adapter.make_handler_class.

  2. Sorge dafür, dass jede Antwort, mit der read_body ablehnt, connection: close enthält. Eine kleine Hilfsfunktion, die json_error aufruft 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 von 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 /1.1 um und schließe die Verbindung nach jedem Body, den er nicht lesen will.

In reading_server.py:

  1. Übergib in make_server protocol_version="HTTP/1.1" an http_adapter.make_handler_class. Behalte http.server.HTTPServer, die Adresse ("127.0.0.1", 0) und server.timeout bei.

  2. Jede Antwort, mit der read_body ablehnt (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 Header connection: close. Behalte ihre und 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 sendet 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 zu öffnen, prüft die Protokollversion der Handler-Klasse und ruft read_body mit Anfrage- auf, um zu sehen, dass jede Ablehnung 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.