WebSocket Frame Format — Understanding Opcodes, Masking, and Payload Length
In this tutorial, you will learn about Websocket Frame Format. We cover key concepts, practical examples, and best practices to help you master this topic.
WebSocket frames are binary structures with a 2-byte minimum header containing FIN, opcode, mask, and payload length fields, followed by optional masking key and payload data.
What You'll Learn
- The structure of a WebSocket frame header
- How payload length is encoded (7-bit, 16-bit, 64-bit)
- How masking works in client-to-server frames
Why It Matters
To implement custom WebSocket clients, build debugging tools, or understand wire-level protocol behavior, you must understand the frame format. Many performance and compatibility issues trace back to incorrect frame construction.
def create_websocket_frame(payload, opcode=1, masked=False):
"""Create a WebSocket frame from payload data"""
frame = bytearray()
# FIN + opcode
fin_and_opcode = 0x80 | (opcode & 0x0F)
frame.append(fin_and_opcode)
# Mask + payload length
payload_len = len(payload)
if masked:
mask_bit = 0x80
else:
mask_bit = 0x00
if payload_len < 126:
frame.append(mask_bit | payload_len)
elif payload_len < 65536:
frame.append(mask_bit | 126)
frame.extend(payload_len.to_bytes(2, 'big'))
else:
frame.append(mask_bit | 127)
frame.extend(payload_len.to_bytes(8, 'big'))
# Masking key (if masked)
if masked:
import random
masking_key = bytes(random.randint(0, 255) for _ in range(4))
frame.extend(masking_key)
payload = bytes(payload[i] ^ masking_key[i % 4] for i in range(len(payload)))
frame.extend(payload)
return bytes(frame)
Frame Parser
def parse_frame(data):
"""Parse a WebSocket frame and return its components"""
if len(data) < 2:
raise ValueError("Frame too short")
first_byte = data[0]
second_byte = data[1]
frame = {
'fin': (first_byte >> 7) & 1,
'rsv1': (first_byte >> 6) & 1,
'rsv2': (first_byte >> 5) & 1,
'rsv3': (first_byte >> 4) & 1,
'opcode': first_byte & 0x0F,
'masked': (second_byte >> 7) & 1,
'payload_length': second_byte & 0x7F,
}
offset = 2
if frame['payload_length'] == 126:
frame['payload_length'] = int.from_bytes(data[2:4], 'big')
offset = 4
elif frame['payload_length'] == 127:
frame['payload_length'] = int.from_bytes(data[2:10], 'big')
offset = 10
if frame['masked']:
frame['masking_key'] = data[offset:offset + 4]
offset += 4
raw_payload = data[offset:offset + frame['payload_length']]
if frame['masked']:
frame['payload'] = bytes(
raw_payload[i] ^ frame['masking_key'][i % 4]
for i in range(len(raw_payload))
)
else:
frame['payload'] = raw_payload
return frame
Opcodes
OPCODES = {
0x0: 'continuation',
0x1: 'text',
0x2: 'binary',
0x8: 'close',
0x9: 'ping',
0xA: 'pong'
}
def handle_frame(frame):
"""Handle a WebSocket frame based on its opcode"""
opcode_name = OPCODES.get(frame['opcode'], f'unknown ({frame["opcode"]})')
if frame['opcode'] == 0x1: # Text
text = frame['payload'].decode('utf-8')
print(f"Text message: {text}")
elif frame['opcode'] == 0x2: # Binary
print(f"Binary message: {len(frame['payload'])} bytes")
elif frame['opcode'] == 0x8: # Close
if len(frame['payload']) >= 2:
close_code = int.from_bytes(frame['payload'][:2], 'big')
reason = frame['payload'][2:].decode('utf-8', errors='replace')
print(f"Close: code={close_code}, reason='{reason}'")
else:
print("Close: no code")
elif frame['opcode'] == 0x9: # Ping
print(f"Ping: {len(frame['payload'])} bytes")
elif frame['opcode'] == 0xA: # Pong
print(f"Pong: {len(frame['payload'])} bytes")
Common Mistakes
1. Forgetting to Mask Client Frames
Client frames must have the mask bit set and a 4-byte masking key. Unmasked client frames are rejected by the server.
2. Masking Server Frames
Server frames must NOT be masked. Masked server frames confuse clients.
3. Incorrect Payload Length Encoding
For payloads over 125 bytes, use 2-byte (126) or 8-byte (127) extended length. Using the wrong encoding breaks frame Parsing.
4. Not Setting the FIN Bit
Without FIN=1, the receiver waits for more fragments that never arrive. Always set FIN=1 for complete messages.
5. Sending Invalid UTF-8 in Text Frames
Text frames (opcode 1) must contain valid UTF-8. Invalid UTF-8 causes the receiver to close the connection.
Practice Questions
- What field indicates the last frame of a message?
- How is payload length encoded for messages over 125 bytes?
- What is the masking key used for?
- What opcode indicates a binary frame?
- How many bytes does the basic frame header use?
Answers
- FIN bit (1 = last frame). 2. 2 bytes for messages 126-65535, 8 bytes for larger. 3. XOR mask to prevent cache poisoning. 4. 0x2. 5. 2 bytes minimum.
Challenge
Build a WebSocket frame inspector that: accepts raw hex frame data, parses and displays all frame fields (FIN, opcode, mask, payload length, masking key, payload), validates the frame structure, and identifies errors.
FAQ
Mini Project
Build a WebSocket frame construction and parsing library that: creates frames for all opcodes with correct FIN, mask, and length encoding, parses frames and returns structured frame objects, validates frame correctness, and includes test cases for edge cases (empty payload, max length, fragmented messages).
What's Next
- Learn about WebSocket close codes and their meanings
- Explore WebSocket extensions like permessage-deflate
- Continue to WebSocket implementation with Express and ws library
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro