0%

Eine Verbindung, viele Anfragen · Abschlussprojekt

Abschlussprojekt: Projekt: Jeden Body lesen

Dein Server hält Verbindungen offen, und dein read_chunked decodiert Chunks, aber dein fetch liest immer noch nur zwei Arten von Body: keinen nach HEAD und genau content-length Bytes. Bei allem anderen löst es aus. Dieses Projekt macht den Client fertig: fetch liest jeden Body nach der Regel aus Lektion 2, und eine neue , fetch_many, sendet mehrere Anfragen über eine Verbindung, so wie dein Server sie jetzt erwartet.

Was der Client tun muss

SERVICE_CONTRACT.md führt die Regeln unter “The client in wire.py” auf. Die beiden neuen:

  • fetch liest einen Body anhand von content-length, anhand der Chunks oder bis zum Schließen, und liest keinen nach HEAD oder bei einem Status 1xx, 204 oder 304. Es lehnt transfer-encoding zusammen mit content-length ab, ebenso jede Übertragungscodierung außer chunked.

  • fetch_many sendet eine Anfrage nach der anderen über eine Verbindung, bittet bei der letzten um das Schließen und löst ConnectionError aus, wenn der Server die Verbindung zu früh beendet.

Meilensteine

Zuerst read_until_close(reader, *, limit=MAX_BODY). Es gibt die Bytes zurück, die schon im Puffer liegen, und jedes Byte danach, bis recv b"" zurückgibt, und hinterlässt einen leeren Puffer. Auch eine Antwort, die mit dem Schließen endet, bekommt eine Grenze: Löse ValueError aus, sobald die Bytes limit überschreiten, bevor du nach mehr fragst. Etwa sechs Zeilen.

Dann read_body(reader, method, headers, *, status=200). Schreibe es nach der Regel aus Lektion 2 um, in ihrer Reihenfolge:

  1. Kein Body bei HEAD, bei einem Status unter 200 oder bei 204 oder 304: Gib b"" zurück, ohne zu lesen.

  2. Mit transfer-encoding: Wenn auch content-length vorhanden ist, löse ValueError aus. Der Header muss genau einen enthalten, chunked in beliebiger Groß- und Kleinschreibung und mit erlaubten Leerzeichen drumherum. Alles andere, etwa gzip, chunked, löst ValueError aus, weil dieser Client keine andere Codierung decodiert. Gib dann zurück, was read_chunked zurückgibt.

  3. Ohne content-length: Gib zurück, was read_until_close zurückgibt.

  4. Sonst lies wie bisher anhand von content-length.

Die Regel aus Kapitel 1, die einen Body ohne Länge abgelehnt hat, gibt es nicht mehr. Die Tests, die mit diesem Kapitel in tests/test_wire.py gekommen sind, erwarten sie nicht mehr, und der Docstring oben in dieser Datei sagt das.

Dann fetch. Es übergibt den Status der Antwort: read_body(reader, method, response["headers"], status=response["status"]). Ohne ihn würde eine 304, die eine content-length enthält, fetch auf Bytes warten lassen, die nie kommen.

Führe das Hilfsprogramm aus:

python pl_http.py bodies

Dein fetch fragt den kleinen Server aus Lektion 3 nach /length, /chunked und /close, mit je einer Verbindung. Vor deinen Änderungen endet die Antwort mit Chunked Encoding in raised ValueError: this client cannot measure a body sent with Transfer-Encoding yet. Danach endet jeder der drei Berichte mit einem Body:

body: b'{}'  (2 bytes)
body: b'{"books": ["Kindred", "Caf\xc3\xa9"]}'  (31 bytes)
body: b'{"ends": "at the close"}'  (24 bytes)

Zuletzt fetch_many(base_url, targets, *, connect=socket.create_connection, timeout=TIMEOUT). Es sendet für jedes Ziel der Reihe nach GET base_url + target über eine Verbindung und gibt eine von Datensätzen zurück, wie fetch einen zurückgibt:

  • Öffne die Verbindung einmal, with connect((host, port), timeout=timeout) as sock:, mit Host und aus base_url (Port 80, wenn sie keinen hat), und erstelle einen Reader für die ganze Verbindung. Bytes der nächsten Antwort können im selben recv ankommen wie das Ende dieser Antwort, und sie warten im Puffer dieses Readers.

  • Baue jede Anfrage mit build_request("GET", base_url + target, headers). Die letzte enthält den Header Connection: close, damit der Server schließt, sobald jede Antwort angekommen ist. Die anderen brauchen keinen Connection-Header.

  • Sende eine Anfrage, lies ihren Kopf und ihren Body, und sende erst dann die nächste. Alle zu senden, bevor man liest, das sogenannte Pipelining, lässt dieser Kurs beiseite.

  • Wenn eine Antwort vor dem letzten Ziel connection: close sagt, liest der Server auf dieser Verbindung nichts mehr. Löse sofort ConnectionError aus, mit einer Meldung, die beide Zahlen enthält, wie viele Ziele beantwortet wurden und wie viele es waren, etwa the server closed the connection after 1 of 3 requests.

Etwa sechzehn Zeilen. Führe es dann gegen deinen eigenen Server aus:

python pl_http.py many
statuses: [200, 200, 200]
/me: b'{"user": null}'  (14 bytes)
serve() returned after 1 connection; the server is closed

pl_http gibt die drei Anfragen als eine client -> server-Zeile aus, weil es alles, was eine Verbindung transportiert hat, zusammen zeigt. Das Log deines Servers enthält drei Anfragen, und serve hat eine Verbindung gezählt.

