The websockets Library, Without asyncio · practice
A Server in a Few Lines
interop.py pointed the library’s client at the Chapter 5 server. This lesson turns that around: the server becomes the library’s. In Chapter 5, serve_conversation read serve_handshake had answered the upgrade. With the library, the part that is about echoing fits in a handler of about six lines.
A handler and its connection
A handler is a
connection.recv(timeout=...)waits at most that many seconds for the next message and returns it: astrfor a text message,bytesfor a binary one. When nothing arrives in time, it raisesTimeoutError.connection.send(message)sends a message: astras text,bytesas binary.connection.close(code, reason)sends a close frame with that code and reason, and runs the closing handshake.connection.request.pathis the path the client asked for, such as/echo. Later chapters use it; this one does not.
The library does everything else. For each client that connects, it runs the handshake and then calls your handler on a thread of its own: the thread-per-connection pattern from Networks with Python Chapter 9. When the handler returns, the library closes the connection with 1000 if the handler has not already closed it.
Three clients connect at once. How many times does the library call echo_handler?
echo_handler(connection) is Chapter 5’s echo MAX_MESSAGES messages have been echoed, read one with connection.recv(timeout=WAIT) and send it straight back with connection.send. Sending the server_reply made with connection.close(1000, "message limit reached"). WAIT and MAX_MESSAGES are already at the top of echo_library.py: 2 seconds and 10 messages.
Serving
run_echo(talk, *, serve=serve) serves the handler for one conversation and hands the talk, a function that plays the client. It starts like this:
with serve(echo_handler, "127.0.0.1", 0) as server:
port = server.socket.getsockname()[1]
serve, from websockets.sync.server, binds and listens, like socket.create_server, and returns a server echo_handler itself, without calling it: the library calls it, once per connection. Port 0 lets the system pick a free port, and server.socket is the listening getsockname()[1] reads the port the way you did in Networks.
Nothing is served yet. server.serve_forever() accepts connections and starts a handler thread for each, and it returns only when the server is shut down. So it needs a thread of its own: create threading.Thread(target=server.serve_forever), passing the talk(port).
When the with block ends, for whatever reason, the server object calls shutdown(). That closes the listening socket, closes any connection still open with 1001, and makes serve_forever return. After the block, join the thread with timeout=WAIT, and return whatever talk returned.
The serve=serve is a seam, like connect= in Networks with Python: run_echo uses the library’s serve unless a caller passes another, and Submit passes a stand-in that records what your code does with it.
What makes serve_forever return?
A library conversation uses more threads than yours did. In a local count, one conversation with the library’s server and client in one process ran seven threads, and five with the library’s
What is gone
Put echo_handler next to Chapter 5’s server. The handshake is gone: no serve_handshake, no check_upgrade, no accept_value. Unmasking is gone, and so is the pong: the library answers every ping as it arrives, and recv only ever hands you messages. The closing handshake is gone too: when the client sends a close, the library answers it, the work your close_frame and wait_for_close did. What remains is the part about this program: how many messages, what to send back, when to close and with which code.
Which of these does your Chapter 5 server do that echo_handler no longer does?
Before you press Run
Run prints a block of about ten lines to standard error that looks like a crash. In this lesson it is expected, and here is why. The Chapter 5 client sends its three messages and then closes with 1000 and the reason done. Your handler is back in recv, waiting for a fourth message, and recv raises ConnectionClosedOK instead. The echo_handler, and the library logs it. The block starts with the line connection handler failed, continues with a
websockets.exceptions.ConnectionClosedOK: received 1000 (OK) done; then sent 1000 (OK) done
That last line says the connection still closed cleanly: the library received the client’s close and sent the same close back. Submit accepts a handler that lets this exception escape in this lesson, and Lesson 4 catches it, so the block goes away.
Now press Run. The Lesson 2 section runs the course’s Chapter 5 client, built by hand, against your library server. A finished version, run on the site, printed:
Lesson 2: your library server, the Chapter 5 client
{'echoes': ['hello', 'café ✓', 'third message'], 'code': 1000}
The hand-built client pinged and got its pong, sent three messages, got three echoes, and closed with 1000; the library answered every frame. The later sections still say “not written yet”. Then press Submit.
If the threads puzzle you, ask Monty:
Why does serve_forever need a thread of its own when the library already starts a thread for each handler?
The server speaks to a hand-built client. In the next lesson the client side moves to the library too.
Task
Write two echo_library.py, in this order. Submit names the first one that still raises NotImplementedError.
echo_handler(connection): while fewer thanMAX_MESSAGESmessages have been echoed, read one withconnection.recv(timeout=WAIT)and send it back unchanged withconnection.send. After the, call connection.close(1000, "message limit reached"). Notryyet: Lesson 4 adds it.run_echo(talk, *, serve=serve): inwith serve(echo_handler, "127.0.0.1", 0) as server:, read thewith server.socket.getsockname()[1], start athreading.Thread(target=server.serve_forever), and calltalk(port). After thewithblock, join the thread withtimeout=WAITand return whattalkreturned.
Press Run: the Lesson 2 section should print {'echoes': ['hello', 'café ✓', 'third message'], 'code': 1000}, and the connection handler failed block described in the lesson appears once on standard error.
Then press Submit. The check never opens a echo_handler stand-in connections with messages it has not shown you, text and binary: it expects each one sent back unchanged, in order, at most MAX_MESSAGES of them (it also tries a smaller MAX_MESSAGES), no read past the limit, a timeout of at most 2 seconds on every recv, and a close with 1000. A client that closes early may make your handler raise in this lesson. It gives run_echo a stand-in serve and watches the thread you start, without running it: serve called with echo_handler, "127.0.0.1" and 0, a thread whose target is serve_forever started before talk, talk called with the port, the server shut down after talk, also when talk raises, a join with a timeout after the shutdown, and talk’s import threading and write threading.Thread(...). Calling server.shutdown() yourself inside the block is fine, and so is a for loop over range(MAX_MESSAGES). Importing echo_library.py must print nothing.