Message Framing
20 min
TCP delivers a raw stream of bytes with no built-in concept of "where
one message ends." WebSocket solves this with explicit frames: each
message is wrapped in a small header that says (among other things) how
long the payload is, so the receiver knows exactly where to stop
reading. The first header byte packs two things into one byte: the top
bit is FIN (is this the final frame of the message?), and the bottom 4
bits are the opcode (0x1 = text, 0x2 = binary, 0x8 = close).
def encode_frame(payload):
header = bytearray()
header.append(0x81) # FIN=1, opcode=1 (text) packed into one byte
length = len(payload)
if length < 126:
header.append(length) # small payloads: length fits directly in byte 1
return bytes(header) + payload
0x81 is 10000001 in binary -- the leading 1 is FIN, and 0001 is the text opcode. Real frames also need a masking bit (client-to-server frames MUST be masked with a random key per RFC 6455, server-to-client frames must NOT be) -- this simplified version only covers the unmasked server-to-client case.
Write `encode_frame(payload)`: build a minimal WebSocket text frame header for a small (< 126 byte), unmasked, single-frame payload — byte 0 is `0x81` (FIN=1, opcode=1 for text), byte 1 is the payload length. Return the header bytes concatenated with `payload`.
Unlike HTTP, WebSocket doesn't send messages as plain, self-delimited text. Why does it need explicit frame headers at all?
You can build a minimal WebSocket frame header and understand why explicit framing is necessary on top of TCP's raw byte stream.