One Server, Many Clients · practice
Send a Snapshot on Join
The envelopes from Lesson 1 are ready, and no server sends them. This lesson writes the first part of that server: what happens when a viewer connects.
What is already in scoreboard_server.py
Open scoreboard_server.py and read its first three
make_board(teams) returns the board, a "version" (0), "scores" (every team at 0), "viewers" (an empty dict, which Lesson 3 fills) and "lock" (a threading.Lock, which Lesson 3 also puts to work). Every function that follows takes this board as an
make_handler(board) returns the handler the library calls. In Chapter 6 you passed serve a function that takes one argument, the connection. The scoreboard’s handler also needs the board, so make_handler defines a small function inside itself that takes the connection and calls handle_connection(connection, board), and returns that inner function as a board, so one serve call can give every connection the same board.
handle_connection(connection, board) routes by path. It takes the path from connection.request.path, which is the whole request target, query included, and cuts it down to the path part with urlsplit, as /watch goes to viewer_handler, /score goes to scorekeeper_handler (Lesson 6), and any other path is closed at once with 1008.
The library calls each handler on its own thread, one per connection, as you saw in Chapter 6. So several viewer_handler calls can run at the same time against one board. That matters as soon as the board changes, and the function you write today is not yet safe for it; Lesson 3 is where that changes.
The whole board, first
A viewer can join at any moment: before the first question, or halfway through the quiz. If the server only sent changes, a viewer that joined after three scores would show an empty board until the fourth score arrived, and even then it would only know one team’s change. So the first message every viewer receives is a snapshot of the whole board as it is right now. Everything after that is a newer snapshot.
A viewer joins after three scores. What is the first message it receives?
A copy of the scores
snapshot_message(board) builds that envelope: type snapshot, the board’s version, and its scores. The scores need one more step than you might expect. Putting board["scores"] itself into the snapshot gives two names for one dict, and every later change to the board would show through the snapshot. A copy keeps the points the snapshot was made with:
same saw the change; copy did not. In this chapter each snapshot is sent as soon as it is made, so the difference rarely shows. In Chapter 8 a snapshot will wait before it is sent, while the board keeps changing, and there the copy is what keeps it true.
Why does snapshot_message copy board["scores"]?
A quiet viewer is a normal viewer
After its snapshot, a viewer has nothing to say. It watches. The server still has to keep the handler running, because returning from the handler closes the connection, and it has to notice when the viewer leaves.
watch_until_closed(connection) does that with a connection.recv(timeout=WAIT). Three things can happen:
a message arrives: read it and drop it, because viewers do not send anything the scoreboard uses;
TimeoutError: nothing arrived for 2 seconds, which is exactly what a viewer does, so go round the loop again;ConnectionClosed: the viewer left, so return.
This is where a viewer differs from the echo handler. The echo handler closed a client that said nothing for 2 seconds, because an echo client that is silent is not waiting for anything. A viewer that is silent is waiting for the next score. What ends a viewer is its own close, or the server shutting down, which closes every open connection with 1001. The timeout is still there so that each wait has an end and the loop keeps control.
recv(timeout=WAIT) raises TimeoutError in a viewer's handler. What should the handler do?
viewer_handler(connection, board) is then two lines: send scoreboard_messages.encode_message(snapshot_message(board)), and call watch_until_closed(connection). Reach the other scoreboard_messages., as the supplied code does.
Run
Press Run. The Lesson 2 section hands viewer_handler a stand-in connection that is quiet once and then closes. A finished version, run on the site, printed:
Lesson 2: a viewer joins
viewer A was sent [{'type': 'snapshot', 'version': 7, 'scores': {'Owls': 4, 'Foxes': 0}}]
The Lesson 3 section now runs your handler too, and it does not finish cleanly:
Lesson 3: the registry
while A is connected, the registry holds: []
after A's handler returned, the registry holds: []
ConnectionClosedError: no close frame received or sent
The registry stays empty because nothing registers a viewer yet. The last line is a viewer whose connection was already gone when its snapshot was sent: send raised, and nothing caught it. Both are Lesson 3’s work. Press Submit.
The next lesson records every connected viewer, so that a score can reach them all, and makes sure each one leaves the record however its connection ends.
Task
In scoreboard_server.py, write three NotImplementedError.
snapshot_message(board): return{"type": "snapshot", "version": board["version"], "scores": board["scores"].copy()}. Change nothing on the board.watch_until_closed(connection): in awhile True, call connection.recv(timeout=WAIT)insidetry. Drop whatever it returns. OnTimeoutError, keep looping. OnConnectionClosed, return.viewer_handler(connection, board): sendscoreboard_messages.encode_message(snapshot_message(board)), then callwatch_until_closed(connection).
Press Run and read the Lesson 2 section, then press Submit. The check uses a board you have not seen (version 12, Owls 31 and Café Quiz 7) and a stand-in connection that is quiet, sends "hello from a viewer", is quiet again and then closes. It expects exactly one message, sent before the first recv: the snapshot. It expects recv to be called until the close, each time with a timeout above 0 and at most 2 seconds, nothing sent back for the viewer’s message, the handler to return without raising, and a later change to the board to leave the returned snapshot as it was. A connection that ends without a close dict(board["scores"]) is as good as .copy(), and you may build the snapshot inside viewer_handler if you prefer.