🕯️ Magic Note
WebSocket is a protocol standardized in 2011 (RFC 6455). It provides full-duplex communication over a single TCP connection. Unlike HTTP, which requires a new connection for each request, WebSocket stays open, allowing instant data transfer in both directions.
| Feature | HTTP | WebSocket |
|---|---|---|
| Direction | Client → Server (request-response) | Bidirectional (client ↔ server) |
| Connection | Short-lived (closed after response) | Persistent (stays open) |
| Server Push | Not possible without polling | Native support |
| Overhead | Headers per request (hundreds of bytes) | Minimal after handshake |
| Latency | Higher (new connection each time) | Lower (messages sent immediately) |
| Use Case | REST APIs, web pages, file downloads | Chat, games, live dashboards, notifications |
🕯️ Magic Note
HTTP polling: client asks every second “Any news?” Most answers are “No.” WebSocket: client connects once, server says “News!” when it arrives. WebSocket saves bandwidth and reduces latency.
Bash
pip install websockets
Python (echo_server.py)
import asyncio
import websockets
async def echo(websocket, path):
print(f”Client connected from {websocket.remote_address}”)
try:
async for message in websocket:
print(f”Received: {message}”)
# Echo the message back to the client
await websocket.send(f”Echo: {message}”)
except websockets.exceptions.ConnectionClosed:
print(f”Client {websocket.remote_address} disconnected”)
async def main():
async with websockets.serve(echo, “localhost”, 8765):
print(“WebSocket server started on ws://localhost:8765”)
await asyncio.Future() # Run forever
if __name__ == “__main__”:
asyncio.run(main())
Python (echo_client.py)
import asyncio
import websockets
async def test_client():
uri = “ws://localhost:8765”
async with websockets.connect(uri) as websocket:
# Send a message
await websocket.send(“Hello, WebSocket!”)
print(“Sent: Hello, WebSocket!”)
# Receive response
response = await websocket.recv()
print(f”Received: {response}”)
asyncio.run(test_client())
🕯️ Magic Note
The WebSocket server uses `async for message in websocket` to receive messages. This pattern is clean and handles disconnections gracefully. The `ConnectionClosed` exception is raised when the client disconnects.
Python (chat_server.py)
import asyncio
import websockets
from datetime import datetime
connected_clients = set()
async def broadcast(message):
“””Send a message to all connected clients.”””
if connected_clients:
await asyncio.wait([client.send(message) for client in connected_clients])
async def chat_handler(websocket, path):
# Add client to the set
connected_clients.add(websocket)
print(f”Client connected. Total: {len(connected_clients)}”)
# Send welcome message
await websocket.send(“Welcome to the chat!”)
await broadcast(f”[System] New user joined ({len(connected_clients)} online)”)
try:
async for message in websocket:
timestamp = datetime.now().strftime(“%H:%M:%S”)
formatted_message = f”[{timestamp}] {message}”
print(formatted_message)
await broadcast(formatted_message)
except websockets.exceptions.ConnectionClosed:
print(“Client disconnected”)
finally:
connected_clients.remove(websocket)
await broadcast(f”[System] User left ({len(connected_clients)} online)”)
async def main():
async with websockets.serve(chat_handler, “localhost”, 8765):
print(“Chat server started on ws://localhost:8765”)
await asyncio.Future()
if __name__ == “__main__”:
asyncio.run(main())
HTML + JavaScript (chat_client.html)
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Chat</title>
</head>
<body>
<div id=”messages” style=”height: 300px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px;”></div>
<input type=”text” id=”messageInput” placeholder=”Type a message…” style=”width: 80%;”>
<button id=”sendButton”>Send</button>
<script>
const messagesDiv = document.getElementById(“messages”);
const input = document.getElementById(“messageInput”);
const button = document.getElementById(“sendButton”);
// Connect to WebSocket server
const ws = new WebSocket(“ws://localhost:8765”);
ws.onopen = function() {
addMessage(“[System] Connected to chat server”);
};
ws.onmessage = function(event) {
addMessage(event.data);
};
ws.onclose = function() {
addMessage(“[System] Disconnected from chat server”);
};
function addMessage(text) {
const div = document.createElement(“div”);
div.textContent = text;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function sendMessage() {
const message = input.value.trim();
if (message) {
ws.send(message);
input.value = “”;
}
}
button.onclick = sendMessage;
input.onkeypress = function(e) {
if (e.key === “Enter”) sendMessage();
};
</script>
</body>
</html>
🕯️ Magic Note
Open the HTML file in multiple browser windows. Type messages in one window. They appear in all windows. This is a working chat application in less than 100 lines of Python and 50 lines of JavaScript.
Python (counter_server.py)
import asyncio
import websockets
connected_clients = set()
counter = 0
async def counter_updater():
“””Update counter every second and broadcast to all clients.”””
global counter
while True:
await asyncio.sleep(1)
counter += 1
if connected_clients:
message = f”{counter}”
await asyncio.wait([client.send(message) for client in connected_clients])
async def handler(websocket, path):
connected_clients.add(websocket)
print(f”Client connected. Total: {len(connected_clients)}”)
try:
await websocket.wait_closed()
finally:
connected_clients.remove(websocket)
print(f”Client disconnected. Total: {len(connected_clients)}”)
async def main():
# Run counter updater in background
asyncio.create_task(counter_updater())
async with websockets.serve(handler, “localhost”, 8765):
print(“Counter server started on ws://localhost:8765”)
await asyncio.Future()
if __name__ == “__main__”:
asyncio.run(main())
HTML + JavaScript (counter_client.html)
<!DOCTYPE html>
<html>
<head>
<title>Real-Time Counter</title>
</head>
<body>
<h1>Real-Time Counter</h1>
<div style=”font-size: 72px; text-align: center; padding: 50px;” id=”counter”>0</div>
<script>
const counterDiv = document.getElementById(“counter”);
const ws = new WebSocket(“ws://localhost:8765”);
ws.onmessage = function(event) {
counterDiv.textContent = event.data;
};
ws.onclose = function() {
counterDiv.textContent = “Disconnected”;
};
</script>
</body>
</html>
🕯️ Magic Note
Open the HTML file in multiple browser windows. The counter updates simultaneously in all windows. This demonstrates server push—the server sends updates without the client asking for them.
Python (private_chat_server.py)
import asyncio
import websockets
import json
clients = {} # username -> websocket
async def broadcast(message, exclude=None):
“””Send message to all connected clients.”””
for client in clients.values():
if client != exclude:
try:
await client.send(message)
except:
pass
async def chat_handler(websocket, path):
# Wait for username
username = await websocket.recv()
if username in clients:
await websocket.send(“ERROR: Username already taken”)
await websocket.close()
return
clients[username] = websocket
print(f”{username} joined. Total: {len(clients)}”)
await broadcast(json.dumps({“type”: “system”, “message”: f”{username} joined”}))
try:
async for raw_message in websocket:
data = json.loads(raw_message)
if data[“type”] == “public”:
await broadcast(json.dumps({
“type”: “public”,
“from”: username,
“message”: data[“message”]
}))
elif data[“type”] == “private”:
target = data[“to”]
if target in clients:
await clients[target].send(json.dumps({
“type”: “private”,
“from”: username,
“message”: data[“message”]
}))
await websocket.send(json.dumps({
“type”: “private_sent”,
“to”: target,
“message”: data[“message”]
}))
else:
await websocket.send(json.dumps({
“type”: “error”,
“message”: f”User {target} not found”
}))
except websockets.exceptions.ConnectionClosed:
pass
finally:
del clients[username]
await broadcast(json.dumps({“type”: “system”, “message”: f”{username} left”}))
print(f”{username} left. Total: {len(clients)}”)
async def main():
async with websockets.serve(chat_handler, “localhost”, 8765):
print(“Private chat server started on ws://localhost:8765”)
await asyncio.Future()
if __name__ == “__main__”:
asyncio.run(main())
JavaScript (reconnecting_client.js)
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.maxRetries = options.maxRetries || 5;
this.retryDelay = options.retryDelay || 3000;
this.onMessage = options.onMessage || (() => {});
this.onConnect = options.onConnect || (() => {});
this.onDisconnect = options.onDisconnect || (() => {});
this.retryCount = 0;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log(“Connected to WebSocket server”);
this.retryCount = 0;
this.onConnect();
};
this.ws.onmessage = (event) => {
this.onMessage(event.data);
};
this.ws.onclose = () => {
console.log(“WebSocket connection closed”);
this.onDisconnect();
this.reconnect();
};
this.ws.onerror = (error) => {
console.error(“WebSocket error:”, error);
};
}
reconnect() {
if (this.retryCount >= this.maxRetries) {
console.log(“Max retries reached. Giving up.”);
return;
}
this.retryCount++;
const delay = this.retryDelay * Math.pow(2, this.retryCount – 1);
console.log(`Reconnecting in ${delay/1000} seconds… (attempt ${this.retryCount}/${this.maxRetries})`);
setTimeout(() => this.connect(), delay);
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
console.warn(“Cannot send message: WebSocket not open”);
}
}
}
- Validate and sanitize all messages (do not trust client input)
- Implement authentication before allowing WebSocket connections
- Use `wss://` (WebSocket Secure) in production (same as HTTPS)
- Set appropriate timeouts to prevent hanging connections
- Limit message size to prevent denial-of-service attacks
- Rate-limit messages per client
Python (secure_server.py – with authentication)
import asyncio
import websockets
import jwt # pip install pyjwt
SECRET_KEY = “your-secret-key”
async def auth_handler(websocket, path):
# Expect first message to be a JWT token
token = await websocket.recv()
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[“HS256”])
username = payload[“username”]
print(f”Authenticated user: {username}”)
# Accept the connection and continue with authenticated session
await websocket.send(“Authenticated successfully”)
# Handle messages…
async for message in websocket:
# Process authenticated message
pass
except jwt.InvalidTokenError:
await websocket.send(“Authentication failed”)
await websocket.close()
- **Simple REST APIs:** WebSocket overhead is unnecessary
- **File downloads:** HTTP is better optimized
- **Rare updates:** HTTP polling may be simpler
- **Serverless environments:** WebSocket support varies
- Not handling disconnections (clients may leave without notice)
- Sending too many messages (overwhelming clients)
- Not implementing reconnection logic on client side
- Using WebSocket where a simple HTTP request would suffice
- Forgetting that WebSocket messages are asynchronous
- What is the main difference between HTTP and WebSocket?
- How do you send a message to all connected clients?
- Write a WebSocket server that echoes messages back to the sender.
- How do you handle client disconnections gracefully?
- What is the purpose of `async for message in websocket`?
- Why would you implement reconnection logic on the client side?
⚡ Whisper
HTTP is request-response. Ask, then wait. Ask again. WebSocket is different. Connect once. Then send. Receive. Both ways. Any time. This is real-time. This is chat, games, live dashboards, collaborative editing. The server pushes news instantly. The client responds immediately. The connection stays open. Use WebSocket when you need speed. When you need push. When polling is too slow. Learn the protocol. Build a chat server. Broadcast messages. Handle disconnections. Implement reconnection. Your applications will come alive. Users will see changes instantly. This is not the future. This is now. Open the connection. Send the message. Watch it arrive.