0%

HTTP is tekst op een socket

Een bericht van regels

Hier is het eerste verzoek dat deze cursus verstuurt: het vraagt practice-api om de eerste pagina van zijn items, twee items per pagina. In Python is het één 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)

Dat is het hele verzoek. Er zit geen verborgen structuur achter: dit zijn de bytes die een client naar de schrijft, en de server leest ze in dezelfde volgorde. De rest van dit hoofdstuk schrijft ze, verstuurt ze en leest wat er terugkomt.

Regels die eindigen op CR LF

Het verzoek bestaat uit regels, en elke regel eindigt op twee bytes: \r, een carriage return, en dan \n, een newline. Samen heten ze CRLF. Het prikbord in Networks with Python sloot elke regel af met alleen \n. koos iets anders, en net als bij het prikbord houden beide kanten zich aan de afspraak, zodat een lezer elke regel kan vinden door naar \r\n te zoeken.

Lees het verzoek regel voor regel:

  1. GET /v1/items?page=1&page_size=2 HTTP/1.1 is de verzoekregel: wat er moet gebeuren, met welke resource, in welke versie van HTTP.

  2. Host: practice-api:8080 is een : een naam, een dubbele punt en een waarde. Deze noemt de server waarvoor het verzoek bedoeld is.

  3. Dan komt een regel waar niets op staat. De lege regel sluit de kop van het verzoek af.

Een antwoord volgt hetzelfde plan. De eerste regel is een statusregel, zoals HTTP/1.1 200 OK; daarna volgen headerregels; een lege regel sluit de kop af; en dan volgt de body, de die je in Python in Practice decodeerde.

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 berichten hebben dezelfde drie delen. Eerst een startregel: de verzoekregel of de statusregel. Dan een blok headerregels. Dan één lege regel, die in bytes alleen \r\n is, dus de kop eindigt op \r\n\r\n: het einde van de laatste headerregel, gevolgd door de lege regel. Alles daarna is de body. Het verzoek hierboven heeft een lege body; een verzoek dat gegevens verstuurt, zoals in latere hoofdstukken, zet ze daar.

Bytes splitsen met split

Bytes en hebben een split. data.split(separator) knipt data bij elk voorkomen van separator en geeft de stukken terug als , zonder de scheidingstekens. Knip het verzoek bij elke \r\n:

Try it

Er zijn vier stukken, en de laatste twee zijn leeg. Het verzoek eindigt op \r\n\r\n, twee scheidingstekens achter elkaar: de eerste \r\n sluit de Host-regel af, en tussen die en de tweede staat niets, dus split geeft daar een leeg stuk terug. Na het laatste scheidingsteken staat ook niets, en dat geeft het tweede lege stuk. De lege regel die de kop afsluit, verschijnt als die lege stukken.

split werkt op tekst op dezelfde manier, en de lijst die het teruggeeft is een gewone lijst, dezelfde die je zou krijgen als je hem uitschreef:

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

Op bytes moet het scheidingsteken bytes zijn, b"\r\n"; op tekst moet het tekst zijn.

Wat drukt dit af?

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

Een antwoord in tweeën splitsen

Een antwoord heeft eerst een andere knip nodig: de kop van de body, bij de eerste \r\n\r\n. Hieronder staat een voorbeeldantwoord in de vorm van practice-api, gebouwd op wat dit verzoek op de site naar verwachting terugkrijgt. De body is die welke practice-api voor deze pagina stuurt; de datum is verzonnen.

Try it

split(b"\r\n\r\n", 1) geeft een lijst van twee items terug: parts[0] is de kop, zonder de lege regel, en parts[1] is de body. Door de kop daarna bij \r\n te splitsen, krijg je één item per regel, de statusregel eerst. Het laatste getal dat wordt afgedrukt is 182, de lengte van de body in bytes, en dat is precies wat de header Content-Length aankondigde. Les 5 gebruikt die header om de body te lezen.

De regels Server en Date komen van de kleine webserver die vóór practice-api staat; als een antwoord dat je ontvangt geen van beide heeft, kwam het rechtstreeks van de dienst, en niets in deze cursus hangt ervan af.

Waarom het tweede argument ertoe doet

De 1 in split(b"\r\n\r\n", 1) is maxsplit: knip hoogstens één keer, bij de eerste overeenkomst, en houd alles daarna als één stuk. Zonder dat knipt split bij elke \r\n\r\n die het vindt. De kop eindigt bij de eerste, maar de body is data, en data kan alles bevatten, ook die vier bytes. Een body die ze bevat, zou in stukken worden geknipt, en parts[1] zou alleen het eerste daarvan zijn. Met maxsplit op 1 komt de kop van de eerste overeenkomst en is de body alles wat volgt, onbeschadigd.

De body decoderen

De body bestaat uit bytes. JSON is tekst, dus decodeer hem eerst, met de codering die de regel Content-Type noemt, en geef de tekst daarna aan json.loads:

Try it

Dit is het soort dat requests je in Python in Practice gaf. Wat requests.get deed tussen ontvangen en decoderen, gebeurt in de regels hierboven, op bytes die je kunt zien.

Waar begint de body van een antwoord?

Splitsen werkt hier omdat elke byte al binnen was. Een socket levert bytes in stukken, zoals je in Networks with Python zag, dus een client kan een antwoord dat hij maar gedeeltelijk heeft ontvangen niet splitsen. De volgende les verstuurt het verzoek echt en leest de kop van het antwoord stuk voor stuk van een socket.