HTTP ist Text auf einem Socket · Übung
Genau den Body lesen
read_head hält bei der Leerzeile an, und parse_head sagt dir, was im Kopf steht. Was auf die Leerzeile folgt, ist der Body, und ein Teil davon liegt vielleicht schon in reader["buffer"], weil er im selben Stück wie das Ende des Kopfes angekommen ist. Die Frage dieser Lektion ist, wo der Body endet. Ein Reader, der zu früh aufhört, liefert ein halbes Dokument; ein Reader, der auf mehr wartet, als der Server sendet, wartet bis zu seinem Timeout.
Es gibt zwei mögliche Antworten: lesen, bis der Server die Verbindung schließt, oder eine Anzahl Bytes lesen, die der Server angekündigt hat. Die erste sieht einfacher aus, also probiere sie zuerst. Behalte dabei eine Tatsache im Kopf: Eine Connection: close. Genau darum geht es in Kapitel 9.
Lesen, bis der Server schließt
Lesen bis zum Schließen ist die recv b"" zurückgibt:
data = b""
chunk = sock.recv(4096)
while chunk != b"":
data = data + chunk
chunk = sock.recv(4096)
Probiere es gegen practice-api aus. Der Befehl unten ist python -, das das Programm aus den folgenden Zeilen liest, bis zu der Zeile, auf der EOF steht. Füge also den ganzen Block in den Tab Shell ein, erste und letzte Zeile eingeschlossen. Er sendet die Hook-Anfrage und lässt die Schleife auf einem
python - <<'EOF'
import socket
import wire
sock = socket.create_connection(wire.ADDRESS, timeout=2.0)
sock.sendall(wire.make_request("GET", wire.HOOK_TARGET))
data = b""
chunk = sock.recv(4096)
while chunk != b"":
data = data + chunk
chunk = sock.recv(4096)
print(data)
EOF
Du solltest sehen, dass er etwa zwei Sekunden wartet und mit TimeoutError anhält: practice-api hält die Verbindung nach seiner Antwort offen, also kommt nie ein b"". Wenn stattdessen der ganze Body auf einmal zurückkommt, hat der Dienst die Verbindung direkt nach der Antwort geschlossen; Lesen bis zum Schließen hat diesmal zufällig funktioniert.
Selbst wenn es funktioniert, kann Lesen bis zum Schließen nicht erkennen, ob der Body vollständig ist. Ein Body, der ganz angekommen ist, und ein Body, der durch eine abgebrochene Verbindung abgeschnitten wurde, enden gleich, mit b"", und die Schleife gibt zurück, was sie hat. Und ein Server, der die Verbindung offen hält, wie HTTP/1.1 es erwartet, sendet dieses b"" überhaupt nie. Alle drei Enden kannst du ohne Netzwerk sehen, mit den geskripteten Sockets aus fakes.py. Das erste Skript liefert eine ganze Antwort und schließt, das zweite schließt zwei Bytes zu früh, und das dritte liefert die ganze Antwort und schweigt dann, wie es eine offene Verbindung tut:
python - <<'EOF'
from fakes import scripted_socket
def read_until_close(sock):
data = b""
chunk = sock.recv(4096)
while chunk != b"":
data = data + chunk
chunk = sock.recv(4096)
return data
reply = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
print(read_until_close(scripted_socket([reply])))
print(read_until_close(scripted_socket([reply[:-2]])))
print(read_until_close(scripted_socket([reply, None])))
EOF
Die ersten beiden Zeilen, die es ausgibt, sind:
b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello'
b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhel'
und der dritte Aufruf endet in einem TimeoutError: scripted silence ist. Die zweite Antwort ist kaputt, und nichts darin sagt das. Die dritte war vollständig, und der Reader hat es nie erfahren.
Die Länge kommt zuerst
Die Antwort oben enthält schon die Lösung: Content-Length: 5. Der Server kündigt die Größe des Bodys in Bytes in einem Header an, bevor der Body beginnt. Das ist das Längenpräfix von der Pinnwand aus Networks with Python, als Text in einer read_exact löst ConnectionError aus, wenn die Verbindung vorher geschlossen wird.
read_exact(reader, n), schon in wire.py, übernimmt das Zählen. Es nimmt zuerst Bytes aus reader["buffer"] und ruft recv nur für das auf, was noch fehlt, sodass die Body-Bytes, die mit dem Kopf angekommen sind, nicht verloren gehen. Verfolge es für einen Body von 182 Bytes, von denen die ersten 100 mit dem Kopf angekommen sind:

