0%

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 , like the echo handler of Chapter 6. It reads at most MAX_SCORE_MESSAGES (10) messages, each with connection.recv(timeout=WAIT), and for each one:

  1. parse it with scoreboard_messages.parse_message;

  2. check that its type is "score": a snapshot or an error is a valid envelope, but not something a scorekeeper may send;

  3. apply it with apply_score(board, message), which broadcasts it to every viewer and returns the new snapshot;

  4. 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:

  • recv raises TimeoutError: 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 at the same time. The site gives you one , so this course supplies 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 , on a the system chooses, plays the viewers and the scorekeeper a scenario names, prints what each of them saw, and shuts the server down at the end. Its output is evidence for you to read; Check my work never reads it.

Run it with no scenario to them; the Chapter 7 one is quiz-round:

python pl_ws.py quiz-round

Viewers A, B and join in turn, the scorekeeper sends two scores, B leaves, and the scorekeeper sends one more. A finished version, run on the site, printed:

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_MESSAGES messages, each with connection.recv(timeout=WAIT);

  • for each, call scoreboard_messages.parse_message, check that its type is "score", call apply_score(board, message), and send the snapshot it returns, encoded with scoreboard_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 ConnectionClosed anywhere, return without raising.

Run python pl_ws.py quiz-round in the and read its transcript, then press Check my work. The check does not run 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 and an 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 over range(MAX_SCORE_MESSAGES) is as good as a counter, and the reason texts are yours.