0%

Abschlussprojekt: Eine Live-Anzeigetafel · Abschlussprojekt

Abschlussprojekt: Den begrenzten Server fertigstellen

Seit Kapitel 7 ist der Server nur innerhalb von pl_ws.py gelaufen, das ihn aus deinen gebaut und wieder heruntergefahren hat. Ein Programm braucht ein Außen: eine , eine Adresse, die es den Leuten nennt, eine Grenze dafür, wie viele hereinkommen dürfen, und eine Möglichkeit, von selbst zu stoppen. Diese Lektion schreibt die letzten beiden Funktionen von scoreboard_server.py, serve_scoreboard und main.

Die vorgegebenen Teile lesen

Drei vorgegebene Teile erledigen den Großteil der Infrastruktur; lies sie zuerst.

server_parser() deklariert fünf Optionen mit ihren Standardwerten: --port 8765, --max-clients 5, --max-updates 10, --deadline 5 Sekunden und --token-file token.txt. Ihre Bereiche zu prüfen ist Aufgabe von main.

make_room_check(board, max_clients) gibt einen process_response-Hook zurück. Der process_request-Hook aus Kapitel 9 läuft, bevor die Bibliothek überhaupt geprüft hat, ob die Anfrage ein gültiges Upgrade ist. Würde der Platz dort gezählt, würde eine fehlerhafte Anfrage, die mit 400 beantwortet wird, oder ein mit 401 abgelehnter Punktezähler für immer einen Platz belegen. process_response läuft später und sieht die Antwort, die die Bibliothek gleich senden wird. Die Platzprüfung zählt nur eine 101, verwandelt eine 101 für eine Verbindung zu viel in 503 Service Unavailable und lässt jede andere Antwort durch. Das vorgegebene handle_connection gibt den Platz in seinem finally zurück. Auf einem lokalen Computer, mit Platz für zwei, bekamen drei Zuschauer 101, 101 und 503, während eine 401 und eine 400 keinen Platz belegten. Eine Lücke bleibt offen: Ein Client, der in seinem eigenen verschwindet, nachdem seine 101 gezählt wurde, hält seinen Platz, bis der Server stoppt, was ein begrenzter Server ohnehin tut.

--max-clients ist die Zahl der Verbindungen, die gleichzeitig offen sein dürfen, keine Gesamtzahl: Jeder score-Befehl öffnet eine Verbindung, also würde eine Gesamtzahl ein Quiz nach einer Handvoll Wertungen beenden. Die Verbindung eines Punktezählers belegt ebenfalls einen Platz, also lassen fünf Zuschauer keinen Platz für eine Wertung.

open_server nimmt jetzt port und max_clients und übergibt die Platzprüfung als process_response an serve.

Fünf Verbindungen sind offen, und --max-clients ist 5. Ein sechster Zuschauer verbindet sich. Was bekommt er?

Zwei Arten zu stoppen

Ein Lauf endet an der ersten von zwei Grenzen: dem letzten Update oder der Frist. serve_scoreboard wartet mit einem einzigen Aufruf auf beide, board["finished"].wait(timeout=deadline). Wie in Kapitel 8 gibt Event.wait True zurück, wenn das Event gesetzt wurde, und False, wenn zuerst das Timeout abgelaufen ist. Das Warten ist also auch die Uhr: serve_scoreboard liest selbst keine Zeit.

True bedeutet, dass jedem Zuschauer der letzte Snapshot gesendet und er mit 1000 geschlossen wurde, also schließt server.shutdown(code=1000, reason="the quiz is over"), was übrig ist, zum Beispiel einen Punktezähler. False bedeutet, dass zuerst die Frist abgelaufen ist: server.shutdown(code=1001, reason="the scoreboard is closing"), wobei 1001 “going away” bedeutet, also dass die Gegenseite weggeht. Die Signatur der Bibliothek ist shutdown(close_connections=True, code=1001, reason=""), und shutdown() ist , also tut der eigene Aufruf des with-Blocks beim Verlassen nichts weiter.

shutdown() wartet außerdem, bis jeder Handler-Thread zurückgekehrt ist. Was den Ausstieg begrenzt, ist also, dass jeder Handler seine geschlossene Verbindung bei seiner nächsten Leerlaufprüfung bemerkt, innerhalb einer halben Sekunde, nicht das Timeout, das du join gibst. Gib join trotzdem ein Timeout: Warte nie ohne Grenze.

