0%

The websockets Library, Without asyncio · capstone

Capstone project: Project: The Echo Service, Rebuilt

You have a handler, a client and a way to read how a conversation ended, each written and checked on its own. What you do not have yet is a program a person could run. In Chapter 5 that program was loopback_demo.py with its : a bounded server, clients one after another, a line per client, and an exit status. This project builds the same program on the library, in echo_library.py, with three more .

What to build

echo_session(url, texts, *, connect=connect) is echo_once that also reports how the conversation ended. Connect with the same three bounds and proxy=None. Inside the with block, put everything in one try: send each text and read its echo, as before; then close with connection.close(1000, "done") and call connection.recv(timeout=WAIT) once more. Once the closing is over, that recv raises ConnectionClosed, and except ConnectionClosed as exc: returns {"echoes": echoes, "close": describe_close(exc)}.

Putting the inside the try matters for the other ending. The server you wrote closes first when it reaches MAX_MESSAGES, and a server can vanish. Either way, the next recv or send raises ConnectionClosed, and the same except returns the echoes that did arrive with the close that ended them: 1000 and message limit reached for a server at its limit, 1006 for one that vanished.

parse_args(argv) reads the same three options as Chapter 5’s project, with the same bounds, and refuses anything outside them with parser.error, which stops the program with status 2:

  • --clients, an int from 1 to MAX_CLIENTS (5), default 2;

  • --messages, an int from 1 to MAX_MESSAGES (10), default 3;

  • --deadline, a float above 0 and at most 8 seconds, default 5.0.

main(argv=None, *, serve=serve, connect=connect, now=time.monotonic) puts it together:

  1. read the options with parse_args(argv), and work out when the deadline falls: now() plus --deadline;

  2. serve echo_handler on "127.0.0.1" 0 in a with statement, print the port, and start serve_forever on a thread, as in run_echo;

  3. for each client, numbered from 1: if now() has reached the deadline, print that the client was not started and count it as failed; otherwise give it --messages texts of your choosing and run echo_session with the ws://127.0.0.1:PORT/echo and connect=connect;

  4. if echo_session raises OSError, print that the client failed and count it as failed, then go on to the next client; ConnectionRefusedError and TimeoutError are both kinds of OSError;

  5. otherwise print one line for the client, such as how many echoes it got and the , and count it as a success only if it got every echo and a close with 1000;

  6. after the with block, join the thread with timeout=WAIT, and return 0 if every client succeeded, 1 if any did not.

The wording of your lines is up to you, and so is how you count: a of results, a counter of successes, or anything else that gives the right exit status.

Seams again

serve= and connect= are for the same reason connect= was in Networks with Python: so a caller can pass stand-ins. Submit passes a serve whose server records what you do with it and serves nothing, and a connect that hands out scripted connections: one that echoes, one whose server closes after one echo, one whose server answers your close with 1001, one that times out, and one that is refused. main has to pass its connect on to echo_session; without connect=connect in that call, echo_session would use its default, the library’s real connect.

now= is the injected clock from Python in Practice. Submit passes one that jumps past the deadline as soon as the first client has connected, and expects no second client to start.

The server’s shutdown needs no code of its own. Because the server is used in a with statement, leaving the block calls shutdown() on every path: after the last client, after a client that failed, and even if an your code did not catch were to leave the block.

echo_session raises TimeoutError for the second client. Does the server still shut down?

Run

Press Run. The last section of library_demo.py calls your main with --clients 2 --messages 2 and prints what it returns. A finished version, run on the site, printed this, where PORT stands for the port the system picked:

Lesson 6: main with two clients
listening on 127.0.0.1:PORT
client 1: 2 echoes, closed with 1000
client 2: 2 echoes, closed with 1000
0

Your lines will read the way you wrote them; the 0 is the exit status main returned, and it should be 0. Standard error should be empty, as in Lesson 4. On your own computer, with the library installed from your requirements.txt, python echo_library.py --clients 3 runs the same program from a , through the last two lines of the file.

Then press Submit. Before it checks the three new functions, it checks the earlier ones again: the handler, run_echo, echo_once and describe_close.

If something in main puzzles you, ask Monty:

Why does my main pass connect=connect to echo_session when echo_session already has connect as its default?

Still one at a time

Look at what main does with the library’s server: one client, then the next. The server would happily serve several clients at once, each on its own handler thread, but this program never asks it to. There is also nothing for clients to share: each one talks only to its own echo. Chapter 7 changes both. It serves several clients at the same time and gives them something in common, the scoreboard. A scoreboard that must stop cleanly while viewers are still connected needs more than this program’s way of stopping between clients, such as closing open connections with 1001 when its deadline comes; the capstone in Chapter 10 handles that.

Task

Write three in echo_library.py, in this order. Submit names the first one that still raises NotImplementedError.

  1. echo_session(url, texts, *, connect=connect): connect as echo_once does, with open_timeout=WAIT, close_timeout=WAIT and proxy=None, in a with statement. Inside one try, send each text and read its echo with recv(timeout=WAIT), then call connection.close(1000, "done") and connection.recv(timeout=WAIT) once more. In except ConnectionClosed as exc:, return {"echoes": echoes, "close": describe_close(exc)}.

  2. parse_args(argv): --clients (int, 1 to MAX_CLIENTS, default 2), --messages (int, 1 to MAX_MESSAGES, default 3) and --deadline (float, above 0 and at most 8, default 5.0), with parser.error for a outside its bounds.

  3. main(argv=None, *, serve=serve, connect=connect, now=time.monotonic): read the options; set the deadline from now(); serve echo_handler on "127.0.0.1" 0 in a with statement and start serve_forever on a threading.Thread; run one echo_session per client, one after another, with --messages texts and connect=connect, starting no client once now() has reached the deadline; count a client that raises OSError, gets fewer echoes than it sent, or gets a close other than 1000 as failed; print one line per client; after the block, join the thread with timeout=WAIT; return 0 if every client succeeded and 1 otherwise.

Press Run: the Lesson 6 section should end with 0, after one line per client.

Then press Submit. The check never opens a and starts no thread. It first checks echo_handler, run_echo, echo_once and describe_close again, as Lessons 2 to 4 did. It runs echo_session against stand-in connections: an echo server (it expects close(1000, "done") and {"echoes": [...], "close": {"code": 1000, "reason": "done"}}), a server that closes after one echo, and a server that vanishes (1006), with the same bounds on connect and recv as in Lesson 3. It runs parse_args on the defaults, the largest allowed values and values just past each bound. It runs main with a stand-in serve, a stand-in connect and a recording threading : serve(echo_handler, "127.0.0.1", 0), a thread on serve_forever started before the first client, one connection per client to the port the server reported, --messages texts each, the shutdown after the clients and a join with a timeout after the shutdown, and 0 for two good clients. It expects 1, with the server still shut down, for a client with one echo of two, a close answered with 1001, a refused connection and a timed-out recv; no second client once its clock passes the deadline; and status 2 for --clients 9. Your printed wording is not checked.