Hoofdstuk 7 · oefening
Eén server, veel clients
Geef elk bericht een vorm
De echoservice uit hoofdstuk 6 maakte het niet uit wat een bericht betekende. Welke tekst een client ook stuurde, die kwam direct terug, en elke client praatte alleen met de server. Dit hoofdstuk bouwt iets anders: een scorebord voor een quizavond. Eén scorebijhouder stuurt scores, en elke kijker die op een scherm meekijkt, moet hetzelfde bord zien. Voordat dat werkt, moeten de server en zijn clients het eens worden over hoe een bericht eruitziet.
Eén envelop per frame
Elk bericht reist als één tekstframe, en de tekst daarin is "type", dat de lezer vertelt hoe hij de rest moet lezen. Zo’n afgesproken vorm heet vaak een envelop: het type staat aan de buitenkant, en de velden binnenin hangen ervan af.
Het scorebord kent drie
{"type": "snapshot", "version": 3, "scores": {"Owls": 4, "Foxes": 2}}
{"type": "score", "team": "Owls", "points": 3}
{"type": "error", "version": 3, "reason": "that team is not playing tonight"}
Een snapshot is het hele bord: de punten van elk team. Een score is de scorebijhouder die de server vraagt punten bij één team op te tellen. Een error zegt dat er iets misging; dit hoofdstuk definieert het, en in hoofdstuk 9 begint de server het te sturen.
De server vertelt een nieuwe kijker de punten van elk team. Welk type stuurt hij?
Versies nummeren het bord
Een snapshot en een error dragen een version, een geheel getal dat begint bij 0 voor het lege bord en bij elke verandering met één omhooggaat. De versie van een snapshot zegt welke toestand van het bord hij toont, dus een client die versie 6 ontvangt na versie 5, weet dat hij het nieuwere bord heeft. Versies gaan alleen omhoog.
Een score heeft geen versie. Alleen de server verandert het bord, dus alleen de server kan zeggen welke toestand een bericht beschrijft. De scorebijhouder vraagt om een verandering; de server voert die uit en nummert het resultaat. Dat is de regel van deze cursus voor zijn protocol, geen regel van RFC 6455, die niets zegt over wat je berichten bevatten.
Waarom draagt een score-bericht geen versie?
Het volledige contract, veld voor veld:
snapshot:version, een geheel getal van 0 of meer, enscores, een object waarvan elkeeen geheel getal is; score:team, eendie niet leeg is, en points, een geheel getal van 1 tot en met 10 (MIN_POINTSenMAX_POINTSbovenaanscoreboard_messages.py);error:version, zoals bij een snapshot, enreason, een string.
Elk ander type wordt geweigerd, en ook alles wat geen object is, zoals een JSON-
True is een int
“Een geheel getal” klinkt als isinstance(points, int). Dat is het niet helemaal. In Python is bool een soort int, dus True komt door die controle, en JSON’s true komt in Python aan als True. Een scorebijhouder die "points": true stuurde, zou per ongeluk één punt scoren.
Wat drukt dit af?
print(isinstance(True, int), type(True) is int)
Dus is_whole_number(value) geeft alleen True terug voor een int die geen bool is. Twee isinstance-controles, verbonden met and not, zeggen precies dat. Gebruik het voor punten, versies en elke score in een snapshot. Een float zoals 2.0 en een string zoals "3" zijn ook geen gehele getallen, en is_whole_number zegt dat al. Controleer het type voordat je vergelijkt: "3" < 1 werpt ValueError.
Controleer op de heenweg, controleer op de terugweg
check_message(message) bevat het hele contract. Hij geeft niets terug als de envelop geldig is, en werpt ValueError op met een korte reden als dat niet zo is. Lees de velden met message.get(...), dat None geeft voor een ontbrekende sleutel, zodat een ontbrekend team op dezelfde controle zakt als een leeg team. Voor een leeg team is team.strip() == "" de test.
Twee encode_message(message) controleert voordat hij codeert, zodat de server nooit een kapotte envelop kan sturen, en geeft json.dumps(message, ensure_ascii=False) terug. Zonder ensure_ascii=False schrijft json.dumps een team met de naam Café als Caf\u00e9; beide zijn geldige JSON, en de leesbare vorm is makkelijker te controleren in een transcript.
parse_message(data) werkt de andere kant op. data is wat recv van de bibliotheek teruggaf. Een tekstframe geeft een str, een binair bytes, en het scorebord spreekt alleen tekst. Weiger eerst alles wat geen str is, met ValueError. Die controle moet je zelf doen: json.loads accepteert ook bytes en zou zonder morren een binair frame lezen. Roep daarna json.loads aan. Tekst die geen JSON is, laat die json.JSONDecodeError opwerpen, een soort ValueError, dus die mag je gewoon laten doorgaan. Controleer tot slot de envelop en geef hem terug.
Elke weigering is een ValueError, wat de reden ook is. De handler van de scorebijhouder in les 6 vangt die ene exception en sluit de verbinding.
Uitvoeren
Klik op Run. scoreboard_demo.py drukt per les één sectie af; zolang de functies van een les niet geschreven zijn, zegt die sectie not written yet. Een afgemaakte versie, op de site uitgevoerd, drukte dit af voor les 1:
Lesson 1: two envelopes and one refusal
encoded: {"type": "snapshot", "version": 3, "scores": {"Owls": 4, "Café Quiz": 2}}
parsed: {'type': 'score', 'team': 'Owls', 'points': 3}
ValueError: points must be a whole number from 1 to 10
De eerste regel is de tekst die één frame zou dragen. De tweede is de parse_message teruggaf. De derde is de weigering van "points": true; de formulering is van jou, dus die van jou kan afwijken. Klik daarna op Submit.
De enveloppen bestaan, en nog niets verstuurt ze. In de volgende les maakt een kijker verbinding en ontvangt zijn eerste snapshot.
Opdracht
Schrijf in scoreboard_messages.py de vier NotImplementedError opwerpt.
is_whole_number(value):Truevoor eenintdie geenboolis, enFalsevoor al het andere.check_message(message): geef niets terug voor een geldige envelop en werpop voor al het andere. Een snapshotheeft eenversionnodig die een geheel getal van 0 of meer is, en eenscoreswaarvan deallemaal gehele getallen zijn. Een errorheeft dezelfdeversionnodig en eenreasondie eenis. Een scoreheeft een stringteamnodig die niet leeg is, enpointsals geheel getal vanMIN_POINTStot en metMAX_POINTS. Weiger elk andertypeen alles wat geen dict is. Negeer extra sleutels.encode_message(message): roepcheck_messageaan en geef daarnajson.dumps(message, ensure_ascii=False)terug.parse_message(data): werpValueErrorop tenzijdataeenstris; voer daarnajson.loadserop uit, roepcheck_messageaan op het resultaat en geef het terug.
Klik op Run en lees de sectie van les 1, en klik daarna op Submit. De controle roept je functies aan met enveloppen die de uitvoer van Run niet toont, zoals een snapshot met een team met de naam Café Quiz op versie 0 en 41, en scores van 1 en 10 punten voor een team met de naam Bärchen. Ze verwacht dat elk ervan wordt gecodeerd als tekst die json.loads terugzet in dezelfde envelop, en ongewijzigd wordt teruggelezen, extra sleutels inbegrepen. Ze stuurt vijftien kapotte enveloppen door zowel encode_message als parse_message, waaronder punten van 0, 11, true, 2.0 en "3", een leeg team, een versie van -1 of true, een snapshotscore van false, een error zonder reden, een onbekend type en een ValueError. parse_message moet ook de bytes van een binair "not json" en de lege string weigeren. De tekst van elke ValueError kies je zelf, en type(value) is int is net zo goed als twee isinstance-controles.