HTTP is tekst op een socket · oefening
Precies de body lezen
read_head stopt bij de lege regel, en parse_head vertelt je wat de kop zegt. Wat na de lege regel volgt, is de body, en een deel daarvan zit misschien al in reader["buffer"], omdat het in hetzelfde stuk aankwam als het einde van de kop. De vraag van deze les is waar de body eindigt. Een reader die te vroeg stopt, levert een half document af; een reader die op meer wacht dan de server stuurt, wacht tot zijn time-out.
Er zijn twee mogelijke antwoorden: lezen totdat de server de verbinding sluit, of een aantal bytes lezen dat de server heeft aangekondigd. Het eerste lijkt eenvoudiger, dus probeer dat eerst. Houd daarbij één feit in gedachten: een Connection: close zegt. Hoofdstuk 9 gaat precies daarover.
Lezen totdat de server sluit
Lezen tot het sluiten is de recv b"" teruggeeft:
data = b""
chunk = sock.recv(4096)
while chunk != b"":
data = data + chunk
chunk = sock.recv(4096)
Probeer het tegen practice-api. Het commando hieronder is python -, dat het programma leest uit de regels die volgen, tot aan de regel met EOF, dus plak het hele blok in het tabblad Shell, eerste en laatste regel inbegrepen. Het verstuurt het openingsverzoek en voert de lus uit op een
python - <<'EOF'
import socket
import wire
sock = socket.create_connection(wire.ADDRESS, timeout=2.0)
sock.sendall(wire.make_request("GET", wire.HOOK_TARGET))
data = b""
chunk = sock.recv(4096)
while chunk != b"":
data = data + chunk
chunk = sock.recv(4096)
print(data)
EOF
Je zou moeten zien dat het ongeveer twee seconden wacht en stopt met TimeoutError: practice-api houdt de verbinding na zijn antwoord open, dus er komt nooit een b"". Komt in plaats daarvan de hele body in één keer terug, dan sloot de dienst de verbinding direct na het antwoord; lezen tot het sluiten werkte deze keer toevallig.
Zelfs als het werkt, kan lezen tot het sluiten niet zien of de body compleet is. Een body die helemaal aankwam en een body die werd afgekapt door een verbroken verbinding eindigen op dezelfde manier, met b"", en de lus geeft terug wat hij heeft. En een server die de verbinding openhoudt, zoals HTTP/1.1 verwacht, stuurt die b"" helemaal nooit. Je kunt alle drie de eindes zonder netwerk zien, met de gescripte sockets uit fakes.py. Het eerste script levert een heel antwoord en sluit, het tweede sluit twee bytes te vroeg, en het derde levert het hele antwoord en blijft daarna stil, zoals een open verbinding doet:
python - <<'EOF'
from fakes import scripted_socket
def read_until_close(sock):
data = b""
chunk = sock.recv(4096)
while chunk != b"":
data = data + chunk
chunk = sock.recv(4096)
return data
reply = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
print(read_until_close(scripted_socket([reply])))
print(read_until_close(scripted_socket([reply[:-2]])))
print(read_until_close(scripted_socket([reply, None])))
EOF
De eerste twee regels die het afdrukt, zijn:
b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello'
b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhel'
en de derde aanroep eindigt in een TimeoutError: scripted silence is. Het tweede antwoord is kapot, en niets erin zegt dat. Het derde was compleet, en de reader kwam daar nooit achter.
De lengte komt eerst
Het antwoord hierboven bevat het antwoord al: Content-Length: 5. De server kondigt de grootte van de body in bytes aan in een header, voordat de body begint. Dat is het lengteprefix van het prikbord uit Networks with Python, als tekst in een read_exact werpt ConnectionError op als de verbinding eerst sluit.
read_exact(reader, n), dat al in wire.py staat, doet het tellen. Het neemt eerst bytes uit reader["buffer"] en roept recv alleen aan voor wat nog ontbreekt, zodat de bodybytes die met de kop aankwamen niet verloren gaan. Volg het voor een body van 182 bytes, waarvan de eerste 100 met de kop aankwamen:

