Asking to Switch Protocols · capstone
Capstone project: Project: One Handshake on Loopback
Your server side parses, decides and answers; your client side asks and checks. The two have never talked to each other. In this project they do, over a real
What to build
Three ws_handshake.py and the third in handshake_demo.py:
serve_handshake(conn)is the server’s half, for one connection that is already accepted. Make a reader withmake_reader(conn)and read the request head withread_head. If reading or parsing raises, because the head is too long or malformed, send refuse(400)and returnNone. If the client closes before the head ends,read_headraisesConnectionError; send nothing and returnNone. Otherwise send the responsecheck_upgradereturns. Return the reader after a 101, andNoneafter a refusal.client_handshakeis the client’s half, for athat is already connected. Make a key with make_client_key(random_bytes), send the request withsendall, read the reply head withread_headthrough a reader of its own, and check it withcheck_accept_response, which raisesValueErrorfor anything but the right 101. Return the reader.run_loopback()puts them on a real connection: a listener on127.0.0.10, a thread that accepts one client and runs serve_handshake, a client in the main thread that connects and runsclient_handshake, a line or two of output saying what each side saw, and a join of the thread.
client_handshake starts with the line ws_handshake.py already holds:
def client_handshake(sock, host, path, *, random_bytes=os.urandom):
...
The * makes random_bytes keyword-only, like connect= in your fetch from run_loopback leaves it out.
Bytes after the head
Once the server has sent its 101, it is free to send recv in the client can return the end of the head and the start of the first frame together. You met this in Networks with Python: whatever arrives after the end of one message is the start of the next, and it stays in the receive buffer. RFC 6455 asks a client to wait for the 101 before it sends a frame, but your server cannot tell a careful client from an eager one, so it treats whatever follows the request head the same way.
read_head already keeps those bytes in reader["buffer"]. That is why both halves return the reader instead of nothing. The frame reading you build in the next chapters starts from that buffer, exactly where the head ended.
The server's first recv returned the whole upgrade request followed by 11 bytes of a frame. What must serve_handshake do with those 11 bytes?
read_head also guards against a head that never ends: past MAX_HEAD, 8192 bytes, it raises ValueError, and your server answers 400.
The wiring
The wiring is the pattern you wrote in Networks with Python Chapter 9: a listener, a thread per connection, joins with timeouts. Here there is one connection, and the program plays both ends. The rules:
Bind
127.0.0.1and port 0, so the system picks a free port.listener.getsockname()[1]tells you which.Give the listener a timeout with
settimeout(WAIT)before the thread callsaccept(), so a client that never connects cannot hold the server thread.WAITis the 2-second constant at the top ofhandshake_demo.py.Give the accepted socket its own timeout as well, before
serve_handshakereads from it. As you saw in Networks with Python, the socketaccept()returns starts with no timeout, whatever the listener’s.Connect with
socket.create_connection(("127.0.0.1", port), timeout=WAIT), so everyrecvinclient_handshakeis bounded too.Join the server thread with a timeout.
The server function runs in its own thread, so it cannot return a result to run_loopback. Pass it a serve_handshake returned a reader. Starting and joining the thread look like this:
server = threading.Thread(target=accept_one, args=(listener, seen), daemon=True)
server.start()
...
server.join(timeout=WAIT)
accept_one is the server function, which accepts one client and calls serve_handshake; seen is the dictionary. daemon=True, a keyword you have not needed before, means a thread still running cannot keep the program alive after the main thread ends, a safety net under the timeouts.
Use with for the listener, the accepted connection and the client socket, so each closes on every path.
What Submit can and cannot see
Submit checks the two halves the way the earlier lessons did, without a real connection. It gives serve_handshake and client_handshake stand-in sockets that deliver the bytes whole, one byte at a time and two bytes at a time, followed by the start of a frame, and then go quiet without closing, as a live peer would. It sends requests that must be refused and a head that never ends. Then it passes your client’s request to your serve_handshake and hands the answer back, so your two halves talk to each other.
run_loopback itself is checked only for the rules above: the address and port, the two settimeout calls, the client’s timeout, and the join’s. That checks how the code is written, not what it does: a real connection is your evidence in Run, not the checker’s.
Run
Press Run. After the four lesson sections, run_loopback prints what each side saw. Here is the end of a run of the finished project:
client: the server answered 101 with the right accept value
client: 0 bytes are waiting after the head
server: switched to WebSocket: True
server thread still running: False
Your wording may differ, and that is fine. Zero bytes are waiting because your server sends nothing after its 101 yet. If the program stops with TimeoutError, one side is waiting for bytes the other never sent; the recv that waited.
If something in the wiring puzzles you, ask Monty:
Why does my server thread need its own settimeout on the socket accept() returned, when the listener already has one?
When the run ends with a switched connection and no thread left running, press Submit.
The connection is open, both sides agree it speaks
Task
Write three NotImplementedError, so you meet one at a time.
serve_handshake(conn)inws_handshake.py: make a reader withmake_reader(conn), read the head withread_head, and parse it withparse_upgrade_request. On, send refuse(400)withconn.sendalland returnNone. OnConnectionError, send nothing and returnNone. Otherwise send whatcheck_upgradereturns, and return the reader if it was a 101,Noneif it was a refusal.client_handshake(sock, host, path, *, random_bytes=os.urandom)inws_handshake.py: make a key withmake_client_key(random_bytes), sendbuild_upgrade_request(host, path, key)withsock.sendall, read the reply head withread_headthroughmake_reader(sock), check it withcheck_accept_response, and return the reader.run_loopback()inhandshake_demo.py, with a server function of your own beside it: bind127.0.0.10 (with socket.create_serveror withbindandlisten), give the listenersettimeout(WAIT), start athreading.Threadwhose function accepts one client withconn, address = listener.accept(), callsconn.settimeout(WAIT)and runsserve_handshake(conn); connect from the main thread withsocket.create_connection(..., timeout=WAIT), runclient_handshake, print what each side saw, and join the thread with a timeout.
Press Run: after the lesson sections, your two halves complete one
Then press Submit. The check gives both halves stand-in serve_handshake. For run_loopback it reads your code in handshake_demo.py and checks the rules only: the address ("127.0.0.1", 0), settimeout on the listener and on the socket accept() returns, a timeout on create_connection, a Thread, and a timeout on every join. Your printed lines are not checked. Importing either file must print nothing.