0%

HTTP ist Text auf einem Socket

Eine Nachricht aus Zeilen

Hier ist die erste Anfrage, die dieser Kurs sendet: Sie bittet practice-api um die erste Seite ihrer Einträge, zwei Einträge pro Seite. In Python ist sie ein einziger Bytes-:

request = b"GET /v1/items?page=1&page_size=2 HTTP/1.1\r\nHost: practice-api:8080\r\n\r\n"
print(request)

Das ist die ganze Anfrage. Dahinter steckt keine versteckte Struktur: Das sind die Bytes, die ein Client auf den schreibt, und der Server liest sie in derselben Reihenfolge. Der Rest dieses Kapitels schreibt sie, sendet sie und liest, was zurückkommt.

Zeilen, die mit CR LF enden

Die Anfrage besteht aus Zeilen, und jede Zeile endet mit zwei Bytes: \r, einem Wagenrücklauf (carriage return), und danach \n, einem Zeilenumbruch. Zusammen heißen sie CRLF. Die Pinnwand in Networks with Python hat jede Zeile nur mit \n beendet. hat sich anders entschieden, und wie bei der Pinnwand halten sich beide Seiten an die Abmachung, sodass ein Leser jede Zeile findet, indem er nach \r\n sucht.

Lies die Anfrage Zeile für Zeile:

  1. GET /v1/items?page=1&page_size=2 HTTP/1.1 ist die Anfragezeile: was zu tun ist, mit welcher Ressource und in welcher HTTP-Version.

  2. Host: practice-api:8080 ist eine : ein Name, ein Doppelpunkt und ein Wert. Diese hier nennt den Server, für den die Anfrage bestimmt ist.

  3. Dann kommt eine Zeile, auf der nichts steht. Die Leerzeile beendet den Kopf der Anfrage.

Eine Antwort folgt demselben Plan. Ihre erste Zeile ist eine Statuszeile, etwa HTTP/1.1 200 OK; Headerzeilen folgen; eine Leerzeile beendet den Kopf; und danach folgt der Body, das , das du in Python in der Praxis dekodiert hast.

request                               response
--------------------------------      --------------------------------
request line                  CR LF   status line                   CR LF
header line                   CR LF   header line                   CR LF
header line                   CR LF   header line                   CR LF
(empty line)                  CR LF   (empty line)                  CR LF
body (empty here)                     body: the JSON

Beide Nachrichten haben dieselben drei Teile. Zuerst eine Startzeile: die Anfragezeile oder die Statuszeile. Dann ein Block aus Headerzeilen. Dann eine Leerzeile, die in Bytes einfach \r\n ist, sodass der Kopf mit \r\n\r\n endet: dem Ende der letzten Headerzeile, gefolgt von der Leerzeile. Alles danach ist der Body. Die Anfrage oben hat einen leeren Body; eine Anfrage, die Daten sendet, wie in späteren Kapiteln, legt sie dort ab.

Bytes mit split zerlegen

Bytes und haben eine split. data.split(separator) schneidet data an jedem Vorkommen von separator auf und gibt die Stücke als zurück, ohne die Trennzeichen. Schneide die Anfrage an jedem \r\n auf:

Try it

Es gibt vier Stücke, und die letzten beiden sind leer. Die Anfrage endet mit \r\n\r\n, zwei Trennzeichen direkt hintereinander: Das erste \r\n beendet die Zeile Host, und zwischen ihm und dem zweiten steht nichts, also gibt split dort ein leeres Stück zurück. Nach dem letzten Trennzeichen steht ebenfalls nichts, was das zweite leere Stück ergibt. Die Leerzeile, die den Kopf beendet, zeigt sich als diese leeren Stücke.

split funktioniert auf Text genauso, und die Liste, die es zurückgibt, ist eine gewöhnliche Liste, dieselbe, die du bekämst, wenn du sie ausschreibst:

words = "a,b,c".split(",")
print(words)
print(words == ["a", "b", "c"])

Auf Bytes muss das Trennzeichen aus Bytes bestehen, b"\r\n"; auf Text muss es Text sein.

Was gibt das aus?

print(len(b"HEAD / HTTP/1.1\r\nHost: x\r\n\r\n".split(b"\r\n")))

Eine Antwort in zwei Teile zerlegen

Eine Antwort braucht zuerst einen anderen Schnitt: den Kopf vom Body trennen, am ersten \r\n\r\n. Unten steht eine Beispielantwort in der Form von practice-api, gebaut aus dem, was diese Anfrage auf der Website zurückbekommen sollte. Der Body ist der, den practice-api für diese Seite sendet; das Datum ist erfunden.

Try it

split(b"\r\n\r\n", 1) gibt eine Liste mit zwei Elementen zurück: parts[0] ist der Kopf, ohne die Leerzeile, und parts[1] ist der Body. Wenn du den Kopf dann an \r\n aufschneidest, bekommst du ein Element pro Zeile, die Statuszeile zuerst. Die letzte ausgegebene Zahl ist 182, die Länge des Bodys in Bytes, also genau das, was der Header Content-Length angekündigt hat. Lektion 5 benutzt diesen Header, um den Body zu lesen.

Die Zeilen Server und Date kommen von dem kleinen Webserver, der vor practice-api steht; wenn eine Antwort, die du bekommst, keine von beiden enthält, kam sie direkt vom Dienst, und nichts in diesem Kurs hängt von ihnen ab.

Warum das zweite Argument zählt

Die 1 in split(b"\r\n\r\n", 1) ist maxsplit: höchstens einmal schneiden, beim ersten Treffer, und alles danach als ein Stück behalten. Ohne sie schneidet split an jedem \r\n\r\n, das es findet. Der Kopf endet beim ersten, aber der Body sind Daten, und Daten können alles enthalten, auch diese vier Bytes. Ein Body, der sie enthielte, würde in Stücke geschnitten, und parts[1] wäre nur das erste davon. Mit maxsplit gleich 1 kommt der Kopf vom ersten Treffer, und der Body ist alles, was folgt, unversehrt.

Den Body dekodieren

Der Body besteht aus Bytes. JSON ist Text, also dekodiere ihn zuerst, mit dem Encoding, das die Zeile Content-Type nennt, und gib den Text dann an json.loads:

Try it

Das ist die Art von , die dir requests in Python in der Praxis übergeben hat. Was requests.get zwischen Empfangen und Dekodieren getan hat, passiert in den Zeilen oben, auf Bytes, die du sehen kannst.

Wo beginnt der Body einer Antwort?

Das Zerlegen funktioniert hier, weil jedes Byte schon vorlag. Ein Socket liefert Bytes in Stücken, wie du in Networks with Python gesehen hast, also kann ein Client eine Antwort, die er erst teilweise empfangen hat, nicht zerlegen. Die nächste Lektion sendet die Anfrage wirklich und liest den Kopf der Antwort von einem Socket, Stück für Stück.