Na read_head bevat reader["buffer"] die eerste 100 bytes, en de andere 82 zijn nog onderweg. read_exact(reader, 182) begint bij die 100 bytes, roept recv aan totdat de buffer er 182 bevat, en geeft de eerste 182 bytes terug; alles daarna blijft in de buffer.
Regels voor deze client
read_body(reader, method, headers) leest de lengte uit de geparste headers, die parse_head als content-length. Voordat het ook maar één bodybyte leest, controleert het de lengte:
Precies één
. Een lijst met twee waarden is dubbelzinnig: de server zei twee verschillende dingen, of twee keer hetzelfde. Het protocol eist dat een client verschillende waarden weigert; deze client weigert elke herhaling. Alleen cijfers. De waarde moet uit ASCII-cijfers bestaan, gecontroleerd met
isascii()enisdigit(), net als de. 12aof-5is geen aantal bytes.Niet te groot. Hoogstens
MAX_BODY, een miljoen bytes. Vergelijk het getal met de limiet vóór het lezen, zoals het prikbord de aangekondigde lengte van een frame met zijn limiet vergeleek: een reader die eerst leest en later controleert, heeft al ontvangen wat de server ook maar besloot te sturen.
Een lengte die een van deze regels breekt, is misvormd, en read_body werpt
Twee soorten antwoorden zijn geldige HTTP die deze client nog niet kan lezen. Een antwoord zonder Content-Length kan zijn body beëindigen door de verbinding te sluiten, en een antwoord met een header Transfer-Encoding, zoals Transfer-Encoding: chunked, stuurt zijn body in stukken, elk met een eigen lengte. Geen van beide is fout, maar deze client kan geen van beide bodies nog afmeten, dus read_body weigert ze allebei met ValueError en een melding die dat zegt. Hoofdstuk 9 voegt beide toe. Controleer eerst op transfer-encoding, en daarna op een ontbrekende content-length.
Bytes, geen tekens
Content-Length telt bytes, en read_exact telt bytes, dus die twee komen overeen. Decodeer de body pas als hij helemaal is aangekomen. Zoals je in Networks with Python zag, kan één teken in
json_body(response) doet het decoderen, zodra de body compleet is: json.loads(response["body"].decode("utf-8")).
Na read_head bevat de buffer 10 bytes van de body, en Content-Length is 25. Hoeveel bytes moeten er nog van de socket komen?
Je opdracht
In wire.py:
Schrijf
read_body(reader, method, headers). WerpValueErrorop alsheaders"transfer-encoding"bevat, als het geen"content-length"bevat, als"content-length"meer dan één waarde bevat, als die waarde niet uit ASCII-cijfers bestaat, of als het getal groter is danMAX_BODY. Geef andersread_exact(reader, length)terug. Demethodis voor de volgende les; deze les gebruikt hem niet.Schrijf
json_body(response): decodeerresponse["body"]met"utf-8"en geef terug watjson.loadsoplevert.Maak
fetchaf. Parse binnen hetwith-blok, na het maken van de reader, de kop metparse_head(read_head(reader)), slaread_body(reader, method, response["headers"])op onder de sleutel"body"van die, en geef de dictionary terug.
Verstuur daarna twee verzoeken vanuit het tabblad Shell:
python wire.py
python -c "import wire; print(wire.json_body(wire.fetch('GET', '/v1/items/1')))"
Het eerste drukt het hele antwoord af als dictionary, met de body als bytes; het tweede drukt het item zelf af, {'category': 'color', 'id': 1, 'name': 'blue', 'value': 10}. Beide komen terug zodra de body compleet is. Geen van beide wacht tot de server de verbinding sluit.
Druk daarna op Check my work. De volgende les verstuurt het ene verzoek waarvan het antwoord een lengte aankondigt en toch geen body heeft.
Opdracht
Schrijf in wire.py read_body en json_body, en maak fetch af.
read_body(reader, method, headers) krijgt een reader waarvan de kop is gelezen en de headers die parse_head teruggaf. Het werpt
als
headers"transfer-encoding"bevat, of geen"content-length"bevat: zo’n body kan deze client nog niet afmeten;als
"content-length"meer dan éénbevat, of een waarde die niet uit ASCII-cijfers bestaat; als de lengte groter is dan
MAX_BODY, voordat er een bodybyte is ontvangen.
Anders geeft het read_exact(reader, length) terug: precies het aangekondigde aantal bytes, te beginnen met de bytes die al in reader["buffer"] staan, en met elke byte na de body nog in de buffer. De method is voor de volgende les.
json_body(response) geeft json.loads(response["body"].decode("utf-8")) terug.
fetch houdt zijn with-blok. Daarbinnen zet het, na het maken van de reader, response = parse_head(read_head(reader)), daarna response["body"] = read_body(reader, method, response["headers"]), en geeft het response terug.
Voer in de python wire.py en python -c "import wire; print(wire.json_body(wire.fetch('GET', '/v1/items/1')))" uit.
Druk daarna op Check my work. De controle roept read_body aan na de kop van antwoorden die je niet hebt gezien, geleverd in één stuk, één byte tegelijk, op elke positie in tweeën geknipt (ook binnen de body en binnen een teken van twee bytes) en in stukken van 7 bytes, elke keer zowel met een server die daarna sluit als met een die de verbinding openhoudt. Op één antwoord volgt het begin van een volgend. Ze geeft read_body ook misvormde en herhaalde lengtes, een antwoord zonder Content-Length, antwoorden met Transfer-Encoding, een lengte ver boven de limiet, en een server die halverwege de body sluit. Ze roept fetch aan met een vervangende connect op een antwoord met 200 en een met 404, en json_body op een body met tekens buiten ASCII. Ze leest je terminal niet en neemt geen contact op met practice-api.