Eine ehrliche . Ein Zuschauer, dessen Gegenseite aufgehört hat zu lesen, kann in send feststecken, sobald die Puffer voll sind (Kapitel 8, Lektion 1). Er sendet den letzten Snapshot nie und verlässt das Register nie, also bleibt finished gelöscht, und die Frist schließt alle mit 1001, obwohl das Quiz fertig ist.

board["finished"].wait(timeout=8.0) gibt False zurück. Was tut dein Server als Nächstes?

Zuerst die Adresse nennen

Mit --port 0 wählt das System einen freien , also kennt ihn niemand, bis der Server ihn hat. serve_scoreboard liest ihn aus server.socket.getsockname()[1] und gibt listening on 127.0.0.1:PORT mit dem echten Port als erste Zeile aus, bevor es wartet. Gib sie mit flush=True aus: Wenn die Standardausgabe eine Pipe statt eines ist, hält Python ausgegebenen Text in einem Puffer, und flush=True sendet die Zeile sofort. Der Standardport, 8765, ist für den Lauf aus Lektion 5 auf deinem eigenen Computer.

Der Reihe nach öffnet serve_scoreboard den Server mit open_server(board, token, port=port, max_clients=max_clients, serve=serve) in einem with-Block, gibt die Adresse aus, startet einen threading.Thread, dessen Ziel server.serve_forever ist, wartet auf finished, fährt mit 1000 oder 1001 herunter, wartet mit einem Timeout wie WAIT auf das Ende des Threads und gibt zurück, was das Warten zurückgegeben hat.

Eine schlechte Kommandozeile ablehnen, bevor irgendetwas gebunden wird

main(argv=None, *, serve=serve) parst argv mit server_parser() und prüft jeden , bevor es irgendetwas öffnet: --port von 0 bis 65535, --max-clients von 1 bis MOST_CLIENTS, --max-updates von 1 bis MOST_UPDATES und --deadline über 0 und höchstens LONGEST_DEADLINE Sekunden. Für einen Wert außerhalb seines Bereichs gibt parser.error(message) die Verwendung und deine Meldung in die Standardfehlerausgabe aus und beendet das Programm mit dem Status 2, und genau das bedeutet eine abgelehnte Kommandozeile in jedem Programm dieses Kurses.

Eine unlesbare oder leere Token-Datei ist eine andere Art von Fehlschlag: Die Kommandozeile war in Ordnung, aber das Programm kann seine Aufgabe nicht erfüllen. Lies sie mit read_token; bei einem OSError oder einem leeren Token gib eine Zeile in die Standardfehlerausgabe aus und gib 1 zurück, bevor irgendetwas bereitgestellt wird. Baue dann die Tafel mit make_board(TEAMS, max_updates=...), rufe serve_scoreboard auf, gib eine abschließende Zeile aus und gib 0 zurück. Beide Enden geben 0 zurück: Die Frist ist einer der beiden Wege, auf denen ein Lauf enden soll.

Jemand führt python scoreboard_server.py --deadline 0 aus. Was passiert?

Deine Aufgabe

Schreibe serve_scoreboard und main. Führe dann das Szenario aus, das dein main zweimal aufruft:

python pl_ws.py server-main

Eine fertige Version hat auf der Seite das hier ausgegeben, wobei PORT für die Portnummer steht, die das System gewählt hat:

scenario server-main: your scoreboard_server.main() runs a quiz of two updates, then a quiz that stops at its deadline
your main() is called with --port 0 --max-updates 2 --deadline 4
viewer A joined; its first message was a snapshot with version 0
viewer B joined; its first message was a snapshot with version 0
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
viewer A received versions [0, 1, 2], then the server closed it with 1000 'the quiz is over'
viewer B received versions [0, 1, 2], then the server closed it with 1000 'the quiz is over'
your server | listening on 127.0.0.1:PORT
your server | the quiz is over at version 2; every viewer was closed with 1000
your main() returned 0
your main() is called with --port 0 --max-updates 2 --deadline 1
viewer A joined; its first message was a snapshot with version 0
viewer B joined; its first message was a snapshot with version 0
the scorekeeper sent 4 points for Herons; its reply was a snapshot with version 1
viewer A received versions [0, 1], then the server closed it with 1001 'the scoreboard is closing'
viewer B received versions [0, 1], then the server closed it with 1001 'the scoreboard is closing'
your server | listening on 127.0.0.1:PORT
your server | the deadline passed at version 1; every connection was closed with 1001
your main() returned 0

