One Server, Many Clients · capstone
Capstone project: Project: Scoreboard v1
Viewers can join, register and receive every change, and your tests prove it. What is missing is the connection that changes anything: the scorekeeper’s, on /score. handle_connection already sends it to scorekeeper_handler, the last stub in scoreboard_server.py. Writing it completes scoreboard v1, and then you will run the whole server with several clients at once.
The scorekeeper’s handler
scorekeeper_handler(connection, board) is a bounded MAX_SCORE_MESSAGES (10) messages, each with connection.recv(timeout=WAIT), and for each one:
parse it with
scoreboard_messages.parse_message;check that its
typeis"score": a snapshot or an error is a valid envelope, but not something a scorekeeper may send;apply it with
apply_score(board, message), which broadcasts it to every viewer and returns the new snapshot;answer the scorekeeper with that snapshot, encoded with
scoreboard_messages.encode_message, so it sees the board its score produced.
Three other endings, each closing the connection and returning:
recvraisesTimeoutError: no score came for 2 seconds; close with 1000 and a reason such as"no score arrived in time";any of steps 1 to 3 raises
(bytes, text that is not , a broken envelope, the wrong type, or a team that is not playing): close with 1008 and a reason such as "that was not a valid score", and apply nothing;the tenth message has been answered: close with 1000 and
"message limit reached".
Count the messages you read, not the scores you applied: a message that is refused ends the loop anyway, and a limit on reads is what keeps one connection from holding a thread forever. A while loop with a counter, as in the echo handler, or a for loop over range(MAX_SCORE_MESSAGES) both work. Close with 1000 after the loop ends, outside it.
For step 2, raising ValueError yourself when the type is wrong lets one except ValueError: handle every invalid message. And wrap the whole loop in a try whose except ConnectionClosed: returns: if the scorekeeper closes, or vanishes, at any point, the handler should end quietly, as the viewer’s does.
Chapter 9 will answer a mistake with an error envelope and keep the connection open. For now, a mistake ends the conversation. The reply goes last on purpose: the scorekeeper is told about version 1 only once every viewer has been sent it, which the run below lets you see.
The scorekeeper sends {"type": "score", "team": "Owls", "points": 11}. How does this chapter's server end the connection?
Meet pl_ws.py
Check my work first repeats the checks of Lessons 1 to 4, then tests your handler with stand-ins, as the earlier lessons did. To see the real thing, you need a server and several clients talking over real pl_ws.py, a program written for this course that plays every client. It is not a tool you will find elsewhere. It starts your server from make_handler on
Run it with no scenario to quiz-round:
python pl_ws.py quiz-round
Viewers A, B and
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
Read it from the top. Each viewer’s first message was a snapshot. The registry grew to three, and shrank to two once B left, through the finally block of Lesson 3. Every viewer’s versions only go up. At the end, the server’s shutdown closed the remaining viewers with 1001, which is how a viewer’s handler ends when nothing else ends it.
pl_ws.py reports order and counts, never times, because the threads of several clients do not finish in the same order on every machine. The order it does report is the interesting part: before each reply reached the scorekeeper, the server had already sent that version to all three viewers.
When pl_ws.py, the course's client simulator, runs scoreboard v1, each reply to the scorekeeper comes after every viewer's copy of the update. Why?
You can also run python scoreboard_demo.py, whose Lesson 6 section scores twice and then sends 11 points, and your tests from Lesson 5, which should still pass:
python -m pytest -q
When quiz-round runs to its end, press Check my work.
What the transcript hints at
Three viewers on loopback, all in one process on one machine, answer at once, so nobody notices the wait. But every reply waits for every viewer’s send, and while one send is in progress the board lock is held, so the next score and every new join wait too. One viewer on a slow connection would make the scorekeeper, and everyone else, wait for it. Chapter 8 measures that, and then gives each viewer its own sender.
Task
In scoreboard_server.py, write scorekeeper_handler(connection, board):
read at most
MAX_SCORE_MESSAGESmessages, each withconnection.recv(timeout=WAIT);for each, call
scoreboard_messages.parse_message, check that its type is"score", callapply_score(board, message), and send the snapshot it returns, encoded withscoreboard_messages.encode_message;on
TimeoutError, close with 1000 and a reason, and return;on
from parsing, the type check or apply_score, close with 1008 and a reason, and return;after the last allowed message, close with 1000 and a reason;
on
ConnectionClosedanywhere, return without raising.
Run python pl_ws.py quiz-round in the pl_ws.py or read your terminal. It first repeats the checks of Lessons 1 to 4, then runs your handler on stand-in connections, with two stand-in viewers registered. Two scores must be answered with versions 1 and 2, each reply sent after both viewers were sent that version, and nothing closed until the scorekeeper closes. After one valid score, each of 11 points, a team called Bats, "not json", a binary error envelope must get no reply, a close with 1008, no further read, and leave the board at version 1. A scorekeeper that sends nothing in time must be closed with 1000. Twelve waiting scores must get ten replies and a close with 1000, with the eleventh never read. A connection that ends, or a reply that finds it closed, must end the handler quietly. Every recv needs a timeout above 0 and at most 2 seconds. A for range(MAX_SCORE_MESSAGES) is as good as a counter, and the reason texts are yours.