HTTP ist Text auf einem Socket · Übung
Eine Anfrage von Hand senden
Die letzte Lektion hat Bytes zerlegt, die schon vorlagen. Diese hier erzeugt sie: Du schreibst die Anfrage in wire.py, sendest sie über eine connect an practice-api und liest den Kopf der Antwort vom
Diese Lektion läuft auf den Servern von Python Land. Der Tab Project enthält die Dateien des Kapitels: wire.py, in der du arbeitest, fakes.py, die die Lektionen 5 und 7 benutzen, tests/test_wire.py, die Lektion 7 benutzt, und README.md, die Routen und Regeln für dieses Kapitel. Der Tab Shell ist ein
Öffne wire.py. Zu ihren Konstanten gehören ADDRESS, der Name und HOST_LINE; HOOK_TARGET, das Ziel der Anfrage aus Lektion 2, die dieses Kapitel die Hook-Anfrage nennt; und die Grenzen MAX_HEAD und MAX_BODY. make_reader und read_exact sind der Empfangspuffer aus Networks with Python, schon fertig geschrieben. Die Funktionen danach schreibst du, ein paar pro Lektion.
Die Anfragezeile
Eine Anfragezeile hat drei Teile, getrennt durch einzelne Leerzeichen: die GET fragt nach einer Ressource); das Anfrageziel (request target), das die Ressource auf dem Server benennt (einen Pfad und möglicherweise eine Query nach ?); und die Version, HTTP/1.1. CRLF beendet die Zeile. Ein
Ein Server liest die Anfragezeile, indem er sie an den Leerzeichen und am CRLF aufschneidet, also darf das Ziel keines davon enthalten. /v1/items/a b würde dem Server vier Wörter geben, wo er drei erwartet, und ein Ziel, das \r\n enthält, würde die Zeile zu früh beenden und eine request_line ein solches Ziel mit / beginnt. Kapitel 2 zeigt, wie du jedes Zeichen sicher in einem Ziel unterbringst.
Die Zeile Host
Die zweite Zeile ist die Zeile Host, die den Server nennt, für den die Anfrage bestimmt ist. wire.py sie als HOST_LINE mit:
HOST_LINE = "Host: practice-api:8080\r\n"
make_request(method, target) fügt die Anfragezeile, HOST_LINE und ein weiteres "\r\n" für die Leerzeile zusammen und kodiert den Text mit "ascii", weil ein Socket Bytes sendet. Für die Hook-Anfrage:
| Zeile | Was sie ist |
|---|---|
GET /v1/items?page=1&page_size=2 HTTP/1.1\r\n | die Anfragezeile |
Host: practice-api:8080\r\n | die Zeile Host |
\r\n | die Leerzeile: hier endet der Kopf |
Die Anfrage endet mit \r\n\r\n, was dem Server sagt, dass der Kopf zu Ende ist; diese Anfrage hat keinen Body. Jede Zeile endet mit CRLF, weil HTTP/1.1 das verlangt. Manche Server akzeptieren auch ein einzelnes \n, aber ein nachsichtiger Server ist kein Grund, weniger zu senden, als das Protokoll verlangt.
Den Kopf lesen, während er ankommt
Der Kopf der Antwort endet beim ersten \r\n\r\n. Ein recv gibt zurück, was bis jetzt angekommen ist, vielleicht die ganze Antwort, vielleicht einen Teil der ersten Zeile, also ist das Lesen des Kopfes eine
Suche mit
findnach\r\n\r\ninreader["buffer"]; es gibt die Position zurück, oder-1, wenn es nicht vorkommt.Solange es nicht vorkommt, rufe
reader["sock"].recv(4096)auf, hänge das Stück ans Ende des Puffers und suche erneut.Wenn es vorkommt, gib die Bytes davor zurück und lege alles nach der Leerzeile wieder in
reader["buffer"]ab.
Prüfe den Puffer vor dem ersten recv: Wenn die Bytes schon da sind, gibt es nichts, worauf du warten musst. Und behalte, was nach dem Kopf folgt. Es ist der Anfang des Bodys, den Lektion 5 liest, und oft kommt eine Antwort mit Kopf und Body in einem Stück an.
Das erste recv in read_head hat b'HTTP/1.1 200 OK\r\nContent-Ty' zurückgegeben. Was sollte read_head tun?
Die Schleife kann auf zwei Arten ohne Kopf enden, und jede bekommt ihre eigene Ausnahme:
recvgibtb""zurück: Der Server hat die Verbindung geschlossen, bevor der Kopf zu Ende war. LöseConnectionErroraus, so wieread_exactes tut.Der Puffer wächst über
limitBytes hinaus, ohne dass ein\r\n\r\nvorkommt: Ein Kopf, der nie endet, würde sonst den Speicher des Programms füllen. LöseValueErroraus, so wie deinread_linein Networks with Python es bei einer Zeile getan hat, die nie endete.limitist standardmäßigMAX_HEAD, 16384 Bytes.
Über connect senden
fetch setzt alles zusammen. Seine ersten beiden Zeilen öffnen die Verbindung und senden die Anfrage:
with connect(ADDRESS, timeout=timeout) as sock:
sock.sendall(make_request(method, target))
connect ist ein socket.create_connection übergeben wird, bleibt am Socket, also wartet jedes spätere recv höchstens timeout Sekunden, standardmäßig 2, und löst dann TimeoutError aus. Vorerst gibt fetch den Kopf als Bytes zurück; Lektion 5 macht daraus eine vollständige Antwort.
Deine Aufgabe
Schreibe in wire.py diese vier Funktionen, in dieser Reihenfolge:
request_line(method, target): Gibf"{method} {target} HTTP/1.1\r\n"zurück; löseValueErroraus, wenn das Ziel nicht mit/beginnt oder ein Leerzeichen,\roder\nenthält.make_request(method, target): Gib die Anfragezeile,HOST_LINEund"\r\n"zurück, kodiert mit"ascii".read_head(reader, *, limit=MAX_HEAD): die Schleife von oben.fetch(method, target, *, connect=socket.create_connection, timeout=TIMEOUT): Verbinde dich mittimeout=timeoutmitADDRESS, sende die Anfrage, erzeuge einen Reader und gib zurück, wasread_headzurückgibt.
Lass die Funktionen, die für die Lektionen 4 bis 6 markiert sind, so, wie sie sind.
Sende dann die Hook-Anfrage im Tab Shell. main gibt aus, was fetch zurückgibt, also siehst du den Kopf als Bytes-
python wire.py
python wire.py HEAD
Der erste Befehl sendet GET mit HOOK_TARGET, der zweite HEAD mit demselben Ziel. Jeder gibt den Kopf als Bytes aus: zuerst die Statuszeile, jeder Zeilenumbruch als \r\n dargestellt, und kein Body. Beide kehren sofort zurück, weil read_head bei der Leerzeile anhält. Nichts aus deinem Terminal wird geprüft.
Wenn du sehen willst, wofür die Zeile Host da ist, sende die Anfrage ohne sie. Dieser Befehl schreibt die Anfragezeile und die Leerzeile und nichts dazwischen:
python -c "import socket, wire; sock = socket.create_connection(wire.ADDRESS, timeout=2.0); sock.sendall((wire.request_line('GET', wire.HOOK_TARGET) + '\r\n').encode('ascii')); print(wire.read_head(wire.make_reader(sock)))"
Lässt du die Zeile Host weg, solltest du 400 Bad Request mit einer kurzen HTML-Seite statt
Wenn dich die Schleife in read_head verwirrt, frag Monty:
Warum sollte mein read_head im Puffer nach \r\n\r\n suchen, bevor es recv aufruft, und nicht nur danach?
Wenn python wire.py den Kopf zeigt, drücke Check my work. Die nächste Lektion macht aus diesen Bytes einen
Aufgabe
Schreibe vier wire.py, in dieser Reihenfolge. Check my work nennt die erste, die noch NotImplementedError auslöst, sodass du sie eine nach der anderen angehst.
request_line(method, target)gibtf"{method} {target} HTTP/1.1\r\n"zurück. Sie löstaus, wenn targetnicht mit/beginnt oder ein Leerzeichen,\roder\nenthält.make_request(method, target)gibt Bytes zurück: die Anfragezeile,HOST_LINEund"\r\n", kodiert mit"ascii".read_head(reader, *, limit=MAX_HEAD)sucht nach\r\n\r\ninreader["buffer"], und solange es nicht vorkommt, hängt siereader["sock"].recv(4096)ans Ende des Puffers und sucht erneut. Sie gibt die Bytes vor der Leerzeile zurück und lässt jedes Byte danach inreader["buffer"]. Sie löstConnectionErroraus, wennrecvvorherb""zurückgibt, undValueError, wenn der Puffer überlimitBytes hinauswächst, ohne dass\r\n\r\nvorkommt.fetch(method, target, *, connect=socket.create_connection, timeout=TIMEOUT)ruftconnect(ADDRESS, timeout=timeout)in einerwith-Anweisung auf, sendetmake_request(method, target)mitsendall, verpackt denmit make_readerund gibt zurück, wasread_headzurückgibt.
Lass parse_head, read_body und json_body so, wie sie sind.
Führe im python wire.py und python wire.py HEAD aus.
Drücke dann Check my work. Die Prüfung ruft request_line und make_request mit read_head auf Ersatz-Sockets auf, die Antworten enthalten, die du nicht gesehen hast, geliefert in einem Stück, ein Byte nach dem anderen, an jeder Position in zwei Teile geschnitten (auch innerhalb der Leerzeile und innerhalb des Bodys) 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 und nichts mehr sendet. Sie prüft außerdem einen Puffer, der schon einen ganzen Kopf enthält, einen Kopf, der nie endet, und einen Server, der mitten im Kopf schließt. Sie ruft fetch mit einem Ersatz für connect auf und prüft die Adresse, das Timeout (den Standardwert und einen, den du übergibst), die gesendeten Bytes, den zurückgegebenen Kopf und dass der with-Block beendet wurde. Sie liest dein Terminal nicht und nimmt keinen Kontakt mit practice-api auf.