- One request returns one response
- The connection closes after the exchange
Websocket Protocol
-Socket Programming with a FastAPI real-time demo chat app
-Amirhossein Khalili • Morteza Khanbabaie • Alireza Khosravi
-WebSocket History Timeline
-The communication model evolved from simple request/response to persistent real-time channels.
-HTTP
-A short-lived request/response cycle with no native live push.
-- Every update needs a new HTTP cycle
- The server cannot push live data by itself
Polling
-The browser repeatedly asks the server for new data.
-- The client checks on a fixed timer
- The server answers even when nothing changed
- Many empty requests waste network work
- Latency depends on the timer interval
Long Polling
-The server holds the request until an update is available.
-- The server keeps the request open
- The client reconnects after each response
- Open requests consume server state
- Reconnect and timeout handling is required
WebSocket
-An HTTP Upgrade creates one persistent Full-duplex channel.
-- HTTP Upgrade switches the protocol
- Frames move both ways on one socket
- Each client keeps a live connection
- Scaling needs connection-aware design
WebSocket in the OSI Model
-Application semantics at Layer 7, reliable byte delivery through TCP/IP.
-HTTP Versions in WebSocket
-Different HTTP versions start WebSocket connections differently.
-- The browser sends a normal GET request with
Upgrade: websocket. - The server replies
101 Switching Protocolsif it accepts. - After that, the same TCP connection carries WebSocket frames both ways.
- This is the classic and easiest model to understand.
- HTTP/2 does not use the old connection-level Upgrade flow.
- It uses Extended CONNECT, so WebSocket traffic lives inside one HTTP/2 stream.
- Other HTTP/2 streams can share the same underlying connection.
- Support depends more on servers, clients, and proxies.
- HTTP/3 runs on QUIC over UDP instead of TCP.
- WebSocket uses a CONNECT-based mapping similar in idea to HTTP/2.
- QUIC streams reduce transport-level head-of-line blocking.
- It is modern, but the demo is easier to explain with HTTP/1.1.
Project Architecture
-Browsers connect to one WebSocket endpoint; ConnectionManager groups sockets by room.
-Backend Codes
-WebSocket router setup in FastAPI.
-import json
-from datetime import datetime, timezone
-
-from fastapi import APIRouter, WebSocket, WebSocketDisconnect
-
-from app.websocket.manager import manager
-
-router = APIRouter()
- Message Payload Shape
-Every server message uses one small JSON structure.
-def now_iso() -> str:
- return datetime.now(timezone.utc).isoformat()
-
-def event(event_type, room_id, client_id, content):
- return {
- "type": event_type,
- "room_id": room_id,
- "client_id": client_id,
- "content": content,
- "timestamp": now_iso(),
- }
- WebSocket Endpoint
-The route parameters select the room and the user identity.
-@router.websocket("/ws/{room_id}/{client_id}")
-async def websocket_endpoint(
- websocket: WebSocket,
- room_id: str,
- client_id: str,
-):
- room_id = clean_value(room_id, "general")
- client_id = clean_value(client_id, "anonymous")
-
- await manager.connect(websocket, room_id, client_id)
- Join and Presence Events
-After connection, the server informs the room and refreshes online users.
-await manager.broadcast_json(
- event("system", room_id, "system", f"{client_id} joined {room_id}."),
- room_id,
-)
-
-await manager.broadcast_json({
- "type": "presence",
- "users": manager.room_users(room_id),
- "timestamp": now_iso(),
-}, room_id)
- Receive Loop and Broadcast
-The socket stays open and every valid message is broadcast to the room.
-while True:
- raw_data = await websocket.receive_text()
-
- try:
- data = json.loads(raw_data)
- message = str(data.get("content", "")).strip()
- except json.JSONDecodeError:
- message = raw_data.strip()
-
- if message:
- await manager.broadcast_json(event("message", room_id, client_id, message), room_id)
- ConnectionManager
-The manager groups sockets by room and removes dead connections.
-class ConnectionManager:
- def __init__(self):
- self.active_connections = {}
-
- async def connect(self, websocket, room_id, client_id):
- await websocket.accept()
- self.active_connections.setdefault(room_id, [])
- self.active_connections[room_id].append(ClientConnection(websocket, client_id))
-
- async def broadcast_json(self, payload, room_id):
- for connection in list(self.active_connections.get(room_id, [])):
- await connection.websocket.send_json(payload)
- Frontend Codes
-browser state and DOM references.
-const usernameInput = document.querySelector('#username');
-const roomInput = document.querySelector('#room');
-const connectBtn = document.querySelector('#connectBtn');
-const messageForm = document.querySelector('#messageForm');
-const messages = document.querySelector('#messages');
-const usersList = document.querySelector('#usersList');
-
-let socket = null;
-let currentUser = '';
-let currentRoom = '';
- Building the WebSocket URL
-The browser automatically switches between ws and wss.
-function connect() {
- currentUser = safeName(usernameInput.value, 'anonymous');
- currentRoom = safeName(roomInput.value, 'general');
-
- const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
- const room = encodeURIComponent(currentRoom);
- const user = encodeURIComponent(currentUser);
- const wsUrl = `${wsProtocol}://${window.location.host}/ws/${room}/${user}`;
-
- socket = new WebSocket(wsUrl);
-}
- Connection Open Handler
-When the socket opens, the UI moves into connected mode.
-socket.addEventListener('open', () => {
- setConnectedState(true);
-
- roomTitle.textContent = `Room: ${currentRoom}`;
- roomSubtitle.textContent = `You are chatting as ${currentUser}.`;
-
- messageInput.focus();
-});
- Handling Incoming Messages
-Presence updates and chat messages are separated by type.
-socket.addEventListener('message', (event) => {
- const payload = JSON.parse(event.data);
-
- if (payload.type === 'presence') {
- updateUsers(payload.users || []);
- return;
- }
-
- addMessage(payload);
-});
- Rendering a Chat Message
-The browser creates message elements without reloading the page.
-function addMessage(payload) {
- clearEmptyState();
-
- const card = document.createElement('article');
- card.className = 'message';
-
- if (payload.client_id === currentUser) card.classList.add('mine');
-
- content.textContent = payload.content || '';
- card.append(meta, content);
- messages.appendChild(card);
- messages.scrollTop = messages.scrollHeight;
-}
- Sending Messages
-The form sends JSON only when the socket is open.
-messageForm.addEventListener('submit', (event) => {
- event.preventDefault();
-
- const content = messageInput.value.trim();
- if (!content || !socket || socket.readyState !== WebSocket.OPEN) return;
-
- socket.send(JSON.stringify({ content }));
- messageInput.value = '';
- messageInput.focus();
-});
- Keynotes and Takeaways
-The essential points to remember from the project.
-- Simple
- Stateless
- Not ideal for live push
- Easy
- Wasteful
- Interval-based latency
- Persistent
- Full-duplex
- Low latency
- Connection state
- Disconnect handling
- Scaling strategy