Frames and the Protocol
The handshake gets the connection open. From that point on, every byte of WebSocket traffic moves in small structured packets called frames. You will rarely manipulate a frame by hand — the browser’s WebSocket API and server libraries do it for you — but knowing what is happening under your ws.send('hello') call makes the rest of the course click into place.
What a frame is
Section titled “What a frame is”A frame is a compact unit with a tiny header followed by a payload. The header is where the protocol’s intelligence lives. It carries, among other things:
- An opcode that says what kind of frame this is.
- A mask bit and (for client frames) a masking key.
- A length field for the payload.
- A FIN bit indicating whether this frame completes a message.
Your single logical “message” becomes one or more frames on the wire.
Opcodes: the kinds of frame
Section titled “Opcodes: the kinds of frame”A handful of opcodes covers everything WebSockets do:
- Text — a UTF-8 string. This is what you get from
ws.send('hello'). - Binary — raw bytes, for things like images, audio chunks, or efficient encodings.
- Ping and Pong — the heartbeat pair. One side sends a ping; the other is obliged to answer with a pong. They let either end check that the connection is still alive without sending application data. (We will use these heavily when we reach the connection-lifecycle module.)
- Close — a polite “I am shutting this down”, optionally carrying a status code and reason. A clean close is both sides exchanging a close frame.
flowchart LR
msg["ws.send('hello')"] --> enc["encode as frame(s)"]
enc --> op{"opcode?"}
op -->|text| t["UTF-8 payload"]
op -->|binary| b["raw bytes"]
op -->|ping / pong| h["heartbeat"]
op -->|close| x["close + status code"]
t --> wire["bytes on the wire"]
b --> wire
h --> wire
x --> wire Masking: client to server
Section titled “Masking: client to server”There is one rule that surprises newcomers: every frame sent from a client to a server must be masked. The client XORs its payload with a random 4-byte key it includes in the frame; the server unmasks it on arrival. This is not encryption — anyone reading the bytes can reverse it — and wss:// is what actually protects the data. Masking exists for a narrower reason: to stop a malicious page from crafting WebSocket payloads that confuse old, WebSocket-unaware HTTP proxies sitting between client and server. Server-to-client frames are not masked.
Fragmentation
Section titled “Fragmentation”A large message does not have to be sent as one giant frame. It can be fragmented across several frames: the first frame carries the opcode with its FIN bit unset, the middle frames continue the payload, and the final frame sets FIN to mark the end. The receiver reassembles them into one message. This lets a sender begin streaming a message before it knows the full length — handy for large or generated data.
ws:// versus wss://
Section titled “ws:// versus wss://”Two URL schemes, exactly mirroring the web:
ws://— an unencrypted WebSocket connection. Fine for local development; unsafe across the public internet because anyone in between can read and tamper with the frames.wss://— WebSocket over TLS, the encrypted form. It is the WebSocket equivalent ofhttps://and the default you should reach for in production. Every live demo in this course connects withwss://.
You rarely touch any of this
Section titled “You rarely touch any of this”Here is the reassuring part: the API handles frames, masking, and reassembly for you. When you write ws.send('hello, server'), the library encodes a masked text frame; when a message arrives, it hands you a clean reassembled string or blob in onmessage. You design messages; the protocol moves frames. Knowing the frame layer simply means you will understand exactly what is happening when we later tune heartbeats, handle binary data, and debug closes.