# Websocket Protocol

Socket Programming with a FastAPI real-time demo chat app

Amirhossein Khalili • Morteza Khanbabaie • Alireza Khosravi

Open with the project name and explain that the presentation focuses on WebSocket concepts through a simple chatroom implementation.

## WebSocket History Timeline

The communication model evolved from simple request/response to persistent real-time channels.

Classic HTTP 1991
Polling 1995+
Long Polling 2006
WebSocket 2011
Real-time Web 2010s+
Use this slide to name the major approaches only. The next slides explain how each mechanism works and what it costs.

## HTTP

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

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

HTTP is excellent for normal pages, forms, and APIs. Its basic model is not optimized for continuous server-to-client updates.

## Polling

The browser repeatedly asks the server for new data.

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

Polling is simple but wasteful. Short intervals improve latency but increase server and network cost.

## Long Polling

The server holds the request until an update is available.

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

Long Polling reduces empty responses compared with Polling, but it still works through repeated HTTP request cycles.

## WebSocket

An HTTP Upgrade creates one persistent Full-duplex channel.

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

This is the key WebSocket slide. The browser starts with HTTP, upgrades the connection, and then both client and server can send frames whenever needed.

## WebSocket in the OSI Model

Application semantics at Layer 7, reliable byte delivery through TCP/IP.

WebSocket is an application-layer protocol. It uses TCP for reliable transport and IP for routing. The lower layers do not know what a chat message or room is.

## HTTP Versions in WebSocket

Different HTTP versions start WebSocket connections differently.

HTTP/1.1 Upgrade + 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/2 Extended 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/3 CONNECT 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.

The project is best explained with HTTP/1.1 Upgrade. HTTP/2 and HTTP/3 support WebSocket through different mechanisms.

## Project Architecture

Browsers connect to one WebSocket endpoint; ConnectionManager groups sockets by room.

Explain the code structure: FastAPI serves the frontend, the WebSocket endpoint accepts connections, and the manager stores active clients by room before broadcasting messages.

## 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()

You can start by explaining that this module owns the WebSocket endpoint and delegates connection storage to the shared manager.

## 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(),
        }

This slide shows the server-to-client contract: message type, room, sender, content, and timestamp.

## 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)

The browser URL maps directly to this route. FastAPI receives the HTTP Upgrade request and the manager accepts the socket.

## 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)

Presence is separate from normal chat messages, so the frontend can update the sidebar without adding a chat bubble.

## 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)

This is the main real-time loop: receive text, parse JSON if possible, ignore empty messages, then broadcast.

## Connection Teardown and Presence Update

Handles user disconnects, cleans up the room, and updates presence for remaining clients.

app/websocket/chat.py

    except WebSocketDisconnect:
                manager.disconnect(websocket, room_id)
                await manager.broadcast_json(
                    event("system", room_id, "سیستم",
                          f"{client_id} از اتاق {room_id} خارج شد."),
                    room_id,
                )
                await manager.broadcast_json(
                    {
                        "type": "presence",
                        "room_id": room_id,
                        "client_id": "سیستم",
                        "content": "presence-updated",
                        "users": manager.room_users(room_id),
                        "timestamp": now_iso(),
                    },
                    room_id,
                )

This block handles client disconnection: it removes the websocket from the room, broadcasts a system message announcing the departure, and then sends an updated presence payload so all clients can refresh the active user list.

## ConnectionManager

Manages WebSocket connections grouped by room and cleans up disconnected clients.

app/websocket/manager.py

    class ConnectionManager:
                def __init__(self):
                    self.active_connections: dict[str, list[ClientConnection]] = {}

                async def connect(self, websocket: WebSocket, room_id: str, client_id: str):
                    await websocket.accept()
                    self.active_connections.setdefault(room_id, [])
                    self.active_connections[room_id].append(ClientConnection(websocket=websocket, client_id=client_id))

                def disconnect(self, websocket: WebSocket, room_id: str):
                    if room_id not in self.active_connections: return

                    self.active_connections[room_id] = [
                        connection
                        for connection in self.active_connections[room_id] if connection.websocket is not websocket]

                    if not self.active_connections[room_id]:
                        del self.active_connections[room_id]

This slide shows how the ConnectionManager tracks active users per room, handles new WebSocket connections, and removes clients when they disconnect. Empty rooms are automatically cleaned up.

## Broadcast

Sends messages to all clients in a room and removes failed WebSocket connections safely.

app/websocket/manager.py

    class ConnectionManager:

        async def broadcast_json(self, payload: dict[str, Any], room_id: str):
            if room_id not in self.active_connections:
                return

            dead_connections: list[WebSocket] = []

            # Iterate over a copy so disconnected clients can be removed safely later.
            for connection in list(self.active_connections[room_id]):
                try:
                    await connection.websocket.send_json(payload)
                except Exception:
                    dead_connections.append(connection.websocket)

            for websocket in dead_connections:
                self.disconnect(websocket, room_id)

This method broadcasts a JSON payload to all clients in a room. It safely handles broken connections by collecting failed sockets and removing them after iteration, preventing runtime errors while modifying the connection list.

## 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 = '';

You can introduce the frontend file by showing that the UI stores one WebSocket object and the current room/user state.

## 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);
    }

This is why the same frontend works locally and behind HTTPS on the server.

## 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();
    });

This slide connects WebSocket state to UI state: buttons, labels, and the message input change after the connection opens.

## 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);
    });

The frontend treats presence as sidebar data and normal messages as chat feed data.

## 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;
    }

This demonstrates DOM rendering and auto-scrolling. The UI changes because WebSocket data arrives, not because the page refreshes.

## 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();
    });

This is the browser-to-server direction. The server receives this JSON and broadcasts the content to the room.

## 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

End with the central idea: WebSocket is useful when real-time bidirectional communication matters, but it moves responsibility to the server to manage long-lived connections.
1 / 10
Left / Right to move - N for notes - F for fullscreen