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 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 recv raises ConnectionClosed, and except ConnectionClosed as exc: returns {"echoes": echoes, "close": describe_close(exc)}.
Putting 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, anintfrom 1 toMAX_CLIENTS(5), default 2;--messages, anintfrom 1 toMAX_MESSAGES(10), default 3;--deadline, afloatabove 0 and at most 8 seconds, default 5.0.
main(argv=None, *, serve=serve, connect=connect, now=time.monotonic) puts it together:
read the options with
parse_args(argv), and work out when the deadline falls:now()plus--deadline;serve
echo_handleron"127.0.0.1"0 in a withstatement, print the port, and startserve_foreveron a thread, as inrun_echo;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--messagestexts of your choosing and runecho_sessionwith thews://127.0.0.1:PORT/echoandconnect=connect;if
echo_sessionraisesOSError, print that the client failed and count it as failed, then go on to the next client;ConnectionRefusedErrorandTimeoutErrorare both kinds ofOSError;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; after the
withblock, join the thread withtimeout=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
Seams again
serve= and connect= are 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
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
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 echo_library.py, in this order. Submit names the first one that still raises NotImplementedError.
echo_session(url, texts, *, connect=connect): connect asecho_oncedoes, withopen_timeout=WAIT,close_timeout=WAITandproxy=None, in awithstatement. Inside onetry, send each text and read its echo withrecv(timeout=WAIT), then callconnection.close(1000, "done")andconnection.recv(timeout=WAIT)once more. Inexcept ConnectionClosed as exc:, return{"echoes": echoes, "close": describe_close(exc)}.parse_args(argv):--clients(int, 1 toMAX_CLIENTS, default 2),--messages(int, 1 toMAX_MESSAGES, default 3) and--deadline(float, above 0 and at most 8, default 5.0), withparser.errorfor aoutside its bounds. main(argv=None, *, serve=serve, connect=connect, now=time.monotonic): read the options; set the deadline fromnow(); serveecho_handleron"127.0.0.1"0 in a withstatement and startserve_foreveron athreading.Thread; run oneecho_sessionper client, one after another, with--messagestexts andconnect=connect, starting no client oncenow()has reached the deadline; count a client that raisesOSError, 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 withtimeout=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 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.