0%

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 , während er ankommt, Stück für Stück, bis zur Leerzeile.

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 im selben Ordner.

Öffne wire.py. Zu ihren Konstanten gehören ADDRESS, der Name und des Servers; 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 , die sagt, was zu tun ist (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 baut sie aus ihren Teilen:

Try it

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 beginnen, die der Aufrufer nie senden wollte. Deshalb lehnt request_line ein solches Ziel mit ab, so wie es ein Ziel ablehnt, das nicht 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. /1.1 verlangt sie in jeder Anfrage, und Kapitel 2 erklärt, warum. In diesem Kapitel ändert sie sich nie, also liefert 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:

ZeileWas sie ist
GET /v1/items?page=1&page_size=2 HTTP/1.1\r\ndie Anfragezeile
Host: practice-api:8080\r\ndie Zeile Host
\r\ndie 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 um den Puffer des Readers:

  1. Suche mit find nach \r\n\r\n in reader["buffer"]; es gibt die Position zurück, oder -1, wenn es nicht vorkommt.

  2. Solange es nicht vorkommt, rufe reader["sock"].recv(4096) auf, hänge das Stück ans Ende des Puffers und suche erneut.

  3. 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:

  • recv gibt b"" zurück: Der Server hat die Verbindung geschlossen, bevor der Kopf zu Ende war. Löse ConnectionError aus, so wie read_exact es tut.

  • Der Puffer wächst über limit Bytes hinaus, ohne dass ein \r\n\r\n vorkommt: Ein Kopf, der nie endet, würde sonst den Speicher des Programms füllen. Löse ValueError aus, so wie dein read_line in Networks with Python es bei einer Zeile getan hat, die nie endete. limit ist standardmäßig MAX_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 , damit ein Test eine Funktion übergeben kann, die einen Fake-Socket zurückgibt, so wie bei deinem Pinnwand-Client in Networks with Python. Das Timeout, das 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:

  1. request_line(method, target): Gib f"{method} {target} HTTP/1.1\r\n" zurück; löse ValueError aus, wenn das Ziel nicht mit / beginnt oder ein Leerzeichen, \r oder \n enthält.

  2. make_request(method, target): Gib die Anfragezeile, HOST_LINE und "\r\n" zurück, kodiert mit "ascii".

  3. read_head(reader, *, limit=MAX_HEAD): die Schleife von oben.

  4. fetch(method, target, *, connect=socket.create_connection, timeout=TIMEOUT): Verbinde dich mit timeout=timeout mit ADDRESS, sende die Anfrage, erzeuge einen Reader und gib zurück, was read_head zurü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 bekommen: Der Webserver vor practice-api lehnt die Anfrage ab, bevor der Dienst sie sieht. Wenn du stattdessen das übliche 200 bekommst, hast du den Dienst direkt erreicht, der das Fehlen toleriert; HTTP/1.1 verlangt die Zeile trotzdem. Der Befehl gibt nur den Kopf aus, also schau dir die Statuszeile an.

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 und ein von Headern.

Aufgabe

Schreibe vier in wire.py, in dieser Reihenfolge. Check my work nennt die erste, die noch NotImplementedError auslöst, sodass du sie eine nach der anderen angehst.

  1. request_line(method, target) gibt f"{method} {target} HTTP/1.1\r\n" zurück. Sie löst aus, wenn target nicht mit / beginnt oder ein Leerzeichen, \r oder \n enthält.

  2. make_request(method, target) gibt Bytes zurück: die Anfragezeile, HOST_LINE und "\r\n", kodiert mit "ascii".

  3. read_head(reader, *, limit=MAX_HEAD) sucht nach \r\n\r\n in reader["buffer"], und solange es nicht vorkommt, hängt sie reader["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 in reader["buffer"]. Sie löst ConnectionError aus, wenn recv vorher b"" zurückgibt, und ValueError, wenn der Puffer über limit Bytes hinauswächst, ohne dass \r\n\r\n vorkommt.

  4. fetch(method, target, *, connect=socket.create_connection, timeout=TIMEOUT) ruft connect(ADDRESS, timeout=timeout) in einer with-Anweisung auf, sendet make_request(method, target) mit sendall, verpackt den mit make_reader und gibt zurück, was read_head zurü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 und Zielen auf, die du nicht gesehen hast, und mit Zielen, die abgelehnt werden müssen. Sie ruft 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.