WebSockets¶
WebSockets turn an HTTP upgrade into a long-lived bidirectional channel.
Handshake essentials¶
Client must send valid upgrade headers, including:
Upgrade: websocketConnection: UpgradeSec-WebSocket-KeySec-WebSocket-Version: 13
If handshake validation fails, connection is rejected.
ASGI flow¶
- app receives
websocket.connect - app sends one of:
websocket.acceptwebsocket.closewebsocket.http.response.start+ body (HTTP-style rejection path)- after accept, app handles receive/send messages
- close handshake and disconnect complete session
Easy echo example:
from __future__ import annotations
async def app(scope, receive, send):
"""Accept websocket clients and echo text messages."""
if scope["type"] != "websocket":
return
await send({"type": "websocket.accept"})
while True:
message = await receive()
message_type = message["type"]
if message_type == "websocket.disconnect":
break
if message_type == "websocket.receive" and "text" in message:
await send({"type": "websocket.send", "text": message["text"]})
Auth gate example:
from __future__ import annotations
from urllib.parse import parse_qs
async def app(scope, receive, send):
"""Allow upgrades only when a known token is present."""
if scope["type"] != "websocket":
return
query_string = scope.get("query_string", b"").decode("utf-8")
params = parse_qs(query_string)
token = params.get("token", [""])[0]
if token != "demo-token":
await send({"type": "websocket.close", "code": 1008, "reason": "unauthorized"})
return
await send({"type": "websocket.accept"})
while True:
message = await receive()
if message["type"] == "websocket.disconnect":
break
if message["type"] == "websocket.receive" and "text" in message:
await send({"type": "websocket.send", "text": f"secure:{message['text']}"})
Stateful room example:
from __future__ import annotations
from collections import defaultdict
ROOMS: dict[str, set] = defaultdict(set)
async def app(scope, receive, send):
"""Broadcast received messages to all clients in one room."""
if scope["type"] != "websocket":
return
query = scope.get("query_string", b"").decode("ascii")
room = "general"
if query.startswith("room="):
room = query.split("=", 1)[1] or "general"
await send({"type": "websocket.accept", "subprotocol": None})
ROOMS[room].add(send)
try:
while True:
message = await receive()
if message["type"] == "websocket.disconnect":
break
text = message.get("text")
if text is None:
continue
for peer_send in list(ROOMS[room]):
await peer_send({"type": "websocket.send", "text": f"[{room}] {text}"})
finally:
ROOMS[room].discard(send)
Runtime controls¶
--ws: backend mode selection--ws-max-size: max frame/message size--ws-max-queue: receive queue sizing--ws-ping-intervaland--ws-ping-timeout--ws-per-message-deflate
Failure cases you should test¶
- invalid handshake headers
- oversized payloads
- invalid UTF-8 text frames
- half-open disconnects
- proxy configurations that drop upgrade headers
Plain-language explanation¶
HTTP is a request letter. WebSocket is a live conversation. It stays open until one side closes.