Die mit your server markierten Zeilen sind das, was dein Programm ausgegeben hat, gesammelt und nach jedem Lauf gezeigt, damit sie sich nicht mit den Zeilen der Zuschauer vermischen; deine abschließende Zeile kann es anders sagen. Probiere dann eine abgelehnte Kommandozeile aus und frag die Shell nach dem Exit-Status:

python scoreboard_server.py --max-clients 9
echo $?

Es gibt die Verwendung und deine Meldung aus, und echo $? gibt 2 aus. Der Server ist jetzt ein Programm, aber auf der anderen Seite ist noch nichts eins: Lektion 3 schreibt den Client.

Aufgabe

Schreibe in scoreboard_server.py zwei . Lass jede andere Funktion, wie sie ist.

  1. serve_scoreboard(board, token, *, port=0, max_clients=MOST_CLIENTS, deadline=DEADLINE, serve=serve): Gib in einem with-Block auf open_server(board, token, port=port, max_clients=max_clients, serve=serve) als erste Zeile listening on 127.0.0.1:PORT (den echten , aus server.socket.getsockname()[1]) mit flush=True aus, bevor irgendetwas wartet; starte einen threading.Thread, dessen Ziel server.serve_forever ausführt; rufe board["finished"].wait(timeout=deadline) auf; gibt es True zurück, rufe server.shutdown(code=1000, reason=...) auf, andernfalls server.shutdown(code=1001, reason=...); warte mit einem Timeout wie WAIT auf das Ende des Threads; gib zurück, was das Warten zurückgegeben hat.

  2. main(argv=None, *, serve=serve): Parse argv mit server_parser(); lehne --port außerhalb von 0 bis 65535, --max-clients außerhalb von 1 bis MOST_CLIENTS, --max-updates außerhalb von 1 bis MOST_UPDATES und eine --deadline von 0 oder weniger oder über LONGEST_DEADLINE ab, jeweils mit parser.error(...) (Exit-Status 2), bevor irgendetwas bereitgestellt wird; lies das Token mit read_token(args.token_file) und gib bei einem OSError oder einem leeren Token eine Zeile in die Standardfehlerausgabe aus und gib 1 zurück; baue die Tafel mit make_board(TEAMS, max_updates=args.max_updates); rufe serve_scoreboard(board, token, port=..., max_clients=..., deadline=..., serve=serve) auf; gib eine abschließende Zeile aus; gib 0 zurück.

Führe python pl_ws.py server-main und python scoreboard_server.py --max-clients 9 aus, dann echo $?. Klicke dann auf Check my work. Die Prüfung führt die Prüfungen aus Lektion 1 erneut aus. Sie ruft serve_scoreboard mit einem Stellvertreter-serve auf, dessen Server serve_forever, shutdown und seinen aufzeichnet, und mit scoreboard_server.threading ersetzt durch Stellvertreter, deren Thread aufzeichnet, was er bekommen hat, und nichts ausführt: Der Server muss 127.0.0.1 und den übergebenen Port binden, mit den - und beiden Hooks; die erste Zeile muss listening on 127.0.0.1: und der Port des Stellvertreters sein, ausgegeben und geflusht vor dem Warten; genau ein Thread muss gestartet werden, dessen Ziel serve_forever erreicht, wenn die Prüfung es danach aufruft; das Warten muss die Frist bekommen (einen Wert, den du nicht gesehen hast); das erste Herunterfahren muss den Code 1000 haben, wenn finished gesetzt ist, und 1001, wenn nicht, wobei True oder False zurückgegeben wird; und join muss ein Timeout bekommen. Sie ruft main auf, mit serve_scoreboard ersetzt durch einen Rekorder und mit einer eigenen Token-Datei: Gute Optionen müssen serve_scoreboard mit dem von Leerzeichen befreiten Token erreichen; sechs Optionen außerhalb ihres Bereichs müssen mit dem Status 2 enden, bevor irgendetwas bereitgestellt wird; eine fehlende und eine leere Token-Datei müssen 1 zurückgeben, mit einer Zeile in der Standardfehlerausgabe. Sie prüft auch die vorgegebene Platzprüfung. Jede abschließende Zeile ist in Ordnung, und ebenso eine ausgegebene Zeile mit return 2 statt parser.error.