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.

Classic HTTP1991
Polling1995+
Long Polling2006
WebSocket2011
Real-time Web2010s+

HTTP

A short-lived request/response cycle with no native live push.

HTTP request response mechanism
Mechanism
  • One request returns one response
  • The connection closes after the exchange
Cost
  • 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.

Polling repeated request mechanism
Mechanism
  • The client checks on a fixed timer
  • The server answers even when nothing changed
Cost
  • Many empty requests waste network work
  • Latency depends on the timer interval

Long Polling

The server holds the request until an update is available.

Long Polling held request mechanism
Mechanism
  • The server keeps the request open
  • The client reconnects after each response
Cost
  • Open requests consume server state
  • Reconnect and timeout handling is required

WebSocket

An HTTP Upgrade creates one persistent Full-duplex channel.

HTTP Upgrade Handshake
Mechanism
  • HTTP Upgrade switches the protocol
  • Frames move both ways on one socket
Cost
  • 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.

WebSocket in OSI model

HTTP Versions in WebSocket

Different HTTP versions start WebSocket connections differently.

HTTP/1.1Upgrade + 101 Switching Protocols
  • The browser sends a normal GET request with Upgrade: websocket.
  • The server replies 101 Switching Protocols if it accepts.
  • After that, the same TCP connection carries WebSocket frames both ways.
  • This is the classic and easiest model to understand.
HTTP/2Extended CONNECT over a Stream
  • 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/3CONNECT over QUIC
  • 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.

WebSocket chatroom architecture

Backend Codes

WebSocket router setup in FastAPI.

app/websocket/chat.py
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.

app/websocket/chat.py
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.

app/websocket/chat.py
@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.

app/websocket/chat.py
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.

app/websocket/chat.py
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.

app/websocket/manager.py
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.

app/static/app.js
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.

app/static/app.js
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.

app/static/app.js
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.

app/static/app.js
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.

app/static/app.js
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.

app/static/app.js
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.

HTTP is request/response
  • Simple
  • Stateless
  • Not ideal for live push
Polling simulates real-time
  • Easy
  • Wasteful
  • Interval-based latency
WebSocket keeps a channel
  • Persistent
  • Full-duplex
  • Low latency
Server design matters
  • Connection state
  • Disconnect handling
  • Scaling strategy
Left / Right to move - N for notes - F for fullscreen