0%

Hoofdstuk 9 · oefening

Eén verbinding, veel verzoeken

De verbinding open houden

Elke uitwisseling in deze cursus heeft tot nu toe een verbinding geopend, één verzoek en één antwoord gedragen, en is daarna gesloten. Je server spreekt /1.0, dat de verbinding na elk antwoord sluit, dus een client die drie dingen wil, maakt drie keer verbinding. Voer dit scenario uit in het tabblad Shell voordat je iets verandert:

python pl_http.py keep-alive

Het eerste deel is de eigen client van pl_http. Die maakt één http.client.HTTPConnection en stuurt er drie verzoeken doorheen: GET /books, HEAD /books en GET /books/1, de laatste met de header Connection: close. Het midden van het rapport zegt wat er gebeurde:

--- pl_http's own http.client connection ---
replies: ['HTTP/1.0 200 OK', 'HTTP/1.0 200 OK', 'HTTP/1.0 200 OK']
connections: 3

Eén verbindingsobject, drie verbindingen: na elk HTTP/1.0-antwoord sloot de server, en http.client maakte stilletjes opnieuw verbinding.

Eén argument

Open http_adapter.py nog een keer. make_handler_class heeft al die tijd een gehad die je nooit hebt meegegeven:

def make_handler_class(handle, state, *, read_body=None, protocol_version="HTTP/1.0"):
    ...
    Handler.protocol_version = protocol_version
    return Handler

http.server leest protocol_version van de handlerklasse. Met "HTTP/1.0" beantwoordt hij één verzoek en sluit. Met "HTTP/1.1" schrijft hij HTTP/1.1 in elke statusregel en leest hij na elk antwoord het volgende verzoek van dezelfde verbinding. Dat is keep-alive, de standaard in HTTP/1.1: een verbinding blijft open voor meer verzoeken totdat een van beide kanten haar beëindigt.

De wijziging is dus één keywordargument in make_server: protocol_version="HTTP/1.1". Breng die aan en voer python pl_http.py keep-alive opnieuw uit:

request 1  GET /books  ->  200 OK
request 2  HEAD /books  ->  200 OK
request 3  GET /books/1  ->  200 OK
--- pl_http's own http.client connection ---
replies: ['HTTP/1.1 200 OK', 'HTTP/1.1 200 OK', 'HTTP/1.1 200 OK']
connections: 1

Drie verzoeken, één verbinding. Het log van je server toont drie verzoekregels, maar je serve- deed voor alle drie samen één aanroep van handle_request(): die aanroep accepteerde de verbinding en keerde terug toen ze eindigde. Het scenario geeft serve vier verbindingen; de drie verzoeken gebruikten er één, een tweede deel van de run gebruikte er nog een, en het resultaatblok zegt pl_http opened 2 empty connection(s) so serve() could stop waiting. Je lus heeft nooit verzoeken geteld. Sinds hoofdstuk 4 telt hij verbindingen, en tot nu toe droeg elke verbinding toevallig één verzoek.

serve(server, max_connections=2) draait onder HTTP/1.1. Eén client stuurt vijf verzoeken over één verbinding en sluit dan. Hoeveel verzoeken worden beantwoord voordat serve op een volgende client wacht?

Wie de verbinding beëindigt

Een open gehouden verbinding eindigt op een van drie manieren:

  • De client stuurt Connection: close mee met een verzoek. De server beantwoordt het en sluit. Het derde verzoek van pl_http deed dat.

  • Een antwoord bevat connection: close. http.server merkt die header op terwijl hij hem schrijft, en sluit na het antwoord.

  • Niemand stuurt iets. De SOCKET_TIMEOUT van de adapter is 2 seconden: na zo lang zonder nieuw verzoek schrijft de server Request timed out: TimeoutError('timed out') naar zijn log en sluit.

Een client ziet de derde soort niet gebeuren. http.client merkt het pas wanneer zijn volgende verzoek over die verbinding mislukt met een ConnectionResetError, of met de subklasse RemoteDisconnected als de client het sluiten door de server al had gelezen; welke van de twee komt, hangt af van de timing. Ook de eerste soort heeft een eigenaardigheid: http.server zet geen Connection-regel in het antwoord waarna hij sluit, en http.client beslist op basis van de headers van het antwoord, dus hij beschouwt die verbinding nog als open totdat hij haar opnieuw probeert te gebruiken.

Keep-alive heeft een prijs op een server als de jouwe: zolang één client zijn verbinding open houdt, wacht de volgende, zoals achter de trage client in Networks with Python. http.server.ThreadingHTTPServer geeft in plaats daarvan elke verbinding een eigen thread. Deze cursus houdt het bij één verbinding tegelijk, zodat pl_http en Check my work ze kunnen tellen.

Je lengtes klopten al

Op een open gehouden verbinding begint het volgende antwoord direct na de laatste byte van dit antwoord, dus een client moet weten waar elke body eindigt. Onder HTTP/1.0 markeerde het sluiten dat; nu moet content-length het zeggen. Je server heeft die header sinds hoofdstuk 4 meegestuurd met elk antwoord dat een body heeft: json_response telt de gecodeerde bytes, een zegt 0, en een 204 of een 304 heeft helemaal geen body.

Een geweigerde body is er nog steeds

Het tweede deel van de keep-alive-run is één ruwe verbinding. pl_http stuurt POST /books met Transfer-Encoding: chunked en zonder content-length, en de body ervan is toevallig zelf een compleet verzoek:

