0%

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 , het formaat dat je in Python in Practice gebruikte. De JSON is altijd een met een veld "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, en scores, een object waarvan elke een geheel getal is;

  • score: team, een die niet leeg is, en points, een geheel getal van 1 tot en met 10 (MIN_POINTS en MAX_POINTS bovenaan scoreboard_messages.py);

  • error: version, zoals bij een snapshot, en reason, een string.

Elk ander type wordt geweigerd, en ook alles wat geen object is, zoals een JSON-. Extra sleutels worden bij binnenkomst genegeerd. Dat is een keuze die deze cursus maakt zodat het protocol kan groeien: een latere versie kan een veld toevoegen zonder een client te breken die het nog niet kent. Of een team echt meespeelt, hoort niet bij de controle van de envelop; alleen het bord kent zijn teams, en les 4 controleert dat.

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 op, geen 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 gebruiken hem. 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 geeft 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 die 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 , in deze volgorde. Submit noemt de eerste die nog NotImplementedError opwerpt.

  1. is_whole_number(value): True voor een int die geen bool is, en False voor al het andere.

  2. check_message(message): geef niets terug voor een geldige envelop en werp op voor al het andere. Een snapshot heeft een version nodig die een geheel getal van 0 of meer is, en een scores waarvan de allemaal gehele getallen zijn. Een error heeft dezelfde version nodig en een reason die een is. Een score heeft een string team nodig die niet leeg is, en points als geheel getal van MIN_POINTS tot en met MAX_POINTS. Weiger elk ander type en alles wat geen dict is. Negeer extra sleutels.

  3. encode_message(message): roep check_message aan en geef daarna json.dumps(message, ensure_ascii=False) terug.

  4. parse_message(data): werp ValueError op tenzij data een str is; voer daarna json.loads erop uit, roep check_message aan 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 , en verwacht van elk ervan 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.