WebSocket Frames — Complete Guide
In this tutorial, you will learn about WebSocket Frames. We cover key concepts, practical examples, and best practices to help you master this topic.
WebSocket frames are the basic unit of data transmission. Each frame contains control information and payload data. Understanding frames is essential for implementing WebSocket protocol handling and debugging low-level issues.
What You'll Learn
- WebSocket frame structure
- Opcodes and their meanings
- Masking for client-to-server frames
- Fragmentation of large messages
- Control frames (ping, pong, close)
Why It Matters
Frame-level understanding helps debug protocol issues, optimize message sizes, and implement custom WebSocket handlers when libraries are insufficient.
Real-World Use
A real-time gaming platform optimizes WebSocket frame sizes by using binary frames with protocol buffers instead of text JSON frames, reducing per-message overhead from 200 bytes to 20 bytes.
Flow Chart
flowchart LR
A[Frame] --> B[Header]
A --> C[Payload]
B --> D[FIN: Final Frame?]
B --> E[RSV: Reserved Bits]
B --> F[Opcode: Type]
B --> G[Mask: Client?]
B --> H[Length]
H --> I[7-bit / 16-bit / 64-bit]
F --> J[Text / Binary / Close / Ping / Pong]
Code Examples
Example 1: Parsing WebSocket Frames in Node.js
function parseFrame(buffer) {
const firstByte = buffer[0];
const fin = (firstByte & 0x80) !== 0;
const opcode = firstByte & 0x0F;
const secondByte = buffer[1];
const masked = (secondByte & 0x80) !== 0;
let payloadLength = secondByte & 0x7F;
let offset = 2;
if (payloadLength === 126) {
payloadLength = buffer.readUInt16BE(offset);
offset += 2;
} else if (payloadLength === 127) {
payloadLength = buffer.readBigUInt64BE(offset);
offset += 8;
}
let maskKey = null;
if (masked) {
maskKey = buffer.slice(offset, offset + 4);
offset += 4;
}
let payload = buffer.slice(offset, offset + payloadLength);
if (masked) {
for (let i = 0; i < payload.length; i++) {
payload[i] ^= maskKey[i % 4];
}
}
return {
fin,
opcode,
masked,
payloadLength: Number(payloadLength),
payload,
isControl: opcode >= 0x08,
};
}
// Usage
const buffer = Buffer.from([0x81, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F]);
const frame = parseFrame(buffer);
console.log('Unmasked payload:', frame.payload.toString());
// Output: Unmasked payload: Hello
Expected output: Parsed frame showing text opcode, unmasked payload containing "Hello".
Example 2: Building WebSocket Frames
function buildFrame(data, opcode = 0x01) {
const payload = Buffer.from(data);
const length = payload.length;
let header;
if (length < 126) {
header = Buffer.alloc(2);
header[1] = length;
} else if (length < 65536) {
header = Buffer.alloc(4);
header[1] = 126;
header.writeUInt16BE(length, 2);
} else {
header = Buffer.alloc(10);
header[1] = 127;
header.writeBigUInt64BE(BigInt(length), 2);
}
header[0] = 0x80 | opcode; // FIN + opcode
return Buffer.concat([header, payload]);
}
const frame = buildFrame('Hello Server');
console.log('Frame bytes:', frame.toString('hex'));
// Output: Frame bytes: 810548656c6c6f20536572766572
Expected output: A properly constructed WebSocket frame with FIN bit set and text opcode.
Example 3: Handling Control Frames
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
server.on('connection', (ws) => {
console.log('Client connected');
// Send ping every 30 seconds
const pingInterval = setInterval(() => {
ws.ping();
}, 30000);
ws.on('pong', () => {
console.log('Received pong from client');
});
ws.on('close', (code, reason) => {
clearInterval(pingInterval);
console.log(`Client disconnected: ${code} - ${reason}`);
});
});
// Client with ping/pong handling
const client = new WebSocket('ws://localhost:8080');
client.on('open', () => {
console.log('Connected');
});
client.on('ping', () => {
console.log('Received ping, sending pong');
client.pong();
});
Expected output: Server sends periodic pings, client responds with pongs, ensuring the connection remains alive.
Common Mistakes
| Mistake | Explanation |
|---|---|
| Forgetting to mask client frames | All client-to-server frames must be masked per the WebSocket specification |
| Ignoring fragmentation | Large messages are fragmented; always reassemble fragments using the FIN bit |
| Confusing opcodes | Use correct opcode: 0x1 for text, 0x2 for binary, 0x8 for close, 0x9 for ping, 0xA for pong |
| Not handling control frames in fragments | Control frames can appear between fragments; Process them without breaking reassembly |
| Misreading extended length | Payload length encoding varies: 7-bit, 16-bit, or 64-bit depending on the first length byte |
Practice Questions
- What is the structure of a WebSocket frame?
- Why are client-to-server frames masked?
- How does WebSocket handle messages larger than the maximum frame size?
- What opcodes are used for control frames?
- How do you send binary data vs text data over WebSocket?
Challenge
Implement a WebSocket frame analyzer that captures raw frames, decodes each field, and displays the frame structure in a human-readable format. Include support for fragmented messages and control frame handling.
FAQ
Mini Project
Build a WebSocket frame visualizer that captures WebSocket traffic, decodes each frame, and displays a real-time timeline showing frame types, sizes, and payload contents. Include the ability to reconstruct fragmented messages.
What's Next
Learn the difference between ws:// and wss://
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro