0%

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 /1.1-Verbindung bleibt nach einer Antwort offen, damit der Client eine weitere Anfrage darauf senden kann, außer eine Seite sagt Connection: close. Genau darum geht es in Kapitel 9.

Lesen, bis der Server schließt

Lesen bis zum Schließen ist die aus Networks with Python, die alles sammelt, bis 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 mit einem Timeout von 2 Sekunden laufen:

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 , dessen letzte Zeile 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 geschrieben statt als vier Bytes vor jedem Frame. Der Reader liest den Kopf, findet die Länge und liest genau so viele Bytes. Er braucht nie, dass der Server schließt, und er weiß, wann ein Body abgeschnitten wurde: 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:

read_exact(reader, 182) beginnt mit den 100 Body-Bytes, die schon im Puffer liegen, und ruft recv auf, bis es 182 hat.

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 gespeichert hat. Bevor es auch nur ein Byte des Bodys liest, prüft es die Länge:

  • 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() und isdigit(), wie beim . 12a oder -5 ist 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 aus.

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 mehrere Bytes brauchen, und ein Stück eines Stroms kann mitten in einem davon enden; Stück für Stück zu dekodieren würde dort fehlschlagen, und Zeichen statt Bytes zu zählen würde zu früh aufhören.

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:

  1. Schreibe read_body(reader, method, headers). Löse ValueError aus, wenn headers den 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 als MAX_BODY ist. Gib sonst read_exact(reader, length) zurück. Der method ist für die nächste Lektion; diese Lektion benutzt ihn nicht.

  2. Schreibe json_body(response): Dekodiere response["body"] mit "utf-8" und gib zurück, was json.loads liefert.

  3. Stelle fetch fertig. Parse im with-Block, nachdem du den Reader erzeugt hast, den Kopf mit parse_head(read_head(reader)), speichere read_body(reader, method, response["headers"]) unter dem Schlüssel "body" dieses und 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 aus:

  • wenn headers den Schlüssel "transfer-encoding" enthält oder kein "content-length": Einen solchen Body kann dieser Client noch nicht abmessen;

  • wenn "content-length" mehr als einen enthält oder einen Wert, der nicht aus ASCII-Ziffern besteht;

  • wenn die Länge größer als MAX_BODY ist, 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.