0%

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 , in one Python program on .

What to build

Three , the first two in 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 with make_reader(conn) and read the request head with read_head. If reading or parsing raises , because the head is too long or malformed, send refuse(400) and return None. If the client closes before the head ends, read_head raises ConnectionError; send nothing and return None. Otherwise send the response check_upgrade returns. Return the reader after a 101, and None after a refusal.

  • client_handshake is the client’s half, for a that is already connected. Make a key with make_client_key(random_bytes), send the request with sendall, read the reply head with read_head through a reader of its own, and check it with check_accept_response, which raises ValueError for anything but the right 101. Return the reader.

  • run_loopback() puts them on a real connection: a listener on 127.0.0.1 0, a thread that accepts one client and runs serve_handshake, a client in the main thread that connects and runs client_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 with Python: a caller must name it, and run_loopback leaves it out.

Bytes after the head

Once the server has sent its 101, it is free to send at once. TCP does not keep the head and those frames apart: one 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.1 and 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 calls accept(), so a client that never connects cannot hold the server thread. WAIT is the 2-second constant at the top of handshake_demo.py.

  • Give the accepted socket its own timeout as well, before serve_handshake reads from it. As you saw in Networks with Python, the socket accept() returns starts with no timeout, whatever the listener’s.

  • Connect with socket.create_connection(("127.0.0.1", port), timeout=WAIT), so every recv in client_handshake is 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 along with the listener, and let it store there what it saw, such as whether 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 names 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 , and neither can say anything yet: they need a way to send a message, and the next chapter builds it, one frame at a time.

Task

Write three , in this order. Submit names the first one that still raises NotImplementedError, so you meet one at a time.

  1. serve_handshake(conn) in ws_handshake.py: make a reader with make_reader(conn), read the head with read_head, and parse it with parse_upgrade_request. On , send refuse(400) with conn.sendall and return None. On ConnectionError, send nothing and return None. Otherwise send what check_upgrade returns, and return the reader if it was a 101, None if it was a refusal.

  2. client_handshake(sock, host, path, *, random_bytes=os.urandom) in ws_handshake.py: make a key with make_client_key(random_bytes), send build_upgrade_request(host, path, key) with sock.sendall, read the reply head with read_head through make_reader(sock), check it with check_accept_response, and return the reader.

  3. run_loopback() in handshake_demo.py, with a server function of your own beside it: bind 127.0.0.1 0 (with socket.create_server or with bind and listen), give the listener settimeout(WAIT), start a threading.Thread whose function accepts one client with conn, address = listener.accept(), calls conn.settimeout(WAIT) and runs serve_handshake(conn); connect from the main thread with socket.create_connection(..., timeout=WAIT), run client_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 , never a real one: requests and replies delivered whole, one byte at a time and two bytes at a time, each followed by the start of a that must stay in the reader you return, and then no more bytes and no close, so a half that reads until the connection closes waits forever and fails. It sends a request with version 8, a head with a line that has no colon, a head that never ends, and a client that closes partway through its head. It runs your client against your own 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.