Deine Tests

Du kannst Tests zu tests/test_wire.py hinzufügen, wie in Kapitel 6. fakes.scripted_socket steht für einen stummen Server, der die Verbindung offen hält, wenn sein Skript None enthält, und für einen geschlossenen, wenn das Skript einfach endet. Eine Antwort ohne Länge wird bis zum Schließen gelesen, also endet das Skript eines Tests für eine solche Antwort ohne None:

def test_a_body_that_ends_at_the_close():
    sock = scripted_socket([b"HTTP/1.1 200 OK\r\n\r\nab", b"c"])
    response = wire.fetch("GET", ITEM_URL, connect=fake_connect(sock, []))
    assert response["body"] == b"abc"

Diesmal führt Check my work deine Tests einmal aus, so wie python -m pytest -x es tun würde. Keiner darf fehlschlagen, und keiner darf eine echte Verbindung öffnen. Führe sie zuerst selbst aus:

python -m pytest -q

Was bewiesen wird, und wie

Check my work verbindet deinen Client ohne mit deinem Server. Das connect, das es fetch_many übergibt, gibt einen Ersatz-Socket zurück: Was dein Client sendet, geht in diesem Prozess durch deine eigene /1.1-Handler-Klasse, und was dein Server schreibt, kommt aus recv zurück. Sobald jede Antwort gelesen wurde und die Verbindung noch offen ist, löst recv TimeoutError aus. So fallen ein Client, der bis zum Schließen liest, und eine Serverantwort ohne Länge auf. Die Läufe bodies und many sind zum Zuschauen da und werden nicht bewertet.

Klicke dann auf Check my work. Das Framing, die offene Verbindung und den Umgang mit dem Schließen, die requests auf einer Verbindung für dich erledigt, hast du jetzt von Hand geschrieben. Lektion 5 steckt genau diese Bytes in .

Aufgabe

Bring wire.py bei, jeden Body zu lesen und mehrere Anfragen über eine Verbindung zu senden.

  1. read_until_close(reader, *, limit=MAX_BODY): Gib die Bytes in reader["buffer"] zurück, gefolgt von jedem Byte, bis recv b"" zurückgibt, und hinterlasse einen leeren Puffer. Löse aus, sobald die Bytes limit überschreiten, bevor du nach mehr fragst.

  2. read_body(reader, method, headers, *, status=200), nach der Regel aus Lektion 2: b"" ohne zu lesen bei HEAD und bei einem Status unter 200, 204 oder 304; mit transfer-encoding ValueError, wenn auch content-length vorhanden ist oder wenn der Header etwas anderes als der eine chunked ist (beliebige Groß- und Kleinschreibung, Leerzeichen erlaubt), sonst read_chunked; ohne content-length read_until_close; sonst wie bisher content-length.

  3. fetch übergibt status=response["status"] an read_body.

  4. fetch_many(base_url, targets, *, connect=socket.create_connection, timeout=TIMEOUT): ein connect-Aufruf mit Host und aus base_url (80, wenn sie keinen hat) und timeout, ein Reader, und für jedes Ziel der Reihe nach ein GET, gebaut mit build_request("GET", base_url + target, headers), vollständig gelesen, bevor das nächste gesendet wird. Nur die letzte Anfrage enthält Connection: close. Gib die Datensätze zurück, einen pro Ziel, jeder das parse_head- mit zusätzlichem "body", so wie fetch es zurückgibt. Wenn eine Antwort vor dem letzten Ziel connection: close sagt, löse ConnectionError aus, mit einer Meldung, die beide Zahlen enthält, wie viele Ziele beantwortet wurden und wie viele es waren, etwa the server closed the connection after 1 of 3 requests.

Dein Server muss weiterhin /1.1 sprechen und nach jedem Body schließen, den read_body ablehnt (Lektion 1). Deine Tests in tests/ müssen bestehen: Führe python -m pytest -q aus. Im zeigen python pl_http.py bodies und python pl_http.py many deinen Client auf einer echten Verbindung.

Klicke dann auf Check my work. Es ruft read_body und read_until_close mit Readern um einen Ersatz- und mit Bodys auf, die du noch nicht gesehen hast: Antworten auf HEAD, 204, 304 und 101, bei denen nichts zu lesen ist; Bodys mit Chunked Encoding, mit chunked in verschiedenen Schreibweisen; Längen; Bodys, die mit dem Schließen enden und schon teilweise im Puffer liegen; einen Body über limit; und transfer-encoding mit content-length, gzip, chunked, zwei Werten und identity. Wo eine Verbindung offen bleibt, löst der Ersatz-Socket TimeoutError aus, wenn mehr von ihm verlangt wird, als der Server gesendet hat. Es führt fetch mit einer 304, einer 204, einer Antwort mit Chunked Encoding und einer aus, die mit dem Schließen endet. Es führt fetch_many gegen die HTTP/1.1-Handler-Klasse deines eigenen Servers aus, in diesem Prozess, mit Büchern, die du noch nicht gesehen hast, und prüft die eine Verbindung, die Anfragezeilen, Connection: close nur bei der letzten Anfrage, den Timeout und jeden Body; danach gegen Antworten, die zu früh connection: close sagen. Es steuert deinen Server mit drei Anfragen über eine Verbindung und mit einem abgelehnten Body mit Chunked Encoding. Zuletzt führt es deine Tests einmal aus, hält beim ersten Fehlschlag an, ohne Netzwerk und mit einer Grenze von 3 Sekunden. Nichts aus deinem Terminal wird bewertet.