0
# Low-Level Protocol
1
2
Direct frame protocol access for advanced use cases requiring fine-grained control over WebSocket frame generation and parsing. This module provides the underlying protocol implementation that powers the high-level WSConnection interface.
3
4
## Capabilities
5
6
### Frame Protocol
7
8
Core WebSocket frame protocol implementation handling frame parsing, generation, and message assembly.
9
10
```python { .api }
11
class FrameProtocol:
12
"""
13
Complete frame protocol implementation for WebSocket connections.
14
"""
15
16
def __init__(self, client: bool, extensions: List[Extension]) -> None:
17
"""
18
Initialize frame protocol.
19
20
Args:
21
client: True if this is a client connection, False for server
22
extensions: List of enabled extensions
23
"""
24
25
def receive_bytes(self, data: bytes) -> None:
26
"""
27
Feed received bytes into the protocol for parsing.
28
29
Args:
30
data: Raw bytes received from network
31
"""
32
33
def received_frames(self) -> Generator[Frame, None, None]:
34
"""
35
Generator yielding parsed frames from received data.
36
37
Yields:
38
Frame objects representing complete WebSocket frames
39
"""
40
41
def send_data(
42
self, payload: Union[bytes, bytearray, str] = b"", fin: bool = True
43
) -> bytes:
44
"""
45
Generate data frame bytes for transmission.
46
47
Args:
48
payload: Data to send (bytes for binary, str for text)
49
fin: Whether this completes the message
50
51
Returns:
52
Raw frame bytes to send over network
53
54
Raises:
55
ValueError: If payload type is invalid
56
TypeError: If data type changes within a fragmented message
57
"""
58
59
def ping(self, payload: bytes = b"") -> bytes:
60
"""
61
Generate ping frame bytes.
62
63
Args:
64
payload: Optional ping payload (max 125 bytes)
65
66
Returns:
67
Raw ping frame bytes to send
68
"""
69
70
def pong(self, payload: bytes = b"") -> bytes:
71
"""
72
Generate pong frame bytes.
73
74
Args:
75
payload: Optional pong payload (max 125 bytes)
76
77
Returns:
78
Raw pong frame bytes to send
79
"""
80
81
def close(self, code: Optional[int] = None, reason: Optional[str] = None) -> bytes:
82
"""
83
Generate close frame bytes.
84
85
Args:
86
code: Close code (see CloseReason enum)
87
reason: Optional close reason string
88
89
Returns:
90
Raw close frame bytes to send
91
92
Raises:
93
TypeError: If reason provided without code
94
"""
95
```
96
97
### Frame Data Structures
98
99
Core data structures representing WebSocket frames and their components.
100
101
```python { .api }
102
class Frame(NamedTuple):
103
"""
104
Complete frame information.
105
"""
106
opcode: Opcode # Frame opcode (TEXT, BINARY, CLOSE, etc.)
107
payload: Union[bytes, str, Tuple[int, str]] # Frame payload data
108
frame_finished: bool # Whether this frame is complete
109
message_finished: bool # Whether this completes the message
110
111
class Header(NamedTuple):
112
"""
113
Frame header information.
114
"""
115
fin: bool # FIN bit - whether frame completes message
116
rsv: RsvBits # Reserved bits (used by extensions)
117
opcode: Opcode # Frame opcode
118
payload_len: int # Payload length
119
masking_key: Optional[bytes] # Masking key (client frames only)
120
121
class RsvBits(NamedTuple):
122
"""
123
Reserved bits in frame header.
124
"""
125
rsv1: bool # RSV1 bit (used by extensions like permessage-deflate)
126
rsv2: bool # RSV2 bit (reserved for future use)
127
rsv3: bool # RSV3 bit (reserved for future use)
128
```
129
130
### Frame Opcodes
131
132
Enumeration of WebSocket frame opcodes as defined in RFC 6455.
133
134
```python { .api }
135
class Opcode(IntEnum):
136
"""
137
RFC 6455, Section 5.2 - Base Framing Protocol opcodes.
138
"""
139
140
CONTINUATION = 0x0 # Continuation frame for fragmented messages
141
TEXT = 0x1 # Text message frame
142
BINARY = 0x2 # Binary message frame
143
CLOSE = 0x8 # Connection close frame
144
PING = 0x9 # Ping control frame
145
PONG = 0xA # Pong control frame
146
147
def iscontrol(self) -> bool:
148
"""
149
Check if this opcode represents a control frame.
150
151
Returns:
152
True if this is a control frame (CLOSE, PING, PONG)
153
"""
154
```
155
156
### Close Reason Codes
157
158
Standard WebSocket close codes as defined in RFC 6455 Section 7.4.1.
159
160
```python { .api }
161
class CloseReason(IntEnum):
162
"""
163
RFC 6455, Section 7.4.1 - Defined Status Codes.
164
"""
165
166
# Standard close codes
167
NORMAL_CLOSURE = 1000 # Normal closure
168
GOING_AWAY = 1001 # Endpoint going away
169
PROTOCOL_ERROR = 1002 # Protocol error
170
UNSUPPORTED_DATA = 1003 # Unsupported data type
171
NO_STATUS_RCVD = 1005 # No status received (reserved)
172
ABNORMAL_CLOSURE = 1006 # Abnormal closure (reserved)
173
INVALID_FRAME_PAYLOAD_DATA = 1007 # Invalid frame payload
174
POLICY_VIOLATION = 1008 # Policy violation
175
MESSAGE_TOO_BIG = 1009 # Message too big
176
MANDATORY_EXT = 1010 # Mandatory extension missing
177
INTERNAL_ERROR = 1011 # Internal server error
178
SERVICE_RESTART = 1012 # Service restart (non-RFC)
179
TRY_AGAIN_LATER = 1013 # Try again later (non-RFC)
180
TLS_HANDSHAKE_FAILED = 1015 # TLS handshake failed (reserved)
181
```
182
183
### Frame Decoder
184
185
Low-level frame decoder for parsing raw WebSocket frame data.
186
187
```python { .api }
188
class FrameDecoder:
189
"""
190
Low-level WebSocket frame decoder.
191
"""
192
193
def __init__(
194
self, client: bool, extensions: Optional[List[Extension]] = None
195
) -> None:
196
"""
197
Initialize frame decoder.
198
199
Args:
200
client: True if decoding client frames, False for server frames
201
extensions: List of extensions for frame processing
202
"""
203
204
def receive_bytes(self, data: bytes) -> None:
205
"""
206
Feed bytes to the decoder.
207
208
Args:
209
data: Raw bytes from network
210
"""
211
212
def process_buffer(self) -> Optional[Frame]:
213
"""
214
Process buffered data and return a frame if complete.
215
216
Returns:
217
Complete Frame object or None if more data needed
218
219
Raises:
220
ParseFailed: If frame parsing fails
221
"""
222
```
223
224
### Protocol Exceptions
225
226
Exceptions raised during frame protocol operations.
227
228
```python { .api }
229
class ParseFailed(Exception):
230
"""
231
Exception raised when frame parsing fails.
232
"""
233
234
def __init__(
235
self, msg: str, code: CloseReason = CloseReason.PROTOCOL_ERROR
236
) -> None:
237
"""
238
Initialize parse failure exception.
239
240
Args:
241
msg: Error message
242
code: Associated close code for the error
243
"""
244
self.code: CloseReason # Close code associated with the error
245
```
246
247
### Message Decoder
248
249
Helper class for assembling fragmented WebSocket messages from individual frames.
250
251
```python { .api }
252
class MessageDecoder:
253
"""
254
Decoder for assembling WebSocket messages from frames.
255
256
Handles fragmented messages by buffering frame data and reconstructing
257
complete messages, including UTF-8 decoding for text messages.
258
"""
259
260
def __init__(self) -> None:
261
"""Initialize message decoder."""
262
263
def process_frame(self, frame: Frame) -> Frame:
264
"""
265
Process a frame and return assembled message data.
266
267
Handles continuation frames and message assembly, including
268
UTF-8 decoding for text messages.
269
270
Args:
271
frame: Frame to process
272
273
Returns:
274
Frame with assembled message data
275
276
Raises:
277
ParseFailed: If frame sequence is invalid or UTF-8 decoding fails
278
"""
279
```
280
281
### Constants
282
283
```python { .api }
284
import struct
285
# Payload length constants
286
PAYLOAD_LENGTH_TWO_BYTE = 126 # Indicator for 2-byte length
287
PAYLOAD_LENGTH_EIGHT_BYTE = 127 # Indicator for 8-byte length
288
MAX_PAYLOAD_NORMAL = 125 # Maximum single-byte payload length
289
MAX_PAYLOAD_TWO_BYTE = 65535 # Maximum 2-byte payload length (2^16 - 1)
290
MAX_PAYLOAD_EIGHT_BYTE = 2**64 - 1 # Maximum 8-byte payload length
291
MAX_FRAME_PAYLOAD = MAX_PAYLOAD_EIGHT_BYTE # Alias for maximum frame payload
292
293
# Frame header bit masks
294
FIN_MASK = 0x80 # FIN bit mask
295
RSV1_MASK = 0x40 # RSV1 bit mask
296
RSV2_MASK = 0x20 # RSV2 bit mask
297
RSV3_MASK = 0x10 # RSV3 bit mask
298
OPCODE_MASK = 0x0F # Opcode mask
299
MASK_MASK = 0x80 # Mask bit mask
300
PAYLOAD_LEN_MASK = 0x7F # Payload length mask
301
302
# WebSocket protocol version
303
WEBSOCKET_VERSION = b"13" # RFC 6455 WebSocket version
304
305
# Close code ranges
306
MIN_CLOSE_REASON = 1000 # Minimum valid close code
307
MIN_PROTOCOL_CLOSE_REASON = 1000 # Minimum protocol-defined close code
308
MAX_PROTOCOL_CLOSE_REASON = 2999 # Maximum protocol-defined close code
309
MIN_LIBRARY_CLOSE_REASON = 3000 # Minimum library-defined close code
310
MAX_LIBRARY_CLOSE_REASON = 3999 # Maximum library-defined close code
311
MIN_PRIVATE_CLOSE_REASON = 4000 # Minimum private close code
312
MAX_PRIVATE_CLOSE_REASON = 4999 # Maximum private close code
313
MAX_CLOSE_REASON = 4999 # Maximum valid close code
314
315
LOCAL_ONLY_CLOSE_REASONS = ( # Codes that must not appear on wire
316
CloseReason.NO_STATUS_RCVD,
317
CloseReason.ABNORMAL_CLOSURE,
318
CloseReason.TLS_HANDSHAKE_FAILED,
319
)
320
321
# WebSocket accept GUID
322
ACCEPT_GUID = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" # RFC 6455 magic string
323
324
# Null mask for server frames
325
NULL_MASK = struct.pack("!I", 0)
326
```
327
328
## Usage Examples
329
330
### Direct Frame Protocol Usage
331
332
```python
333
from wsproto.frame_protocol import FrameProtocol, Opcode, CloseReason
334
from wsproto.extensions import PerMessageDeflate
335
336
# Create frame protocol instance
337
extensions = [PerMessageDeflate()]
338
protocol = FrameProtocol(client=True, extensions=extensions)
339
340
# Send text data
341
text_frame_bytes = protocol.send_data("Hello, WebSocket!", fin=True)
342
print(f"Text frame: {len(text_frame_bytes)} bytes")
343
344
# Send binary data
345
binary_frame_bytes = protocol.send_data(b"Binary data", fin=True)
346
print(f"Binary frame: {len(binary_frame_bytes)} bytes")
347
348
# Send fragmented message
349
fragment1_bytes = protocol.send_data("Start of ", fin=False)
350
fragment2_bytes = protocol.send_data("fragmented message", fin=True)
351
352
# Send control frames
353
ping_bytes = protocol.ping(b"ping-payload")
354
pong_bytes = protocol.pong(b"pong-payload")
355
close_bytes = protocol.close(CloseReason.NORMAL_CLOSURE, "Goodbye")
356
```
357
358
### Frame Parsing
359
360
```python
361
from wsproto.frame_protocol import FrameProtocol, FrameDecoder, Opcode
362
363
# Create frame decoder
364
decoder = FrameDecoder(client=False, extensions=[])
365
366
# Process incoming frame data
367
decoder.receive_bytes(frame_data_chunk1)
368
decoder.receive_bytes(frame_data_chunk2)
369
370
# Try to parse complete frames
371
frame = decoder.process_buffer()
372
if frame:
373
print(f"Received frame: opcode={frame.opcode}")
374
print(f"Payload: {frame.payload}")
375
print(f"Frame finished: {frame.frame_finished}")
376
print(f"Message finished: {frame.message_finished}")
377
378
# Handle different frame types
379
if frame.opcode == Opcode.TEXT:
380
print(f"Text message: {frame.payload}")
381
elif frame.opcode == Opcode.BINARY:
382
print(f"Binary message: {len(frame.payload)} bytes")
383
elif frame.opcode == Opcode.CLOSE:
384
code, reason = frame.payload
385
print(f"Close frame: code={code}, reason='{reason}'")
386
elif frame.opcode == Opcode.PING:
387
print(f"Ping frame: payload={frame.payload}")
388
elif frame.opcode == Opcode.PONG:
389
print(f"Pong frame: payload={frame.payload}")
390
```
391
392
### Message Assembly
393
394
```python
395
from wsproto.frame_protocol import FrameProtocol, MessageDecoder, Opcode
396
397
# Assemble fragmented messages
398
message_decoder = MessageDecoder()
399
text_buffer = ""
400
binary_buffer = b""
401
402
protocol = FrameProtocol(client=False, extensions=[])
403
protocol.receive_bytes(fragmented_frame_data)
404
405
for frame in protocol.received_frames():
406
if frame.opcode in (Opcode.TEXT, Opcode.BINARY, Opcode.CONTINUATION):
407
# Process data frames through message decoder
408
message_frame = message_decoder.process_frame(frame)
409
410
if message_frame.opcode == Opcode.TEXT:
411
text_buffer += message_frame.payload
412
if message_frame.message_finished:
413
print(f"Complete text message: {text_buffer}")
414
text_buffer = ""
415
416
elif message_frame.opcode == Opcode.BINARY:
417
binary_buffer += message_frame.payload
418
if message_frame.message_finished:
419
print(f"Complete binary message: {len(binary_buffer)} bytes")
420
binary_buffer = b""
421
```
422
423
### Custom Frame Generation
424
425
```python
426
import struct
427
from wsproto.frame_protocol import Opcode, RsvBits
428
429
def create_custom_frame(opcode: Opcode, payload: bytes, fin: bool = True, masked: bool = True):
430
"""Create a custom WebSocket frame."""
431
# Build first byte (FIN + RSV + OPCODE)
432
first_byte = (int(fin) << 7) | int(opcode)
433
434
# Build second byte (MASK + payload length)
435
payload_len = len(payload)
436
if payload_len <= 125:
437
second_byte = (int(masked) << 7) | payload_len
438
extended_length = b""
439
elif payload_len <= 65535:
440
second_byte = (int(masked) << 7) | 126
441
extended_length = struct.pack("!H", payload_len)
442
else:
443
second_byte = (int(masked) << 7) | 127
444
extended_length = struct.pack("!Q", payload_len)
445
446
# Build frame
447
frame = bytes([first_byte, second_byte]) + extended_length
448
449
if masked:
450
import os
451
mask = os.urandom(4)
452
frame += mask
453
# Apply mask to payload
454
masked_payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
455
frame += masked_payload
456
else:
457
frame += payload
458
459
return frame
460
461
# Create custom frames
462
text_frame = create_custom_frame(Opcode.TEXT, b"Hello", fin=True, masked=True)
463
binary_frame = create_custom_frame(Opcode.BINARY, b"\x01\x02\x03", fin=True, masked=True)
464
ping_frame = create_custom_frame(Opcode.PING, b"ping", fin=True, masked=True)
465
```
466
467
### Error Handling
468
469
```python
470
from wsproto.frame_protocol import FrameProtocol, ParseFailed, CloseReason
471
472
protocol = FrameProtocol(client=False, extensions=[])
473
474
try:
475
protocol.receive_bytes(malformed_frame_data)
476
for frame in protocol.received_frames():
477
print(f"Received frame: {frame}")
478
479
except ParseFailed as e:
480
print(f"Frame parsing failed: {e}")
481
print(f"Suggested close code: {e.code}")
482
483
# Generate appropriate close frame
484
close_frame = protocol.close(e.code, str(e))
485
486
# Handle specific error types
487
if e.code == CloseReason.PROTOCOL_ERROR:
488
print("Protocol violation detected")
489
elif e.code == CloseReason.INVALID_FRAME_PAYLOAD_DATA:
490
print("Invalid payload data")
491
elif e.code == CloseReason.MESSAGE_TOO_BIG:
492
print("Message exceeds size limit")
493
```
494
495
### Performance Monitoring
496
497
```python
498
from wsproto.frame_protocol import FrameProtocol
499
import time
500
501
class MonitoredFrameProtocol(FrameProtocol):
502
"""Frame protocol with performance monitoring."""
503
504
def __init__(self, *args, **kwargs):
505
super().__init__(*args, **kwargs)
506
self.frame_count = 0
507
self.bytes_sent = 0
508
self.bytes_received = 0
509
self.start_time = time.time()
510
511
def send_data(self, payload=b"", fin=True):
512
frame_bytes = super().send_data(payload, fin)
513
self.frame_count += 1
514
self.bytes_sent += len(frame_bytes)
515
return frame_bytes
516
517
def receive_bytes(self, data):
518
self.bytes_received += len(data)
519
super().receive_bytes(data)
520
521
def get_stats(self):
522
elapsed = time.time() - self.start_time
523
return {
524
'frames_sent': self.frame_count,
525
'bytes_sent': self.bytes_sent,
526
'bytes_received': self.bytes_received,
527
'elapsed_time': elapsed,
528
'send_rate': self.bytes_sent / elapsed if elapsed > 0 else 0,
529
'receive_rate': self.bytes_received / elapsed if elapsed > 0 else 0,
530
}
531
532
# Use monitored protocol
533
protocol = MonitoredFrameProtocol(client=True, extensions=[])
534
535
# Send some data
536
protocol.send_data("Hello")
537
protocol.send_data(b"Binary data")
538
protocol.ping(b"ping")
539
540
# Check performance
541
stats = protocol.get_stats()
542
print(f"Performance stats: {stats}")
543
```