Framing
Envelopes With the Page Count on the Front
TCP delivers a river of bytes with no gaps. If you mail two letters back to back, they can arrive glued together, or one letter can arrive in pieces. How does the reader know where one message ends?
Put the page count on the front of the envelope. Before every message we write its length in 4 bytes. The reader reads those 4 bytes first, then reads exactly that many bytes, and knows precisely where the message stops. This is framing, and it is the base of every message protocol built on top of PyNet.
The Bytes
send_frame(b"hi") send_frame(b"")
+-------------+-------+ +-------------+
| 00 00 00 02 | h i | | 00 00 00 00 |
+-------------+-------+ +-------------+
4-byte size payload size 0, no payload
(big-endian)
On the wire, back to back:
| 00 00 00 02 | h i | 00 00 00 00 | 00 00 00 03 | a b c |
\_____ frame 1 _____/ \__ frame 2 _/ \_____ frame 3 _____/
Using It
from pynet.framing import FramedConnection
from pynet.loop import Event, run
from pynet.streams import open_connection, start_server
async def main() -> None:
done = Event()
async def handler(reader, writer):
conn = FramedConnection(reader, writer)
print("got", await conn.recv_frame())
conn.close()
done.set()
server = await start_server(handler, "127.0.0.1", 0)
reader, writer = await open_connection("127.0.0.1", server.port)
conn = FramedConnection(reader, writer)
await conn.send_frame(b"hello")
await done.wait()
conn.close()
await conn.wait_closed()
server.close()
await server.wait_closed()
run(main())
Rules
- Too big? A header claiming more than
max_frame_size(16 MiB by default) is refused before the body is read, the connection is aborted, andFrameTooLargeError(with.sizeand.limit) is raised. Otherwise a liar could make us buffer gigabytes. - Goodbye vs. cut off. The peer closing between frames raises
ConnectionClosedError("peer closed"). Closing halfway through a frame raisesConnectionClosedErrormentioningmid-frame. Callingrecv_frame()again keeps raising it. - Safe to time out.
wait_for(conn.recv_frame(), 1.0)may give up after the 4-byte size has arrived but before the body has. The connection keeps that size like a bookmark, so the nextrecv_frame()reads the rest of that frame -- it never mistakes body bytes for a new size and falls out of step.
[00 00 00 05] [h e] ... timeout! bookmark: "5 bytes, body pending"
[l l o] [00 00 00 03] [a b c]
next recv_frame() -> b"hello" then -> b"abc"
- One sender at a time.
send_frameis not safe for several tasks at once; share a connection by guarding sends with aLock.
When to Use
Any time you send discrete messages over TCP: chat, RPC, the PyNet
message protocol. (Line-based text protocols can use read_until
instead.)
What We Test
encode_frame(b"hi")is00 00 00 02thenhi.- Many frames in one write come out as separate frames.
- A frame dribbled across several writes is reassembled.
- Empty frames work.
- Oversize payloads are refused when encoding and sending.
- An oversize header closes the connection and never reads the body.
- Clean EOF and EOF mid-frame (or mid-header) are told apart.
- A
recv_frame()that times out between the header and the body is finished by the next call, and the frame after it still reads correctly.
Next Up
Messages built on frames come next: see
Messaging for the envelope. Meanwhile, revisit
Streams to see how read_exactly works.