Ein Server, viele Clients · Abschlussprojekt
Abschlussprojekt: Projekt: Anzeigetafel v1
Zuschauer können beitreten, sich registrieren und jede Änderung empfangen, und deine Tests beweisen es. Was fehlt, ist die Verbindung, die überhaupt etwas ändert: die des Punktezählers, auf /score. handle_connection schickt sie schon an scorekeeper_handler, den letzten Stub in scoreboard_server.py. Wenn du ihn schreibst, ist Anzeigetafel v1 fertig, und dann führst du den ganzen Server mit mehreren Clients gleichzeitig aus.
Der Handler des Punktezählers
scorekeeper_handler(connection, board) ist eine begrenzte MAX_SCORE_MESSAGES (10) Nachrichten, jede mit connection.recv(timeout=WAIT), und für jede:
parse sie mit
scoreboard_messages.parse_message;prüfe, dass ihr
type"score"ist: Ein Snapshot oder ein Fehler ist ein gültiger Umschlag, aber nichts, was ein Punktezähler senden darf;wende sie mit
apply_score(board, message)an, das sie an jeden Zuschauer sendet und den neuen Snapshot zurückgibt;antworte dem Punktezähler mit diesem Snapshot, codiert mit
scoreboard_messages.encode_message, damit er die Tafel sieht, die seine Wertung erzeugt hat.
Drei andere Enden, von denen jedes die Verbindung schließt und zurückkehrt:
recvlöstTimeoutErroraus: 2 Sekunden lang kam keine Wertung; schließe mit 1000 und einem Grund wie"no score arrived in time";einer der Schritte 1 bis 3 löst
aus (Bytes, Text, der kein ist, ein kaputter Umschlag, der falsche oder ein Team, das nicht mitspielt): Schließe mit 1008 und einem Grund wie "that was not a valid score"und wende nichts an;die zehnte Nachricht ist beantwortet: Schließe mit 1000 und
"message limit reached".
Zähle die Nachrichten, die du liest, nicht die Wertungen, die du anwendest: Eine abgelehnte Nachricht beendet die Schleife sowieso, und eine Grenze für Lesevorgänge ist das, was eine Verbindung davon abhält, einen Thread ewig festzuhalten. Eine while-Schleife mit einem Zähler, wie im Echo-Handler, oder eine for-Schleife über range(MAX_SCORE_MESSAGES) funktionieren beide. Schließe nach dem Ende der Schleife mit 1000, außerhalb von ihr.
Wenn du für Schritt 2 bei falschem Typ selbst ValueError auslöst, kann ein einziges except ValueError: jede ungültige Nachricht behandeln. Und packe die ganze Schleife in ein try, dessen except ConnectionClosed: zurückkehrt: Wenn der Punktezähler schließt oder irgendwann verschwindet, soll der Handler still enden, wie der des Zuschauers.
Kapitel 9 wird einen Fehler mit einem error-Umschlag beantworten und die Verbindung offen halten. Vorerst beendet ein Fehler das Gespräch. Die Antwort kommt absichtlich zuletzt: Der Punktezähler erfährt von Version 1 erst, wenn sie an jeden Zuschauer gesendet wurde, und der Lauf unten lässt dich das sehen.
Der Punktezähler sendet {"type": "score", "team": "Owls", "points": 11}. Wie beendet der Server dieses Kapitels die Verbindung?
pl_ws.py kennenlernen
Check my work wiederholt zuerst die Prüfungen der Lektionen 1 bis 4 und testet dann deinen Handler mit Stellvertretern, wie die früheren Lektionen. Um das Echte zu sehen, brauchst du einen Server und mehrere Clients, die gleichzeitig über echte pl_ws.py mit, ein für diesen Kurs geschriebenes Programm, das jeden Client spielt. Es ist kein Werkzeug, das du anderswo findest. Es startet deinen Server aus make_handler auf
Führe es ohne Szenario aus, um die Szenarien aufzulisten; das für Kapitel 7 heißt quiz-round:
python pl_ws.py quiz-round
Die Zuschauer A, B und
scenario quiz-round: viewers A, B and C join in turn, then a scorekeeper scores
server options: the library's defaults with close_timeout=1.0 (no open_server in this chapter yet)
viewer A joined; its first message was a snapshot with version 0
viewer B joined; its first message was a snapshot with version 0
viewer C joined; its first message was a snapshot with version 0
the registry holds 3 viewers
the scorekeeper sent 3 points for Owls; its reply was a snapshot with version 1
the scorekeeper sent 2 points for Foxes; its reply was a snapshot with version 2
before the reply with version 1 reached the scorekeeper, the server had sent that version to 3 of 3 viewers
before the reply with version 2 reached the scorekeeper, the server had sent that version to 3 of 3 viewers
viewer B left after version 2; the registry holds 2 viewers
the scorekeeper sent 5 points for Herons; its reply was a snapshot with version 3
the server shut down
viewer A received versions [0, 1, 2, 3], then the server closed it with 1001
viewer B received versions [0, 1, 2], then it closed the connection itself
viewer C received versions [0, 1, 2, 3], then the server closed it with 1001
Lies es von oben. Die erste Nachricht jedes Zuschauers war ein Snapshot. Das Register wuchs auf drei und schrumpfte auf zwei, als B ging, durch den finally-Block aus Lektion 3. Die Versionen jedes Zuschauers steigen nur. Am Ende hat das Herunterfahren des Servers die übrigen Zuschauer mit 1001 geschlossen, und so endet der Handler eines Zuschauers, wenn ihn nichts anderes beendet.
pl_ws.py meldet Reihenfolge und Anzahl, nie Zeiten, weil die Threads mehrerer Clients nicht auf jeder Maschine in derselben Reihenfolge fertig werden. Die Reihenfolge, die es meldet, ist der interessante Teil: Bevor jede Antwort den Punktezähler erreicht hat, hatte der Server diese Version schon an alle drei Zuschauer gesendet.
Warum kommt im Protokoll jede Antwort an den Punktezähler nach der Kopie des Updates für jeden Zuschauer?
Du kannst auch python scoreboard_demo.py ausführen, dessen Abschnitt zu Lektion 6 zweimal wertet und dann 11 Punkte sendet, und deine Tests aus Lektion 5, die weiterhin bestehen sollten:
python -m pytest -q
Wenn quiz-round bis zum Ende läuft, klicke auf Check my work.
Worauf das Protokoll hindeutet
Drei Zuschauer auf Loopback, alle in einem Prozess auf einer Maschine, antworten sofort, also bemerkt niemand das Warten. Aber jede Antwort wartet auf das Senden an jeden Zuschauer, und während ein Senden läuft, wird das Lock der Tafel gehalten, also warten auch die nächste Wertung und jeder neue Beitritt. Ein einziger Zuschauer mit einer langsamen Verbindung würde den Punktezähler und alle anderen auf sich warten lassen. Kapitel 8 misst das und gibt dann jedem Zuschauer seinen eigenen Sender.
Aufgabe
Schreibe in scoreboard_server.py scorekeeper_handler(connection, board):
lies höchstens
MAX_SCORE_MESSAGESNachrichten, jede mitconnection.recv(timeout=WAIT);rufe für jede
scoreboard_messages.parse_messageauf, prüfe, dass ihr"score"ist, rufeapply_score(board, message)auf und sende den zurückgegebenen Snapshot, codiert mitscoreboard_messages.encode_message;schließe bei
TimeoutErrormit 1000 und einem Grund und kehre zurück;schließe bei
aus dem Parsen, der Typprüfung oder apply_scoremit 1008 und einem Grund und kehre zurück;schließe nach der letzten erlaubten Nachricht mit 1000 und einem Grund;
kehre bei
ConnectionClosedan beliebiger Stelle zurück, ohne eine Ausnahme auszulösen.
Führe python pl_ws.py quiz-round im pl_ws.py nicht aus und liest dein Terminal nicht. Sie wiederholt zuerst die Prüfungen der Lektionen 1 bis 4 und führt dann deinen Handler auf Stellvertreter-Verbindungen aus, mit zwei registrierten Stellvertreter-Zuschauern. Zwei Wertungen müssen mit den Versionen 1 und 2 beantwortet werden, jede Antwort gesendet, nachdem beide Zuschauer diese Version bekommen haben, und nichts darf geschlossen werden, bis der Punktezähler schließt. Nach einer gültigen Wertung müssen 11 Punkte, ein Team namens Bats, "not json", ein Binär-error-Umschlag jeweils keine Antwort bekommen, ein Schließen mit 1008 auslösen, kein weiteres Lesen nach sich ziehen und die Tafel auf Version 1 lassen. Ein Punktezähler, der nicht rechtzeitig etwas sendet, muss mit 1000 geschlossen werden. Zwölf wartende Wertungen müssen zehn Antworten und ein Schließen mit 1000 bekommen, wobei die elfte nie gelesen wird. Eine Verbindung, die endet, oder eine Antwort, die sie geschlossen vorfindet, muss den Handler still beenden. Jedes recv braucht ein Timeout über 0 und höchstens 2 Sekunden. Eine for-range(MAX_SCORE_MESSAGES) ist so gut wie ein Zähler, und die Texte der Gründe wählst du selbst.