Hoofdstuk 4
Zelf HTTP serveren
De verbinding omdraaien
In hoofdstuk 1 tot en met 3 schreef je verzoekregels en las je antwoordregels. practice-api deed de andere helft, en je kon geen byte veranderen van wat hij terugstuurde. Dit hoofdstuk draait de verbinding om. Een server leest de verzoekregels die een client schreef, beslist wat hij antwoordt, en schrijft de antwoordregels die die client zal lezen. Hij doet die drie dingen één keer per verbinding, op een socket die hij van accept kreeg, zoals je prikbordserver in Networks with Python deed. Het verschil is de tekst op de socket: HTTP, dat je nu in beide richtingen kunt lezen.
Dezelfde regels, vanaf de andere kant
Volg één verzoek van de client die je bouwde, door een server heen, en terug:

Je client uit hoofdstuk 1 schrijft GET /books HTTP/1.1, Host: books.test en een lege regel. Op de server leest de module http.server van Python de verzoekregel en headers van de socket, en een klein bestand dat bij dit hoofdstuk wordt meegeleverd, de adapter, maakt er een verzoek-dict van. Een functie die je zelf schrijft, handle(request, state), beslist het antwoord en geeft het terug als antwoord-dict. De adapter geeft die dict aan http.server, dat hem als bytes terugschrijft: de statusregel HTTP/1.0 200 OK, zijn eigen regels Server en Date, de header content-length: 13, een lege regel, en de body {"books": []}. Je client leest die regels precies zoals hij in hoofdstuk 1 deed.
Hier zijn de leeshelft en de schrijfhelft met de hand gedaan op vastgelegde bytes. Het draait in je browser, dus er komt geen socket aan te pas; elke byte is een letterlijke waarde:
De eerste helft leest: ze knipt de kop af bij de lege regel, splitst hem in regels, en splitst de verzoekregel in methode, doel en versie. De tweede helft schrijft: ze bouwt een JSON-body als bytes en de statusregel en de regel content-length die een server ervoor zou sturen. In dit hoofdstuk doen http.server en het meegeleverde bestand beide helften voor je. Het beslissen in het midden is aan jou.
Wie doet wat
Drie onderdelen verdelen het werk:
http.serverleest de verzoekbytes van de socket en schrijft de antwoordbytes terug. Het controleert of de verzoekregel goed gevormd is en vindt de methode.http_adapter.py, meegeleverd in je werkruimte, maakt van wathttp.serverlas een verzoek-dict, roept je functie aan, en geeft de antwoord-dict terug aanhttp.serverom te schrijven:send_responseschrijft de statusregel,send_headerschrijft elke headerregel,end_headersschrijft de lege regel, en daarna gaat de body ongewijzigd naar buiten.Je functie,
handle(request, state), beslist. Ze krijgt een verzoek-dict en een state-dict met de boeken, en geeft een antwoord-dict terug. Ze raakt nooit een socket aan.
De twee dicts zien er zo uit:
request = {
"method": "GET",
"target": "/books?limit=2",
"version": "HTTP/1.1",
"headers": {"host": ["books.test"], "accept": ["application/json"]},
"body": b"",
}
response = {
"status": 200,
"headers": {"content-type": "application/json", "content-length": "13"},
"body": b'{"books": []}',
}
print(request["method"], request["target"])
print(response["status"], response["headers"]["content-length"], response["body"])
Headernamen zijn in beide dicts in kleine letters. In het verzoek is elke naam gekoppeld aan een lijst waarden, dezelfde vorm die parse_head in hoofdstuk 1 bouwde, zodat een header die twee keer werd gestuurd beide waarden houdt. De body bestaat uit bytes, omdat bytes zijn wat er reist.
Wat er op de lijn gaat
De adapter schrijft precies wat je antwoord-dict zegt. http.server voegt direct na de statusregel twee eigen headerregels toe: Server, met de naam van de bibliotheek en de Python-versie, en Date, het tijdstip van het antwoord. Er wordt niets anders toegevoegd en niets weggehaald. Heeft je dict een body en geen content-length, dan gaat het antwoord zonder lengte naar buiten. Onder HTTP/1.0 markeert het sluiten van de verbinding door de server dan het einde van de body, maar een client die de lengte nodig heeft, zoals de wire.py die je in hoofdstuk 1 schreef, weigert dat antwoord. De bytes van de body tellen is dus jouw taak.
De adapter spreekt HTTP/1.0 tot hoofdstuk 9. Zijn statusregel begint met HTTP/1.0, ook als de client HTTP/1.1 schreef, en de server sluit de verbinding na één antwoord, dus elke verbinding vervoert precies één verzoek.
De adapter lezen
Hier is het midden van make_handler_class in http_adapter.py:
class Handler(http.server.BaseHTTPRequestHandler):
timeout = SOCKET_TIMEOUT
def respond(self):
request: dict = request_record(self)
if read_body is not None:
error = read_body(request, self.rfile.read)
if error is not None:
write_response(self, error)
return
write_response(self, handle(request, state))
def do_GET(self):
self.respond()
def do_HEAD(self):
self.respond()
def do_POST(self):
self.respond()
def do_PUT(self):
self.respond()
def do_DELETE(self):
self.respond()
Het gebruikt drie constructies die je eerdere cursussen niet hebben behandeld. Elk kost een zin of twee om te lezen.
class Handler(http.server.BaseHTTPRequestHandler): definieert een nieuw soort object, Handler, gebouwd op BaseHTTPRequestHandler van http.server zelf. Omdat het daarop gebouwd is, weet http.server hoe het te gebruiken: voor elke verbinding maakt het een nieuwe Handler en roept het de methoden ervan aan, zoals do_GET voor een GET-verzoek. Een verzoek waarvan de methode hier geen do_-methode heeft, zoals PATCH, krijgt een 501-antwoord van http.server zelf, en je functie wordt nooit uitgevoerd.
Elke def binnen de klasse krijgt eerst self. self is de handler voor het ene verzoek dat wordt beantwoord, dus self.command is de methode van dat verzoek (request_record leest hem als handler.command) en self.respond() roept een andere van zijn methoden aan.
De : dict na request is een annotatie, een notitie voor de lezer dat deze naam een dict bevat. Python controleert hem niet; hem verwijderen zou niets veranderen.
Python Fundamentals II behandelt klassen, self en annotaties volledig. Deze cursus vraagt je nooit om er een van te schrijven: alles wat je schrijft, is een gewone functie. make_handler_class bouwt deze klasse rond jouw handle en jouw state en geeft haar terug; in les 3 geef je die klasse aan http.server.HTTPServer. De regels met read_body blijven ongebruikt tot hoofdstuk 5.
Welke stap is van jou in de server van deze cursus?
Je antwoord-dict heeft een body maar geen content-length. Wat bereikt de client?
Een client verstuurt PATCH /books. Wat gebeurt er?
De adapter schrijft één regel per header. Wat drukt dit af?
response = {"status": 200, "headers": {"content-type": "application/json", "content-length": "2"}, "body": b"[]"}
lines = []
for name in response["headers"]:
lines.append(name + ": " + response["headers"][name])
print(len(lines), lines[1])
Niets in je werkruimte beantwoordt nog een verzoek: handle werpt nog NotImplementedError op. De volgende les schrijft het.