HTTP is tekst op een socket · oefening
Een verzoek met de hand versturen
De vorige les splitste bytes die al binnen waren. Deze maakt ze: je schrijft het verzoek in wire.py, verstuurt het via een connect-
Deze les draait op de servers van Python Land. Het tabblad Project bevat de bestanden van het hoofdstuk: wire.py, waarin je werkt, fakes.py, dat les 5 en 7 gebruiken, tests/test_wire.py, dat les 7 gebruikt, en README.md, met de routes en regels voor dit hoofdstuk. Het tabblad Shell is een
Open wire.py. De constanten zijn onder meer ADDRESS, de naam en HOST_LINE; HOOK_TARGET, het doel van het verzoek uit les 2, dat dit hoofdstuk het openingsverzoek noemt; en de limieten MAX_HEAD en MAX_BODY. make_reader en read_exact zijn de ontvangstbuffer uit Networks with Python, al geschreven. De functies daarna schrijf jij, een paar per les.
De verzoekregel
Een verzoekregel heeft drie delen, gescheiden door enkele spaties: de GET vraagt om een resource); het verzoekdoel (request target), dat de resource op de server noemt (een pad, en eventueel een query na ?); en de versie, HTTP/1.1. CRLF sluit de regel af. Een
Een server leest de verzoekregel door hem bij de spaties en bij de CRLF te knippen, dus het doel mag geen van die tekens bevatten. /v1/items/a b zou de server vier woorden geven waar hij er drie verwacht, en een doel met \r\n erin zou de regel te vroeg afsluiten en een request_line zo’n doel met / begint. Hoofdstuk 2 laat zien hoe je elk teken veilig in een doel zet.
De Host-regel
De tweede regel is de Host-regel, die de server noemt waarvoor het verzoek bedoeld is. wire.py levert hem als HOST_LINE:
HOST_LINE = "Host: practice-api:8080\r\n"
make_request(method, target) voegt de verzoekregel, HOST_LINE en nog één "\r\n" voor de lege regel samen, en codeert de tekst met "ascii", omdat een socket bytes verstuurt. Voor het openingsverzoek:
| Regel | Wat het is |
|---|---|
GET /v1/items?page=1&page_size=2 HTTP/1.1\r\n | de verzoekregel |
Host: practice-api:8080\r\n | de Host-regel |
\r\n | de lege regel: hier eindigt de kop |
Het verzoek eindigt op \r\n\r\n, wat de server vertelt dat de kop voorbij is; dit verzoek heeft geen body. Elke regel eindigt op CRLF, omdat HTTP/1.1 dat eist. Sommige servers accepteren ook een losse \n, maar een tolerante server is geen reden om minder te sturen dan het protocol vraagt.
De kop lezen terwijl hij binnenkomt
De kop van het antwoord eindigt bij de eerste \r\n\r\n. Eén recv geeft terug wat er tot dan toe is aangekomen, misschien het hele antwoord, misschien een deel van de eerste regel, dus de kop lezen is een
Zoek
\r\n\r\ninreader["buffer"]metfind, dat de positie teruggeeft, of-1als het er niet in staat.Zolang het er niet in staat, roep je
reader["sock"].recv(4096)aan, voeg je het stuk aan het eind van de buffer toe en zoek je opnieuw.Staat het er wel in, geef dan de bytes ervóór terug, en zet alles na de lege regel terug in
reader["buffer"].
Controleer de buffer vóór de eerste recv: als de bytes er al zijn, hoef je nergens op te wachten. En bewaar wat na de kop komt. Dat is het begin van de body, die les 5 leest, en een antwoord komt vaak met kop en body in één stuk aan.
De eerste recv in read_head gaf b'HTTP/1.1 200 OK\r\nContent-Ty' terug. Wat moet read_head doen?
De lus kan op twee manieren eindigen zonder kop, en elk krijgt een eigen exception:
recvgeeftb""terug: de server sloot de verbinding voordat de kop eindigde. WerpConnectionErrorop, zoalsread_exactdoet.De buffer groeit voorbij
limitbytes zonder\r\n\r\n: een kop die nooit eindigt, zou anders het geheugen van het programma vullen. WerpValueErrorop, zoals jeread_linein Networks with Python deed voor een regel die nooit eindigde.limitis standaardMAX_HEAD, 16384 bytes.
Versturen via connect
fetch zet alles bij elkaar. De eerste twee regels openen de verbinding en versturen het verzoek:
with connect(ADDRESS, timeout=timeout) as sock:
sock.sendall(make_request(method, target))
connect is een socket.create_connection wordt meegegeven, blijft op de socket staan, dus elke latere recv wacht hoogstens timeout seconden, standaard 2, en werpt dan TimeoutError op. Voorlopig geeft fetch de kop terug als bytes; les 5 maakt er een volledig antwoord van.
Je opdracht
Schrijf in wire.py deze vier functies, in deze volgorde:
request_line(method, target): geeff"{method} {target} HTTP/1.1\r\n"terug; werpValueErrorop als het doel niet met/begint, of een spatie,\rof\nbevat.make_request(method, target): geef de verzoekregel,HOST_LINEen"\r\n"terug, gecodeerd met"ascii".read_head(reader, *, limit=MAX_HEAD): de lus hierboven.fetch(method, target, *, connect=socket.create_connection, timeout=TIMEOUT): maak verbinding metADDRESSmettimeout=timeout, verstuur het verzoek, maak een reader en geef terug watread_headteruggeeft.
Laat de functies die voor les 4 tot en met 6 gemarkeerd zijn zoals ze zijn.
Verstuur daarna het openingsverzoek vanuit het tabblad Shell. main drukt af wat fetch teruggeeft, dus je ziet de kop als bytes-
python wire.py
python wire.py HEAD
De eerste verstuurt GET met HOOK_TARGET, de tweede HEAD met hetzelfde doel. Elk drukt de kop als bytes af: eerst de statusregel, elke regeleinde getoond als \r\n, en geen body. Beide komen meteen terug, omdat read_head bij de lege regel stopt. Niets uit je terminal wordt gecontroleerd.
Wil je zien waar de Host-regel voor dient, verstuur het verzoek dan zonder die regel. Dit commando schrijft de verzoekregel en de lege regel, en niets daartussen:
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)))"
Laat je de Host-regel weg, dan zou je 400 Bad Request moeten krijgen met een korte HTML-pagina in plaats van
Als de lus in read_head je in verwarring brengt, vraag het dan aan Monty:
Waarom moet mijn read_head in de buffer naar \r\n\r\n zoeken voordat het recv aanroept, en niet alleen daarna?
Als python wire.py de kop laat zien, druk je op Check my work. De volgende les maakt van die bytes een
Opdracht
Schrijf vier wire.py, in deze volgorde. Check my work noemt de eerste die nog NotImplementedError opwerpt, zodat je ze een voor een tegenkomt.
request_line(method, target)geeftf"{method} {target} HTTP/1.1\r\n"terug. Het werptop als targetniet met/begint, of als het een spatie,\rof\nbevat.make_request(method, target)geeft bytes terug: de verzoekregel,HOST_LINEen"\r\n", gecodeerd met"ascii".read_head(reader, *, limit=MAX_HEAD)zoekt\r\n\r\ninreader["buffer"], en zolang het er niet in staat, voegt hetreader["sock"].recv(4096)aan het eind van de buffer toe en zoekt opnieuw. Het geeft de bytes vóór de lege regel terug en laat elke byte erna inreader["buffer"]staan. Het werptConnectionErrorop alsrecveerstb""teruggeeft, enValueErrorals de buffer voorbijlimitbytes groeit zonder\r\n\r\n.fetch(method, target, *, connect=socket.create_connection, timeout=TIMEOUT)roeptconnect(ADDRESS, timeout=timeout)aan in eenwith-statement, verstuurtmake_request(method, target)metsendall, verpakt demet make_readeren geeft terug watread_headteruggeeft.
Laat parse_head, read_body en json_body zoals ze zijn.
Voer in de python wire.py en python wire.py HEAD uit.
Druk daarna op Check my work. De controle roept request_line en make_request aan met read_head aan op vervangende sockets met antwoorden die je niet hebt gezien, geleverd in één stuk, één byte tegelijk, op elke positie in tweeën geknipt (ook binnen de lege regel en binnen de body) en in stukken van 7 bytes, elke keer zowel met een server die daarna sluit als met een die de verbinding openhoudt en niets meer stuurt. Ze controleert ook een buffer die al een hele kop bevat, een kop die nooit eindigt, en een server die halverwege de kop sluit. Ze roept fetch aan met een vervangende connect en controleert het adres, de time-out (de standaardwaarde en een die je meegeeft), de verstuurde bytes, de teruggegeven kop en of het with-blok is afgesloten. Ze leest je terminal niet en neemt geen contact op met practice-api.