Nach read_head enthält reader["buffer"] diese ersten 100 Bytes, und die anderen 82 sind noch unterwegs. read_exact(reader, 182) beginnt mit diesen 100 Bytes, ruft recv auf, bis der Puffer 182 enthält, und gibt die ersten 182 Bytes zurück; alles danach bleibt im Puffer.
Regeln für diesen Client
read_body(reader, method, headers) liest die Länge aus den geparsten Headern, die parse_head unter dem kleingeschriebenen Namen content-length als
Genau ein
. Eine Liste mit zwei Werten ist mehrdeutig: Der Server hat zwei verschiedene Dinge gesagt oder dasselbe zweimal. Das Protokoll verlangt, dass ein Client verschiedene Werte ablehnt; dieser Client lehnt jede Wiederholung ab. Nur Ziffern. Der Wert muss aus ASCII-Ziffern bestehen, geprüft mit
isascii()undisdigit(), wie beim. 12aoder-5ist keine Anzahl von Bytes.Nicht zu groß. Höchstens
MAX_BODY, eine Million Bytes. Vergleiche die Zahl vor dem Lesen mit der Grenze, so wie die Pinnwand die angekündigte Länge eines Frames mit ihrer Grenze verglichen hat: Ein Reader, der zuerst liest und später prüft, hat schon empfangen, was auch immer der Server senden wollte.
Eine Länge, die gegen eine dieser Regeln verstößt, ist fehlerhaft, und read_body löst
Zwei Arten von Antworten sind gültiges HTTP, das dieser Client noch nicht lesen kann. Eine Antwort ohne Content-Length darf ihren Body beenden, indem sie die Verbindung schließt, und eine Antwort mit einem Header Transfer-Encoding, etwa Transfer-Encoding: chunked, sendet ihren Body in Stücken, jedes mit seiner eigenen Länge. Keine von beiden ist falsch, aber dieser Client kann keinen dieser Bodys bisher abmessen, also lehnt read_body beide mit ValueError und einer Meldung ab, die das sagt. Kapitel 9 fügt beides hinzu. Prüfe zuerst auf transfer-encoding, dann auf ein fehlendes content-length.
Bytes, nicht Zeichen
Content-Length zählt Bytes, und read_exact zählt Bytes, also stimmen die beiden überein. Dekodiere den Body erst, wenn er ganz angekommen ist. Wie du in Networks with Python gesehen hast, kann ein Zeichen in
json_body(response) übernimmt das Dekodieren, sobald der Body vollständig ist: json.loads(response["body"].decode("utf-8")).
Nach read_head enthält der Puffer 10 Bytes des Bodys, und Content-Length ist 25. Wie viele Bytes müssen noch vom Socket kommen?
Deine Aufgabe
In wire.py:
Schreibe
read_body(reader, method, headers). LöseValueErroraus, wennheadersden Schlüssel"transfer-encoding"enthält, wenn es kein"content-length"enthält, wenn"content-length"mehr als einen Wert enthält, wenn dieser Wert nicht aus ASCII-Ziffern besteht oder wenn die Zahl größer alsMAX_BODYist. Gib sonstread_exact(reader, length)zurück. Dermethodist für die nächste Lektion; diese Lektion benutzt ihn nicht.Schreibe
json_body(response): Dekodiereresponse["body"]mit"utf-8"und gib zurück, wasjson.loadsliefert.Stelle
fetchfertig. Parse imwith-Block, nachdem du den Reader erzeugt hast, den Kopf mitparse_head(read_head(reader)), speichereread_body(reader, method, response["headers"])unter dem Schlüssel"body"diesesund gib das Dictionary zurück.
Sende dann zwei Anfragen im Tab Shell:
python wire.py
python -c "import wire; print(wire.json_body(wire.fetch('GET', '/v1/items/1')))"
Die erste gibt die ganze Antwort als Dictionary aus, ihren Body als Bytes; die zweite gibt den Eintrag selbst aus, {'category': 'color', 'id': 1, 'name': 'blue', 'value': 10}. Beide kehren zurück, sobald der Body vollständig ist. Keine wartet darauf, dass der Server die Verbindung schließt.
Drücke dann Check my work. Die nächste Lektion sendet die eine Anfrage, deren Antwort eine Länge ankündigt und trotzdem keinen Body hat.
Aufgabe
Schreibe in wire.py die read_body und json_body und stelle fetch fertig.
read_body(reader, method, headers) nimmt einen Reader, dessen Kopf gelesen wurde, und die Header, die parse_head zurückgegeben hat. Die Funktion löst
wenn
headersden Schlüssel"transfer-encoding"enthält oder kein"content-length": Einen solchen Body kann dieser Client noch nicht abmessen;wenn
"content-length"mehr als einenenthält oder einen Wert, der nicht aus ASCII-Ziffern besteht; wenn die Länge größer als
MAX_BODYist, bevor auch nur ein Byte des Bodys empfangen wird.
Sonst gibt sie read_exact(reader, length) zurück: genau die angekündigte Anzahl Bytes, beginnend mit denen, die schon in reader["buffer"] liegen, und jedes Byte nach dem Body bleibt im Puffer. Der method ist für die nächste Lektion.
json_body(response) gibt json.loads(response["body"].decode("utf-8")) zurück.
fetch behält seinen with-Block. Darin setzt es, nachdem es den Reader erzeugt hat, response = parse_head(read_head(reader)), dann response["body"] = read_body(reader, method, response["headers"]), und gibt response zurück.
Führe im python wire.py und python -c "import wire; print(wire.json_body(wire.fetch('GET', '/v1/items/1')))" aus.
Drücke dann Check my work. Die Prüfung ruft read_body nach dem Kopf von Antworten auf, die du nicht gesehen hast, geliefert in einem Stück, ein Byte nach dem anderen, an jeder Position in zwei Teile geschnitten (auch innerhalb des Bodys und innerhalb eines Zeichens, das zwei Bytes braucht) und in Stücken von 7 Bytes, jedes Mal sowohl mit einem Server, der danach schließt, als auch mit einem, der die Verbindung offen hält. Auf eine Antwort folgt der Anfang einer weiteren. Sie gibt read_body außerdem fehlerhafte und wiederholte Längen, eine Antwort ohne Content-Length, Antworten mit Transfer-Encoding, eine Länge weit über der Grenze und einen Server, der mitten im Body schließt. Sie ruft fetch mit einem Ersatz für connect auf einer Antwort 200 und einer Antwort 404 auf, und json_body auf einem Body mit Zeichen außerhalb von ASCII. Sie liest dein Terminal nicht und nimmt keinen Kontakt mit practice-api auf.