Eine Verbindung, viele Anfragen · Übung
Einen Body mit Chunked Encoding decodieren
Lektion 2 hat einen Body mit Chunked Encoding mit bloßem Auge gelesen. In dieser Lektion bringst du wire.py bei, einen zu lesen: read_chunked(reader, *, limit=MAX_BODY) nimmt das Reader-read_head gerade benutzt hat, und gibt den Body zurück. Den Reader hinterlässt es bereit für alles, was als Nächstes auf der Verbindung kommt. Sieh zuerst zu, wie es fehlschlägt:
python pl_http.py chunked
Hier betreiben dein make_server und dein serve einen kleinen Server, der für diesen Kurs geschrieben wurde, statt deiner Leseliste. pl_http sendet GET /chunked, und dieser Server antwortet mit dem Body mit Chunked Encoding aus Lektion 2. pl_http liest den Kopf der Antwort selbst, legt die Bytes, die danach kamen, in den Puffer eines Readers und übergibt den Reader an dein read_chunked. Bevor du es schreibst, sagt der Bericht:
--- your read_chunked() returned ---
raised NotImplementedError: not written yet: Lesson 3
Zwei Reader, die du schon hast
Zwei wire.py übernehmen das Lesen für dich.
read_exact(reader, n) aus Kapitel 1 gibt genau n Bytes zurück, zuerst aus dem Puffer und dann von recv, und behält überzählige Bytes im Puffer. Es löst ConnectionError aus, wenn die Gegenseite schließt, bevor n Bytes angekommen sind.
read_crlf_line(reader, *, limit=MAX_LINE) ist neu in diesem Kapitel und schon für dich geschrieben. Es tut für eine Zeile, was read_head für einen Kopf tut: Es gibt die Bytes vor dem nächsten CRLF zurück, ohne das CRLF, und lässt den Rest im Puffer. Eine Zeile, die länger als MAX_LINE ist, also 1024 Bytes, löst ConnectionError aus.
Mit diesen beiden besteht jeder Chunk aus drei Lesevorgängen:
the size line read_crlf_line(reader)
the data read_exact(reader, size)
the CRLF read_exact(reader, 2), which must be b"\r\n"
Wiederhole das, bis die Größe 0 ist. Die zwei Bytes nach den Daten sind nicht optional: Ein Chunk, dessen Daten kein CRLF folgt, ist fehlerhaft, und wer weiterliest, als wäre alles in Ordnung, sucht die nächste Größenzeile an der falschen Stelle. Löse ValueError aus, wenn sie etwas anderes sind.
Die Größenzeile kommt als Bytes an. Decodiere sie mit "iso-8859-1", so wie parse_head einen Kopf decodiert, schneide sie dann am ersten ; ab, denn alles danach ist eine Erweiterung, die du ignorierst, und entferne die Leerzeichen um das, was übrig bleibt.
Hex, streng gelesen
Die Größe ist hexadezimal, und int(text, 16) liest Hexadezimalzahlen. Es liest aber noch mehr:
Was gibt das aus?
print(int("1a", 16), int("0x1a", 16))
Beide sind 26, aber nur 1a ist eine Chunk-Größe. int mit Basis 16 akzeptiert auch 1_a und +1a, und aus -1a macht es -26. Eine Chunk-Größe besteht aus einer oder mehreren Hex-Ziffern und sonst nichts. Prüfe also jedes Zeichen gegen HEX_DIGITS, den "0123456789abcdefABCDEF" in wire.py, bevor du int aufrufst, und löse ValueError für jedes andere Zeichen und für eine leere Größe aus.
Lektion 1 hat gezeigt, was passiert, wenn zwei Programme auf einer Verbindung uneins sind, wo ein Body endet: Die Bytes einer Nachricht werden als eine andere gelesen. Eine Größe, die ein Leser akzeptiert und ein anderer ablehnt, ist genau so eine Uneinigkeit. http.client selbst ist hier nachsichtig: Es schneidet die Größenzeile bei ; ab und gibt den Rest an int(line, 16) weiter. Dein Reader wird strenger sein.
Das Ende
Eine Größe von 0 bedeutet, dass die Daten zu Ende sind, und auf diese Zeile folgen weder Daten noch CRLF. Es folgen Trailer-Zeilen, also Header-Zeilen, die nach dem Body gesendet werden, und dann eine leere Zeile. Lies Zeilen mit read_crlf_line, bis eine leer ist, verwirf sie und gib den Body zurück. Zähle die Bytes dieser Zeilen, so wie read_head einen Kopf begrenzt, und löse ValueError aus, sobald sie MAX_HEAD überschreiten. Was ein Trailer-Feld bedeutet, geht über diesen Kurs hinaus. Dein Reader muss nur an ihnen vorbeikommen.
read_chunked hat gerade die Größenzeile 0 gelesen. Was kommt als Nächstes?
Was nach der leeren Zeile kommt, gehört zur nächsten Antwort auf derselben Verbindung. Lass es in reader["buffer"]. Deshalb ist der Reader ein Dictionary, das länger lebt als ein Aufruf: Das nächste read_head beginnt bei diesen Bytes.
Grenzen
Die Pinnwand in Networks with Python hat einen Frame, dessen angekündigte Länge über ihrer Grenze lag, abgelehnt, bevor sie ein Byte davon gelesen hat. Dieselbe Regel gilt hier, Chunk für Chunk: Prüfe vor dem Lesen der Daten eines Chunks, ob der bisherige Body plus die Größe dieses Chunks limit überschreiten würde, und löse in dem Fall ValueError aus. Sonst würde dein Client bei einer Größenzeile FFFFFFFF auf 4 GB warten. limit ist standardmäßig MAX_BODY, also 1000000 Bytes.
Deine Aufgabe
Schreibe read_chunked(reader, *, limit=MAX_BODY) in wire.py: Größenzeilen, Daten, CRLFs, den Null-Chunk, Trailer-Zeilen und die leere Zeile, mit den Prüfungen von oben. Etwa zwanzig Zeilen reichen dafür, und eine kleine Hilfsfunktion, die eine Größenzeile in eine Zahl umwandelt, ist willkommen.
Du kannst es im Tab Shell mit dem geskripteten fakes.py ausprobieren, dessen recv die Bytes herausgibt, die du ihm gibst:
python - <<'EOF'
import wire
from fakes import scripted_socket
reader = wire.make_reader(scripted_socket([b"4\r\nWiki\r\n5;x=1\r\npedia\r\n0\r\n\r\nNEXT"]))
print(wire.read_chunked(reader))
print(reader["buffer"])
EOF
Es gibt b'Wikipedia' aus und dann b'NEXT', den Anfang einer nächsten Antwort, der noch im Puffer liegt. Du kannst auch eigene Tests zu tests/test_wire.py hinzufügen und sie mit python -m pytest -q ausführen. Sie gehören dir, und Check my work führt sie nicht aus. Führe dann noch einmal das Hilfsprogramm aus:
python pl_http.py chunked
--- your read_chunked() returned ---
body: b'{"books": ["Kindred", "Caf\xc3\xa9"]}' (31 bytes)
left in the buffer: b'' (0 bytes)
Sieben Bytes und vierundzwanzig, und nichts bleibt übrig. Ein Decoder, der 18 als achtzehn liest, nimmt sechs Bytes zu wenig und findet dann kein CRLF, wo er eines erwartet.
Klicke dann auf Check my work. Lektion 4 baut diesen Decoder in fetch ein, neben einen Reader für Bodys, die mit dem Schließen enden.
Aufgabe
Decodiere einen Body mit Chunked Encoding in wire.py.
Schreibe read_chunked(reader, *, limit=MAX_BODY). Es liest aus dem Reader-read_crlf_line und read_exact, und gibt den Body als Bytes zurück:
Lies eine Größenzeile. Decodiere sie als
"iso-8859-1", verwirf alles ab dem ersten;und entferne Leerzeichen. Der Rest muss aus einem oder mehreren Zeichen bestehen, jedes davon inHEX_DIGITS; sonst löseaus. Wandle ihn dann mit int(text, 16)um.Bei einer Größe über 0: Wenn der bisherige Body plus diese Größe größer als
limitist, löseValueErroraus, bevor du die Daten liest. Sonst lies genau so viele Bytes, dann zwei weitere, dieb"\r\n"sein müssen (sonstValueError).Lies nach der Größe 0 Zeilen bis zu einer leeren und verwirf sie. Löse
ValueErroraus, sobald diese Zeilen zusammen mehr alsMAX_HEADBytes ergeben. Gib dann den Body zurück.
Lass jedes Byte nach der leeren Zeile in reader["buffer"]. Lass einen ConnectionError durch ein Schließen mittendrin aus der
Im python pl_http.py chunked deiner Funktion eine echte Antwort mit Chunked Encoding und zeigt, was sie zurückgegeben hat und was im Puffer übrig blieb.
Klicke dann auf Check my work. Es ruft read_chunked mit Readern um einen Ersatz-recv Bodys herausgibt, die du noch nicht gesehen hast, an jedem Byte geteilt, ein Byte nach dem anderen und in ungleichmäßigen Stücken: Größen in Groß- und Kleinschreibung, Erweiterungen, ein Chunk, dessen Daten CRLF enthalten, Trailer-Zeilen, 1000 Chunks mit je einem Byte und der Anfang einer nächsten Antwort nach der Nachricht. Es prüft Größen wie 0x7, -7, 7_, g und eine leere, Daten ohne nachfolgendes CRLF, ein Schließen innerhalb eines Chunks, zu viele Trailer-Bytes und einen Body über limit, bei dem die Daten des Chunks, der die Grenze überschreitet, nie angefordert werden dürfen. Wenn die Nachricht vollständig ist, bleibt der Ersatz-Socket offen, sodass ein Decoder auffällt, der nach der leeren Zeile weiterliest. Deine eigenen Tests werden nicht ausgeführt. Nichts aus deinem Terminal wird bewertet.