WebSockets: In-Depth Explanation & Python Example
WebSockets is a full-duplex communication protocol (bidirectional, persistent, stateful connection) over TCP, enabling real-time, low-latency data exchange between client and server without the overhead of repeated HTTP requests. Standardized in RFC 6455 (2011), WebSockets upgrade from HTTP/1.1 handshakes and maintain an open channel for continuous messaging, making it ideal for interactive apps. Unlike HTTP's request-response model, WebSockets support streaming in both directions, reducing latency and bandwidth.
WebSocket Handshake
- Initiation: Client sends an HTTP request with
Upgrade: websocketheader to/ws(or any path). - Server Response: Server replies with
101 Switching Protocolsif accepting, includingSec-WebSocket-Keychallenge-response for security. - Example Handshake (Headers): ``` Client Request: GET /chat HTTP/1.1 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13
Server Response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
``
- **Nuance**: Fallback to HTTP if handshake fails; supports subprotocols (e.g.,graphql-ws`).
Framing and Message Format
- Frames: Data sent in binary/text frames (up to 2^63 bytes; fragmented if larger).
- Opcode: Type (0x1 = text, 0x2 = binary, 0x8 = close, 0x9 = ping, 0xA = pong).
- Masking: Client frames masked (XOR with key) for security; server unmasked.
- Payload: Actual data; FIN bit signals end of message.
- Control Frames: Ping/pong for heartbeats (keep-alive); close for graceful shutdown.
- Extensions: Compression (permessage-deflate), multiplexing.
Connection Lifecycle
- Establish: HTTP upgrade handshake.
- Data Exchange: Bidirectional frames (e.g., client sends JSON, server responds).
- Close: Either side sends close frame (status code + reason); TCP close follows.
- Error Handling: Unexpected close → onerror; timeouts via ping.
WebSockets vs HTTP/REST
| Aspect | WebSockets | HTTP/REST |
|---|---|---|
| Connection | Persistent (full-duplex). | Stateless (request-response). |
| Latency | Low (no handshake per message). | Higher (new TCP/TLS per request). |
| Overhead | Minimal framing (2-14 bytes). | Headers (200-500 bytes). |
| Use Case | Real-time (chat, gaming). | Stateless APIs (CRUD). |
| Scalability | Sticky sessions or pub/sub. | Load-balanced stateless. |
Pros: Real-time bidirectional; efficient for frequent small messages. Cons: State management; harder scaling (no native load balancing).
** Use Cases**
- Real-Time Apps: Chat (e.g., Slack), live updates (stock tickers).
- Gaming: Multiplayer sync (position updates).
- IoT: Sensor streams.
- Collaborative Tools: Shared editing (Google Docs).
- Fallback: Long-polling or Server-Sent Events (SSE) for HTTP-only.
Security Considerations
-
TLS (WSS): Mandatory for production (encrypts handshake/frames).
- TLS certificate verification happens only once, during the initial TLS handshake when the WebSocket connection is established (i.e., when upgrading from HTTPS to WSS).
- After the handshake is complete and the TLS tunnel is created.
- Every subsequent WebSocket frame (message, ping, pong, etc.) is encrypted and sent inside the already-verified TLS tunnel.
- The server does not re-verify the certificate for each frame.
- The connection remains mutually authenticated and encrypted for its entire lifetime (until close or timeout).
-
Origin Validation: Server checks Origin header to prevent CSRF.
- Authentication: JWT in headers or subprotocol. For every client message (stream data), the server typically validates the JWT to ensure session integrity, as WebSockets lack built-in auth per frame. This is stateless (no server session store) and secure against token replay.
- Rate Limiting: Prevent DoS (e.g., via middleware).
Sample Python Code: Simple WebSocket Server & Client
Use the websockets library (pip install websockets) for a quick, runnable echo server (server echoes client messages) and client. Run server first, then client.
Server (server.py)
import asyncio
import websockets
import json
async def echo(websocket, path):
async for message in websocket:
try:
data = json.loads(message)
print(f"Received: {data}")
response = {"type": "echo", "data": data, "timestamp": time.time()}
await websocket.send(json.dumps(response))
except json.JSONDecodeError:
await websocket.send(f"Echo: {message}")
if __name__ == "__main__":
import time
print("WebSocket server starting on ws://localhost:8765")
start_server = websockets.serve(echo, "localhost", 8765)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
- How It Works: Listens on port 8765; echoes JSON messages with timestamp. Handles text/binary.
Client (client.py)
import asyncio
import websockets
import json
import time
async def client():
uri = "ws://localhost:8765"
async with websockets.connect(uri) as websocket:
# Send message
message = {"action": "hello", "value": "world"}
await websocket.send(json.dumps(message))
# Receive response
response = await websocket.recv()
print(f"Server response: {response}")
# Ping for keep-alive
await websocket.ping()
if __name__ == "__main__":
asyncio.run(client())
- How It Works: Connects, sends JSON, receives echo. Run:
python client.pyafter server.
Running:
1. Install: pip install websockets.
2. Server: python server.py.
3. Client: python client.py (in new terminal).
- Output: Server logs received; client prints echoed response.
This demonstrates handshake, framing, and bidirectional flow. For production (TLS, auth), add ssl_context to connect/serve. Let me know for extensions!