diff --git a/docs/slides/index.html b/docs/slides/index.html index 2076bbd..947ca02 100644 --- a/docs/slides/index.html +++ b/docs/slides/index.html @@ -1,377 +1,465 @@ - - - - - - WebSocket Protocol - - - -
-
-
-

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
-
-
- - - + + + + + + WebSocket Protocol + + + +
+
+
+

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

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,
+            )
+
+ + +
+ +
+

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]
+
+ + +
+ +
+

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

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