0
# WebSocket Support
1
2
Sanic provides native WebSocket support for real-time, bidirectional communication between client and server. This includes WebSocket routing, connection management, message handling, and integration with the broader Sanic ecosystem.
3
4
## Capabilities
5
6
### WebSocket Routes
7
8
Define WebSocket endpoints using decorators and programmatic registration.
9
10
```python { .api }
11
def websocket(
12
uri: str,
13
host: str = None,
14
strict_slashes: bool = None,
15
subprotocols: list = None,
16
name: str = None,
17
apply: list = None,
18
version: int = None,
19
version_prefix: str = "/v",
20
error_format: str = None,
21
):
22
"""
23
Decorator for WebSocket routes.
24
25
Parameters:
26
- uri: WebSocket endpoint path
27
- host: Host restriction pattern
28
- strict_slashes: Override global strict slash setting
29
- subprotocols: Supported WebSocket subprotocols
30
- name: Route name for url_for
31
- apply: List of decorators to apply
32
- version: API version number
33
- version_prefix: Version URL prefix
34
- error_format: Error response format
35
36
Usage:
37
@app.websocket('/ws')
38
async def websocket_handler(request, ws):
39
await ws.send("Hello WebSocket!")
40
msg = await ws.recv()
41
print(f"Received: {msg}")
42
"""
43
44
def add_websocket_route(
45
handler,
46
uri: str,
47
**kwargs
48
):
49
"""
50
Add WebSocket route programmatically.
51
52
Parameters:
53
- handler: WebSocket handler function
54
- uri: WebSocket endpoint path
55
- **kwargs: Additional route options
56
"""
57
```
58
59
### WebSocket Protocol Interface
60
61
The WebSocket connection interface for message handling and connection management.
62
63
```python { .api }
64
class Websocket:
65
"""WebSocket connection protocol interface."""
66
67
async def send(
68
self,
69
data,
70
encoding: str = "text"
71
):
72
"""
73
Send data to WebSocket client.
74
75
Parameters:
76
- data: Data to send (str, bytes, or dict for JSON)
77
- encoding: Data encoding type ("text", "bytes", or "json")
78
79
Raises:
80
- WebsocketClosed: If connection is closed
81
- ConnectionError: If send fails
82
"""
83
84
async def recv(self, timeout: float = None):
85
"""
86
Receive data from WebSocket client.
87
88
Parameters:
89
- timeout: Receive timeout in seconds
90
91
Returns:
92
- Received data (str or bytes)
93
94
Raises:
95
- WebsocketClosed: If connection is closed
96
- asyncio.TimeoutError: If timeout exceeded
97
"""
98
99
async def ping(self, data: bytes = b""):
100
"""
101
Send ping frame to client.
102
103
Parameters:
104
- data: Optional ping data
105
106
Returns:
107
- Pong waiter coroutine
108
"""
109
110
async def pong(self, data: bytes = b""):
111
"""
112
Send pong frame to client.
113
114
Parameters:
115
- data: Optional pong data
116
"""
117
118
async def close(
119
self,
120
code: int = 1000,
121
reason: str = ""
122
):
123
"""
124
Close WebSocket connection.
125
126
Parameters:
127
- code: Close status code
128
- reason: Close reason string
129
"""
130
131
async def wait_closed(self):
132
"""Wait for connection to be closed."""
133
134
@property
135
def closed(self) -> bool:
136
"""Whether the connection is closed."""
137
138
@property
139
def open(self) -> bool:
140
"""Whether the connection is open."""
141
142
@property
143
def client_state(self):
144
"""Client connection state."""
145
146
@property
147
def server_state(self):
148
"""Server connection state."""
149
150
@property
151
def subprotocol(self) -> str:
152
"""Negotiated subprotocol."""
153
```
154
155
### WebSocket Handler Function
156
157
The signature and structure for WebSocket handler functions.
158
159
```python { .api }
160
async def websocket_handler(request, ws):
161
"""
162
WebSocket handler function signature.
163
164
Parameters:
165
- request: HTTP request object (for initial handshake)
166
- ws: WebSocket connection object
167
168
The handler should:
169
- Handle the WebSocket connection lifecycle
170
- Process incoming messages
171
- Send outgoing messages
172
- Handle connection errors and cleanup
173
- Manage connection state
174
"""
175
```
176
177
### WebSocket Configuration
178
179
Configuration options for WebSocket behavior and limits.
180
181
```python { .api }
182
# WebSocket timeout settings
183
WEBSOCKET_TIMEOUT: int = 10 # Connection timeout
184
WEBSOCKET_PING_INTERVAL: int = 20 # Ping interval in seconds
185
WEBSOCKET_PING_TIMEOUT: int = 20 # Ping timeout in seconds
186
187
# WebSocket message limits
188
WEBSOCKET_MAX_SIZE: int = 1048576 # Maximum message size (1MB)
189
WEBSOCKET_MAX_QUEUE: int = 32 # Maximum queued messages
190
191
# WebSocket compression
192
WEBSOCKET_COMPRESSION: str = None # Compression algorithm
193
```
194
195
### WebSocket Subprotocols
196
197
Support for WebSocket subprotocols for application-specific protocols.
198
199
```python { .api }
200
@app.websocket('/ws', subprotocols=['chat', 'notification'])
201
async def websocket_with_subprotocols(request, ws):
202
"""
203
WebSocket handler with subprotocol negotiation.
204
205
The client can request specific subprotocols during handshake.
206
The server selects the first supported subprotocol.
207
"""
208
209
selected_protocol = ws.subprotocol
210
211
if selected_protocol == 'chat':
212
await handle_chat_protocol(ws)
213
elif selected_protocol == 'notification':
214
await handle_notification_protocol(ws)
215
else:
216
await ws.close(code=1002, reason="Unsupported subprotocol")
217
```
218
219
### WebSocket Authentication
220
221
Authenticate WebSocket connections using request data and custom logic.
222
223
```python { .api }
224
@app.websocket('/ws/authenticated')
225
async def authenticated_websocket(request, ws):
226
"""
227
WebSocket handler with authentication.
228
229
Authentication is performed during the initial HTTP handshake
230
using headers, query parameters, or other request data.
231
"""
232
233
# Authenticate using request headers
234
auth_header = request.headers.get('Authorization')
235
if not auth_header:
236
await ws.close(code=1008, reason="Authentication required")
237
return
238
239
try:
240
user = await validate_websocket_token(auth_header)
241
request.ctx.user = user
242
except AuthenticationError:
243
await ws.close(code=1008, reason="Invalid authentication")
244
return
245
246
# Continue with authenticated WebSocket handling
247
await handle_authenticated_websocket(request, ws)
248
```
249
250
## Usage Examples
251
252
### Basic WebSocket Echo Server
253
254
```python
255
from sanic import Sanic
256
from sanic.response import html
257
258
app = Sanic("WebSocketApp")
259
260
@app.websocket('/ws')
261
async def websocket_echo(request, ws):
262
"""Simple echo WebSocket handler."""
263
264
await ws.send("Welcome to WebSocket echo server!")
265
266
async for msg in ws:
267
print(f"Received: {msg}")
268
await ws.send(f"Echo: {msg}")
269
270
@app.route('/')
271
async def index(request):
272
"""Serve WebSocket test page."""
273
return html('''
274
<!DOCTYPE html>
275
<html>
276
<head><title>WebSocket Test</title></head>
277
<body>
278
<div id="messages"></div>
279
<input type="text" id="messageInput" placeholder="Type a message...">
280
<button onclick="sendMessage()">Send</button>
281
282
<script>
283
const ws = new WebSocket('ws://localhost:8000/ws');
284
const messages = document.getElementById('messages');
285
286
ws.onmessage = function(event) {
287
const div = document.createElement('div');
288
div.textContent = event.data;
289
messages.appendChild(div);
290
};
291
292
function sendMessage() {
293
const input = document.getElementById('messageInput');
294
ws.send(input.value);
295
input.value = '';
296
}
297
</script>
298
</body>
299
</html>
300
''')
301
302
if __name__ == '__main__':
303
app.run(host='0.0.0.0', port=8000)
304
```
305
306
### Chat Room WebSocket Server
307
308
```python
309
import asyncio
310
from sanic import Sanic
311
from sanic.response import json
312
import json as json_lib
313
314
app = Sanic("ChatApp")
315
316
# Store active connections
317
connected_clients = set()
318
chat_rooms = {}
319
320
@app.websocket('/ws/chat/<room_id>')
321
async def chat_room(request, ws, room_id):
322
"""Multi-room chat WebSocket handler."""
323
324
# Initialize room if it doesn't exist
325
if room_id not in chat_rooms:
326
chat_rooms[room_id] = set()
327
328
# Add client to room
329
chat_rooms[room_id].add(ws)
330
connected_clients.add(ws)
331
332
try:
333
# Send welcome message
334
await ws.send(json_lib.dumps({
335
"type": "system",
336
"message": f"Welcome to chat room {room_id}",
337
"room": room_id
338
}))
339
340
# Broadcast user joined
341
await broadcast_to_room(room_id, {
342
"type": "user_joined",
343
"message": "A user joined the room",
344
"room": room_id
345
}, exclude=ws)
346
347
# Handle messages
348
async for message in ws:
349
try:
350
data = json_lib.loads(message)
351
352
if data.get("type") == "chat":
353
# Broadcast chat message to room
354
await broadcast_to_room(room_id, {
355
"type": "chat",
356
"message": data.get("message", ""),
357
"user": data.get("user", "Anonymous"),
358
"room": room_id
359
})
360
361
except json_lib.JSONDecodeError:
362
await ws.send(json_lib.dumps({
363
"type": "error",
364
"message": "Invalid JSON format"
365
}))
366
367
except Exception as e:
368
print(f"WebSocket error: {e}")
369
370
finally:
371
# Clean up when client disconnects
372
if ws in connected_clients:
373
connected_clients.remove(ws)
374
if room_id in chat_rooms and ws in chat_rooms[room_id]:
375
chat_rooms[room_id].remove(ws)
376
377
# Broadcast user left
378
await broadcast_to_room(room_id, {
379
"type": "user_left",
380
"message": "A user left the room",
381
"room": room_id
382
})
383
384
# Remove empty rooms
385
if not chat_rooms[room_id]:
386
del chat_rooms[room_id]
387
388
async def broadcast_to_room(room_id, message, exclude=None):
389
"""Broadcast message to all clients in a room."""
390
if room_id not in chat_rooms:
391
return
392
393
message_str = json_lib.dumps(message)
394
disconnected = set()
395
396
for client in chat_rooms[room_id]:
397
if client == exclude:
398
continue
399
400
try:
401
await client.send(message_str)
402
except Exception:
403
# Mark disconnected clients for removal
404
disconnected.add(client)
405
406
# Remove disconnected clients
407
chat_rooms[room_id] -= disconnected
408
409
@app.route('/api/rooms')
410
async def get_active_rooms(request):
411
"""Get list of active chat rooms."""
412
return json({
413
"rooms": list(chat_rooms.keys()),
414
"total_clients": len(connected_clients)
415
})
416
```
417
418
### Real-time Data Streaming
419
420
```python
421
import asyncio
422
import random
423
from datetime import datetime
424
425
app = Sanic("DataStreamApp")
426
427
@app.websocket('/ws/data-stream')
428
async def data_stream(request, ws):
429
"""Stream real-time data to WebSocket clients."""
430
431
# Send initial connection confirmation
432
await ws.send(json_lib.dumps({
433
"type": "connected",
434
"message": "Data stream connected",
435
"timestamp": datetime.utcnow().isoformat()
436
}))
437
438
# Create background task for data streaming
439
stream_task = asyncio.create_task(stream_data(ws))
440
receive_task = asyncio.create_task(handle_client_messages(ws))
441
442
try:
443
# Wait for either task to complete
444
done, pending = await asyncio.wait(
445
[stream_task, receive_task],
446
return_when=asyncio.FIRST_COMPLETED
447
)
448
449
# Cancel pending tasks
450
for task in pending:
451
task.cancel()
452
453
except Exception as e:
454
print(f"WebSocket streaming error: {e}")
455
456
finally:
457
if not ws.closed:
458
await ws.close()
459
460
async def stream_data(ws):
461
"""Stream data to WebSocket client."""
462
while not ws.closed:
463
try:
464
# Generate sample data
465
data = {
466
"type": "data",
467
"timestamp": datetime.utcnow().isoformat(),
468
"value": random.randint(1, 100),
469
"sensor_id": "sensor_001",
470
"temperature": round(random.uniform(20.0, 30.0), 2),
471
"humidity": round(random.uniform(40.0, 80.0), 2)
472
}
473
474
await ws.send(json_lib.dumps(data))
475
await asyncio.sleep(1) # Send data every second
476
477
except Exception as e:
478
print(f"Data streaming error: {e}")
479
break
480
481
async def handle_client_messages(ws):
482
"""Handle messages from WebSocket client."""
483
async for message in ws:
484
try:
485
data = json_lib.loads(message)
486
487
if data.get("type") == "ping":
488
await ws.send(json_lib.dumps({
489
"type": "pong",
490
"timestamp": datetime.utcnow().isoformat()
491
}))
492
493
elif data.get("type") == "subscribe":
494
# Handle subscription requests
495
sensor_id = data.get("sensor_id")
496
await ws.send(json_lib.dumps({
497
"type": "subscribed",
498
"sensor_id": sensor_id,
499
"message": f"Subscribed to {sensor_id}"
500
}))
501
502
except json_lib.JSONDecodeError:
503
await ws.send(json_lib.dumps({
504
"type": "error",
505
"message": "Invalid message format"
506
}))
507
```
508
509
### WebSocket with Authentication and Rate Limiting
510
511
```python
512
import time
513
from collections import defaultdict
514
515
app = Sanic("SecureWebSocketApp")
516
517
# Rate limiting storage
518
client_requests = defaultdict(list)
519
RATE_LIMIT = 10 # 10 messages per minute
520
RATE_WINDOW = 60 # 1 minute window
521
522
@app.websocket('/ws/secure')
523
async def secure_websocket(request, ws):
524
"""WebSocket with authentication and rate limiting."""
525
526
# Authenticate connection
527
token = request.args.get('token') or request.headers.get('Authorization')
528
if not token:
529
await ws.close(code=1008, reason="Authentication token required")
530
return
531
532
try:
533
user = await validate_token(token)
534
client_id = user['id']
535
except Exception:
536
await ws.close(code=1008, reason="Invalid authentication token")
537
return
538
539
await ws.send(json_lib.dumps({
540
"type": "authenticated",
541
"user": user['username'],
542
"message": "Authentication successful"
543
}))
544
545
try:
546
async for message in ws:
547
# Rate limiting check
548
if not check_rate_limit(client_id):
549
await ws.send(json_lib.dumps({
550
"type": "error",
551
"message": "Rate limit exceeded. Please slow down."
552
}))
553
continue
554
555
# Process message
556
try:
557
data = json_lib.loads(message)
558
559
# Echo message back with user info
560
response = {
561
"type": "message",
562
"user": user['username'],
563
"message": data.get("message", ""),
564
"timestamp": datetime.utcnow().isoformat()
565
}
566
567
await ws.send(json_lib.dumps(response))
568
569
except json_lib.JSONDecodeError:
570
await ws.send(json_lib.dumps({
571
"type": "error",
572
"message": "Invalid JSON format"
573
}))
574
575
except Exception as e:
576
print(f"Secure WebSocket error: {e}")
577
578
finally:
579
# Clean up rate limiting data
580
if client_id in client_requests:
581
del client_requests[client_id]
582
583
def check_rate_limit(client_id):
584
"""Check if client is within rate limits."""
585
now = time.time()
586
587
# Clean old requests
588
client_requests[client_id] = [
589
req_time for req_time in client_requests[client_id]
590
if now - req_time < RATE_WINDOW
591
]
592
593
# Check rate limit
594
if len(client_requests[client_id]) >= RATE_LIMIT:
595
return False
596
597
# Record this request
598
client_requests[client_id].append(now)
599
return True
600
601
async def validate_token(token):
602
"""Validate authentication token."""
603
# Implement your token validation logic
604
if token == "valid_token":
605
return {"id": "user123", "username": "testuser"}
606
raise ValueError("Invalid token")
607
```