client -> server  b'POST /books HTTP/1.1\r\nHost: books.test\r\nTransfer-Encoding: chunked\r\n\r\nGET /books/2 HTTP/1.1\r\nHost: books.test\r\n\r\n'  (113 bytes)
server -> client  b'HTTP/1.1 411 Length Required\r\nServer: ...
(N bytes followed the reply: b'HTTP/1.1 200 OK\r\nServer: ...
...
127.0.0.1 - - [the date and time] "GET /books/2 HTTP/1.1" 200 -

Regels die eindigen op ... zijn hier ingekort, N hangt af van de Server-regel, en de laatste regel komt uit het log van je server. Je read_body antwoordde terecht met 411 en las niets van de body. Maar de verbinding bleef open, dus de server las verder vanaf de plek waar hij was gestopt en hield de bytes van de body voor het volgende verzoek. Het log toont een GET /books/2 die de client nooit als verzoek heeft gestuurd, en pl_http toont de 200 daarvan na de 411. Hier is die verbinding, voor en na de oplossing:

Na een 411 wordt de ongelezen body als tweede verzoek gelezen, tenzij connection: close de verbinding beëindigt.

Als de body uit gewone chunks had bestaan, zoals 5\r\nhello, dan had de server de regel 5 als verzoekregel gelezen, Bad request syntax ('5') gelogd en een HTML-foutpagina geschreven zonder enige statusregel.

De server antwoordt 411 op een POST en houdt de verbinding open zonder de 40 bytes van de body te lezen. Wat leest hij daarna?

De regel komt van het protocol. RFC 9112, sectie 9.3, zegt dat een server moet “read the entire request message body or close the connection after sending its response”: de hele body van het verzoek lezen, of de verbinding sluiten nadat hij zijn antwoord heeft gestuurd. Je read_body weigert bewust voordat hij leest, dus hij moet sluiten: elk antwoord dat hij teruggeeft (de 411, beide 400’s en de 413) bevat de header connection: close. Antwoorden die worden gemaakt nadat de body volledig is gelezen, zoals 415 of 422, laten de verbinding bruikbaar en hebben niets nodig.

Over die 411: een HTTP/1.1-server is verplicht om chunked bodies van verzoeken te decoderen (RFC 9112, sectie 7.1). De jouwe antwoordt in plaats daarvan met 411, wat volgens RFC 9112, sectie 6.3, echte services ook doen, en een client kan daarop reageren door de body opnieuw te sturen, met een lengte. In les 3 decodeer je zelf chunks, aan de kant van de client.

Bij welk antwoord sluit http.server de verbinding nadat hij het heeft geschreven?

Je opdracht

  1. Geef in make_server protocol_version="HTTP/1.1" mee aan http_adapter.make_handler_class.

  2. Zorg dat elk antwoord waarmee read_body weigert connection: close bevat. Een kleine hulpfunctie die json_error aanroept en de header toevoegt, houdt de vier weigeringen kort.

Voer daarna nog een keer python pl_http.py keep-alive uit. De 411, hier getoond zonder de van Server en Date, beëindigt nu de run:

server -> client  b'HTTP/1.1 411 Length Required\r\nServer: ...\r\nDate: ...\r\ncontent-type: application/json\r\ncontent-length: 56\r\nconnection: close\r\n\r\n...

Er volgt niets meer, en het log bevat geen GET /books/2. Druk op Check my work. Daarna stelt les 2 de vraag die deze les opriep, vanaf de kant van de client: hoe weet een lezer waar een body eindigt?

Opdracht

Zet je server over op /1.1, en sluit de verbinding na elke body die hij weigert te lezen.

In reading_server.py:

  1. Geef in make_server protocol_version="HTTP/1.1" mee aan http_adapter.make_handler_class. Behoud http.server.HTTPServer, het adres ("127.0.0.1", 0) en server.timeout.

  2. Elk antwoord waarmee read_body weigert (de 411 voor een ontbrekende lengte, de 400 voor een misvormde lengte, de 413 en de 400 voor een body die te vroeg eindigde) bevat de header connection: close. Behoud hun en de volgorde van de controles uit hoofdstuk 5.

Verder verandert er niets: de antwoorden van handle, ook zijn foutmeldingen, laten de verbinding open.

In de stuurt python pl_http.py keep-alive drie verzoeken over één verbinding, daarna een chunked POST waarvan de ongelezen body een volledig GET-verzoek is, en toont wat er terugkwam.

Druk daarna op Check my work. Die bouwt je server met make_server zonder een te openen, controleert de protocolversie van de handlerklasse en roept read_body aan met verzoekdicts om te zien dat elke weigering connection: close bevat. Daarna stuurt hij meerdere verzoeken tegelijk door die handlerklasse, als één verbinding, met boeken, gebruikers en tokens die je niet hebt gezien, en splitst hij de antwoorden op hun content-length: drie leesverzoeken moeten drie antwoorden krijgen, een schrijfverzoek gevolgd door een leesverzoek twee, en een geweigerde body, of het nu een verborgen GET is of chunks, precies één antwoord, de weigering. Hij stuurt ook elf verzoeken uit hoofdstuk 4 tot en met 8 over één verbinding om te zien dat elke eerdere regel nog geldt. Niets uit je terminal wordt beoordeeld.