Compare commits

..

6 Commits

6 changed files with 589 additions and 153 deletions

View File

@@ -9,5 +9,8 @@ __pycache__/
.ruff_cache/ .ruff_cache/
.uvicorn.pid .uvicorn.pid
docs/ docs/images/
docs/screenshots/
docs/*.pdf
docs/presentation.md
README.md README.md

View File

@@ -6,9 +6,11 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir --upgrade pip setuptools wheel -i https://package-mirror.liara.ir/repository/pypi/simple \
&& pip install --no-cache-dir -r requirements.txt -i https://package-mirror.liara.ir/repository/pypi/simple
COPY app ./app COPY app ./app
COPY docs/slides ./docs/slides
EXPOSE 8000 EXPOSE 8000

View File

@@ -1,7 +1,7 @@
from pathlib import Path from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import FileResponse from fastapi.responses import FileResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from app.websocket.chat import router as chat_router from app.websocket.chat import router as chat_router
@@ -9,6 +9,7 @@ from app.websocket.manager import manager
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
STATIC_DIR = BASE_DIR / "static" STATIC_DIR = BASE_DIR / "static"
SLIDES_DIR = BASE_DIR.parent / "docs" / "slides"
app = FastAPI(title="Simple WebSocket Chatroom") app = FastAPI(title="Simple WebSocket Chatroom")
@@ -20,6 +21,17 @@ app.include_router(chat_router)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
@app.get("/slides", include_in_schema=False)
async def slides_redirect():
"""Redirect to the static slides directory so relative asset paths work."""
return RedirectResponse(url="/slides/")
# Serves the presentation and its relative assets.
# This keeps docs/slides/index.html openable directly from the file system too.
app.mount("/slides", StaticFiles(directory=SLIDES_DIR, html=True), name="slides")
@app.get("/") @app.get("/")
async def home(): async def home():
"""Serve the chatroom GUI.""" """Serve the chatroom GUI."""

Binary file not shown.

View File

@@ -1,145 +1,465 @@
<!doctype html> <!doctype html>
<html lang="en" dir="ltr"> <html lang="en" dir="ltr">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebSocket Protocol</title> <title>WebSocket Protocol</title>
<link rel="stylesheet" href="styles.css" /> <link rel="stylesheet" href="styles.css" />
</head> </head>
<body> <body>
<main class="deck" id="deck"> <main class="deck" id="deck">
<section class="slide active center-slide" data-title="Title"> <section class="slide active center-slide" data-title="Title">
<div class="center-content"> <div class="center-content">
<h1>Websocket Protocol</h1> <h1>Websocket Protocol</h1>
<p class="subtitle">Socket Programming with a FastAPI real-time demo chat app</p> <p class="subtitle">Socket Programming with a FastAPI real-time demo chat app</p>
<p class="members">Amirhossein Khalili &bull; Morteza Khanbabaie &bull; Alireza Khosravi</p> <p class="members">Amirhossein Khalili &bull; Morteza Khanbabaie &bull; Alireza Khosravi</p>
</div> </div>
<aside class="notes">Open with the project name and explain that the presentation focuses on WebSocket concepts through a simple chatroom implementation.</aside> <aside class="notes">Open with the project name and explain that the presentation focuses on WebSocket concepts through a simple chatroom implementation.</aside>
</section> </section>
<section class="slide" data-title="Timeline"> <section class="slide" data-title="Timeline">
<h2>WebSocket History Timeline</h2> <h2>WebSocket History Timeline</h2>
<p class="subtitle">The communication model evolved from simple request/response to persistent real-time channels.</p> <p class="subtitle">The communication model evolved from simple request/response to persistent real-time channels.</p>
<div class="timeline-only glass-panel"> <div class="timeline-only glass-panel">
<div class="timeline-method"><span>Classic HTTP</span><small>1991</small></div> <div class="timeline-method"><span>Classic HTTP</span><small>1991</small></div>
<div class="timeline-method"><span>Polling</span><small>1995+</small></div> <div class="timeline-method"><span>Polling</span><small>1995+</small></div>
<div class="timeline-method"><span>Long Polling</span><small>2006</small></div> <div class="timeline-method"><span>Long Polling</span><small>2006</small></div>
<div class="timeline-method"><span>WebSocket</span><small>2011</small></div> <div class="timeline-method"><span>WebSocket</span><small>2011</small></div>
<div class="timeline-method"><span>Real-time Web</span><small>2010s+</small></div> <div class="timeline-method"><span>Real-time Web</span><small>2010s+</small></div>
</div> </div>
<aside class="notes">Use this slide to name the major approaches only. The next slides explain how each mechanism works and what it costs.</aside> <aside class="notes">Use this slide to name the major approaches only. The next slides explain how each mechanism works and what it costs.</aside>
</section> </section>
<section class="slide protocol-slide" data-title="HTTP"> <section class="slide protocol-slide" data-title="HTTP">
<h2>HTTP</h2> <h2>HTTP</h2>
<p class="subtitle">A short-lived request/response cycle with no native live push.</p> <p class="subtitle">A short-lived request/response cycle with no native live push.</p>
<div class="protocol-layout"> <div class="protocol-layout">
<div class="protocol-visual glass-panel"> <div class="protocol-visual glass-panel">
<img src="assets/http-mechanism.svg" alt="HTTP request response mechanism" /> <img src="assets/http-mechanism.svg" alt="HTTP request response mechanism" />
</div> </div>
<div class="protocol-points glass-panel"> <div class="protocol-points glass-panel">
<article><b>Mechanism</b><ul><li>One request returns one response</li><li>The connection closes after the exchange</li></ul></article> <article><b>Mechanism</b><ul><li>One request returns one response</li><li>The connection closes after the exchange</li></ul></article>
<article><b>Cost</b><ul><li>Every update needs a new HTTP cycle</li><li>The server cannot push live data by itself</li></ul></article> <article><b>Cost</b><ul><li>Every update needs a new HTTP cycle</li><li>The server cannot push live data by itself</li></ul></article>
</div> </div>
</div> </div>
<aside class="notes">HTTP is excellent for normal pages, forms, and APIs. Its basic model is not optimized for continuous server-to-client updates.</aside> <aside class="notes">HTTP is excellent for normal pages, forms, and APIs. Its basic model is not optimized for continuous server-to-client updates.</aside>
</section> </section>
<section class="slide protocol-slide" data-title="Polling"> <section class="slide protocol-slide" data-title="Polling">
<h2>Polling</h2> <h2>Polling</h2>
<p class="subtitle">The browser repeatedly asks the server for new data.</p> <p class="subtitle">The browser repeatedly asks the server for new data.</p>
<div class="protocol-layout"> <div class="protocol-layout">
<div class="protocol-visual glass-panel"> <div class="protocol-visual glass-panel">
<img src="assets/polling-mechanism.svg" alt="Polling repeated request mechanism" /> <img src="assets/polling-mechanism.svg" alt="Polling repeated request mechanism" />
</div> </div>
<div class="protocol-points glass-panel"> <div class="protocol-points glass-panel">
<article><b>Mechanism</b><ul><li>The client checks on a fixed timer</li><li>The server answers even when nothing changed</li></ul></article> <article><b>Mechanism</b><ul><li>The client checks on a fixed timer</li><li>The server answers even when nothing changed</li></ul></article>
<article><b>Cost</b><ul><li>Many empty requests waste network work</li><li>Latency depends on the timer interval</li></ul></article> <article><b>Cost</b><ul><li>Many empty requests waste network work</li><li>Latency depends on the timer interval</li></ul></article>
</div> </div>
</div> </div>
<aside class="notes">Polling is simple but wasteful. Short intervals improve latency but increase server and network cost.</aside> <aside class="notes">Polling is simple but wasteful. Short intervals improve latency but increase server and network cost.</aside>
</section> </section>
<section class="slide protocol-slide" data-title="Long Polling"> <section class="slide protocol-slide" data-title="Long Polling">
<h2>Long Polling</h2> <h2>Long Polling</h2>
<p class="subtitle">The server holds the request until an update is available.</p> <p class="subtitle">The server holds the request until an update is available.</p>
<div class="protocol-layout"> <div class="protocol-layout">
<div class="protocol-visual glass-panel"> <div class="protocol-visual glass-panel">
<img src="assets/long-polling-mechanism.svg" alt="Long Polling held request mechanism" /> <img src="assets/long-polling-mechanism.svg" alt="Long Polling held request mechanism" />
</div> </div>
<div class="protocol-points glass-panel"> <div class="protocol-points glass-panel">
<article><b>Mechanism</b><ul><li>The server keeps the request open</li><li>The client reconnects after each response</li></ul></article> <article><b>Mechanism</b><ul><li>The server keeps the request open</li><li>The client reconnects after each response</li></ul></article>
<article><b>Cost</b><ul><li>Open requests consume server state</li><li>Reconnect and timeout handling is required</li></ul></article> <article><b>Cost</b><ul><li>Open requests consume server state</li><li>Reconnect and timeout handling is required</li></ul></article>
</div> </div>
</div> </div>
<aside class="notes">Long Polling reduces empty responses compared with Polling, but it still works through repeated HTTP request cycles.</aside> <aside class="notes">Long Polling reduces empty responses compared with Polling, but it still works through repeated HTTP request cycles.</aside>
</section> </section>
<section class="slide protocol-slide" data-title="WebSocket"> <section class="slide protocol-slide" data-title="WebSocket">
<h2>WebSocket</h2> <h2>WebSocket</h2>
<p class="subtitle">An HTTP Upgrade creates one persistent Full-duplex channel.</p> <p class="subtitle">An HTTP Upgrade creates one persistent Full-duplex channel.</p>
<div class="protocol-layout"> <div class="protocol-layout">
<div class="protocol-visual glass-panel"> <div class="protocol-visual glass-panel">
<img src="assets/websocket-handshake.svg" alt="HTTP Upgrade Handshake" /> <img src="assets/websocket-handshake.svg" alt="HTTP Upgrade Handshake" />
</div> </div>
<div class="protocol-points glass-panel"> <div class="protocol-points glass-panel">
<article><b>Mechanism</b><ul><li>HTTP Upgrade switches the protocol</li><li>Frames move both ways on one socket</li></ul></article> <article><b>Mechanism</b><ul><li>HTTP Upgrade switches the protocol</li><li>Frames move both ways on one socket</li></ul></article>
<article><b>Cost</b><ul><li>Each client keeps a live connection</li><li>Scaling needs connection-aware design</li></ul></article> <article><b>Cost</b><ul><li>Each client keeps a live connection</li><li>Scaling needs connection-aware design</li></ul></article>
</div> </div>
</div> </div>
<aside class="notes">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.</aside> <aside class="notes">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.</aside>
</section> </section>
<section class="slide image-slide" data-title="OSI"> <section class="slide image-slide" data-title="OSI">
<h2>WebSocket in the OSI Model</h2> <h2>WebSocket in the OSI Model</h2>
<p class="subtitle">Application semantics at Layer 7, reliable byte delivery through TCP/IP.</p> <p class="subtitle">Application semantics at Layer 7, reliable byte delivery through TCP/IP.</p>
<div class="single-visual glass-panel"> <div class="single-visual glass-panel">
<img src="assets/websocket-osi.svg" alt="WebSocket in OSI model" /> <img src="assets/websocket-osi.svg" alt="WebSocket in OSI model" />
</div> </div>
<aside class="notes">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.</aside> <aside class="notes">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.</aside>
</section> </section>
<section class="slide" data-title="HTTP Versions"> <section class="slide" data-title="HTTP Versions">
<h2>HTTP Versions in WebSocket</h2> <h2>HTTP Versions in WebSocket</h2>
<p class="subtitle">Different HTTP versions start WebSocket connections differently.</p> <p class="subtitle">Different HTTP versions start WebSocket connections differently.</p>
<div class="version-row point-cards detailed-version-row"> <div class="version-row point-cards detailed-version-row">
<article><span>HTTP/1.1</span><b>Upgrade + 101 Switching Protocols</b><ul><li>The browser sends a normal GET request with <code>Upgrade: websocket</code>.</li><li>The server replies <code>101 Switching Protocols</code> if it accepts.</li><li>After that, the same TCP connection carries WebSocket frames both ways.</li><li>This is the classic and easiest model to understand.</li></ul></article> <article><span>HTTP/1.1</span><b>Upgrade + 101 Switching Protocols</b><ul><li>The browser sends a normal GET request with <code>Upgrade: websocket</code>.</li><li>The server replies <code>101 Switching Protocols</code> if it accepts.</li><li>After that, the same TCP connection carries WebSocket frames both ways.</li><li>This is the classic and easiest model to understand.</li></ul></article>
<article><span>HTTP/2</span><b>Extended CONNECT over a Stream</b><ul><li>HTTP/2 does not use the old connection-level Upgrade flow.</li><li>It uses Extended CONNECT, so WebSocket traffic lives inside one HTTP/2 stream.</li><li>Other HTTP/2 streams can share the same underlying connection.</li><li>Support depends more on servers, clients, and proxies.</li></ul></article> <article><span>HTTP/2</span><b>Extended CONNECT over a Stream</b><ul><li>HTTP/2 does not use the old connection-level Upgrade flow.</li><li>It uses Extended CONNECT, so WebSocket traffic lives inside one HTTP/2 stream.</li><li>Other HTTP/2 streams can share the same underlying connection.</li><li>Support depends more on servers, clients, and proxies.</li></ul></article>
<article><span>HTTP/3</span><b>CONNECT over QUIC</b><ul><li>HTTP/3 runs on QUIC over UDP instead of TCP.</li><li>WebSocket uses a CONNECT-based mapping similar in idea to HTTP/2.</li><li>QUIC streams reduce transport-level head-of-line blocking.</li><li>It is modern, but the demo is easier to explain with HTTP/1.1.</li></ul></article> <article><span>HTTP/3</span><b>CONNECT over QUIC</b><ul><li>HTTP/3 runs on QUIC over UDP instead of TCP.</li><li>WebSocket uses a CONNECT-based mapping similar in idea to HTTP/2.</li><li>QUIC streams reduce transport-level head-of-line blocking.</li><li>It is modern, but the demo is easier to explain with HTTP/1.1.</li></ul></article>
</div> </div>
<aside class="notes">The project is best explained with HTTP/1.1 Upgrade. HTTP/2 and HTTP/3 support WebSocket through different mechanisms.</aside> <aside class="notes">The project is best explained with HTTP/1.1 Upgrade. HTTP/2 and HTTP/3 support WebSocket through different mechanisms.</aside>
</section> </section>
<section class="slide image-slide" data-title="FastAPI Architecture"> <section class="slide image-slide" data-title="FastAPI Architecture">
<h2>Project Architecture</h2> <h2>Project Architecture</h2>
<p class="subtitle">Browsers connect to one WebSocket endpoint; ConnectionManager groups sockets by room.</p> <p class="subtitle">Browsers connect to one WebSocket endpoint; ConnectionManager groups sockets by room.</p>
<div class="single-visual glass-panel"> <div class="single-visual glass-panel">
<img src="assets/chatroom-architecture.svg" alt="WebSocket chatroom architecture" /> <img src="assets/chatroom-architecture.svg" alt="WebSocket chatroom architecture" />
</div> </div>
<aside class="notes">Explain the code structure: FastAPI serves the frontend, the WebSocket endpoint accepts connections, and the manager stores active clients by room before broadcasting messages.</aside> <aside class="notes">Explain the code structure: FastAPI serves the frontend, the WebSocket endpoint accepts connections, and the manager stores active clients by room before broadcasting messages.</aside>
</section> </section>
<section class="slide" data-title="Takeaways"> <section class="slide code-slide" data-title="Backend Router">
<h2>Keynotes and Takeaways</h2> <h2>Backend Codes</h2>
<p class="subtitle">The essential points to remember from the project.</p> <p class="subtitle">WebSocket router setup in FastAPI.</p>
<div class="takeaway-grid point-cards"> <div class="code-window">
<article><b>HTTP is request/response</b><ul><li>Simple</li><li>Stateless</li><li>Not ideal for live push</li></ul></article> <div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
<article><b>Polling simulates real-time</b><ul><li>Easy</li><li>Wasteful</li><li>Interval-based latency</li></ul></article> <pre><code><span class="line"><span class="kw">import</span> json</span>
<article><b>WebSocket keeps a channel</b><ul><li>Persistent</li><li>Full-duplex</li><li>Low latency</li></ul></article> <span class="line"><span class="kw">from</span> datetime <span class="kw">import</span> datetime, timezone</span>
<article><b>Server design matters</b><ul><li>Connection state</li><li>Disconnect handling</li><li>Scaling strategy</li></ul></article> <span class="line"></span>
</div> <span class="line"><span class="kw">from</span> fastapi <span class="kw">import</span> APIRouter, WebSocket, WebSocketDisconnect</span>
<aside class="notes">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.</aside> <span class="line"></span>
</section> <span class="line"><span class="kw">from</span> app.websocket.manager <span class="kw">import</span> manager</span>
</main> <span class="line"></span>
<span class="line">router = APIRouter()</span></code></pre>
<div class="controls" aria-hidden="true"> </div>
<button id="prev" title="Previous">&lt;</button> <aside class="notes">You can start by explaining that this module owns the WebSocket endpoint and delegates connection storage to the shared manager.</aside>
<span id="counter">1 / 10</span> </section>
<button id="next" title="Next">&gt;</button>
</div> <section class="slide code-slide" data-title="Backend Payload">
<div class="hint">Left / Right to move - N for notes - F for fullscreen</div> <h2>Message Payload Shape</h2>
<div class="progress"><span id="progressBar"></span></div> <p class="subtitle">Every server message uses one small JSON structure.</p>
<div class="speaker-notes" id="speakerNotes"></div> <div class="code-window">
<script src="script.js"></script> <div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
</body> <pre><code><span class="line"><span class="kw">def</span> <span class="fn">now_iso</span>() -&gt; str:</span>
</html> <span class="line"> <span class="kw">return</span> datetime.now(timezone.utc).isoformat()</span>
<span class="line"></span>
<span class="line"><span class="kw">def</span> <span class="fn">event</span>(event_type, room_id, client_id, content):</span>
<span class="line"> <span class="kw">return</span> {</span>
<span class="line"> <span class="str">"type"</span>: event_type,</span>
<span class="line"> <span class="str">"room_id"</span>: room_id,</span>
<span class="line"> <span class="str">"client_id"</span>: client_id,</span>
<span class="line"> <span class="str">"content"</span>: content,</span>
<span class="line"> <span class="str">"timestamp"</span>: now_iso(),</span>
<span class="line"> }</span></code></pre>
</div>
<aside class="notes">This slide shows the server-to-client contract: message type, room, sender, content, and timestamp.</aside>
</section>
<section class="slide code-slide" data-title="Backend Endpoint">
<h2>WebSocket Endpoint</h2>
<p class="subtitle">The route parameters select the room and the user identity.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
<pre><code><span class="line">@router.websocket(<span class="str">"/ws/{room_id}/{client_id}"</span>)</span>
<span class="line"><span class="kw">async def</span> <span class="fn">websocket_endpoint</span>(</span>
<span class="line"> websocket: WebSocket,</span>
<span class="line"> room_id: str,</span>
<span class="line"> client_id: str,</span>
<span class="line">):</span>
<span class="line"> room_id = clean_value(room_id, <span class="str">"general"</span>)</span>
<span class="line"> client_id = clean_value(client_id, <span class="str">"anonymous"</span>)</span>
<span class="line"></span>
<span class="line"> <span class="kw">await</span> manager.connect(websocket, room_id, client_id)</span></code></pre>
</div>
<aside class="notes">The browser URL maps directly to this route. FastAPI receives the HTTP Upgrade request and the manager accepts the socket.</aside>
</section>
<section class="slide code-slide" data-title="Backend Presence">
<h2>Join and Presence Events</h2>
<p class="subtitle">After connection, the server informs the room and refreshes online users.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
<pre><code><span class="line"><span class="kw">await</span> manager.broadcast_json(</span>
<span class="line"> event(<span class="str">"system"</span>, room_id, <span class="str">"system"</span>, <span class="str">f"{client_id} joined {room_id}."</span>),</span>
<span class="line"> room_id,</span>
<span class="line">)</span>
<span class="line"></span>
<span class="line"><span class="kw">await</span> manager.broadcast_json({</span>
<span class="line"> <span class="str">"type"</span>: <span class="str">"presence"</span>,</span>
<span class="line"> <span class="str">"users"</span>: manager.room_users(room_id),</span>
<span class="line"> <span class="str">"timestamp"</span>: now_iso(),</span>
<span class="line">}, room_id)</span></code></pre>
</div>
<aside class="notes">Presence is separate from normal chat messages, so the frontend can update the sidebar without adding a chat bubble.</aside>
</section>
<section class="slide code-slide" data-title="Backend Receive Loop">
<h2>Receive Loop and Broadcast</h2>
<p class="subtitle">The socket stays open and every valid message is broadcast to the room.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
<pre><code><span class="line"><span class="kw">while</span> True:</span>
<span class="line"> raw_data = <span class="kw">await</span> websocket.receive_text()</span>
<span class="line"></span>
<span class="line"> <span class="kw">try</span>:</span>
<span class="line"> data = json.loads(raw_data)</span>
<span class="line"> message = str(data.get(<span class="str">"content"</span>, <span class="str">""</span>)).strip()</span>
<span class="line"> <span class="kw">except</span> json.JSONDecodeError:</span>
<span class="line"> message = raw_data.strip()</span>
<span class="line"></span>
<span class="line"> <span class="kw">if</span> message:</span>
<span class="line"> <span class="kw">await</span> manager.broadcast_json(event(<span class="str">"message"</span>, room_id, client_id, message), room_id)</span></code></pre>
</div>
<aside class="notes">This is the main real-time loop: receive text, parse JSON if possible, ignore empty messages, then broadcast.</aside>
</section>
<section class="slide code-slide" data-title="WebSocket Disconnect Handling">
<h2>Connection Teardown and Presence Update</h2>
<p class="subtitle">Handles user disconnects, cleans up the room, and updates presence for remaining clients.</p>
<div class="code-window">
<div class="code-titlebar">
<span></span><span></span><span></span>
<strong>app/websocket/chat.py</strong>
</div>
<pre><code><span class="line"><span class="kw">except</span> WebSocketDisconnect:</span>
<span class="line"> manager.disconnect(websocket, room_id)</span>
<span class="line"> <span class="kw">await</span> manager.broadcast_json(</span>
<span class="line"> event(<span class="str">"system"</span>, room_id, <span class="str">"سیستم"</span>, </span>
<span class="line"> f<span class="str">"{client_id} از اتاق {room_id} خارج شد."</span>),</span>
<span class="line"> room_id,</span>
<span class="line"> )</span>
<span class="line"> <span class="kw">await</span> manager.broadcast_json(</span>
<span class="line"> {</span>
<span class="line"> <span class="str">"type"</span>: <span class="str">"presence"</span>,</span>
<span class="line"> <span class="str">"room_id"</span>: room_id,</span>
<span class="line"> <span class="str">"client_id"</span>: <span class="str">"سیستم"</span>,</span>
<span class="line"> <span class="str">"content"</span>: <span class="str">"presence-updated"</span>,</span>
<span class="line"> <span class="str">"users"</span>: manager.room_users(room_id),</span>
<span class="line"> <span class="str">"timestamp"</span>: now_iso(),</span>
<span class="line"> },</span>
<span class="line"> room_id,</span>
<span class="line"> )</span></code></pre>
</div>
<aside class="notes">
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.
</aside>
</section>
<section class="slide code-slide" data-title="Backend Connection Manager">
<h2>ConnectionManager</h2>
<p class="subtitle">Manages WebSocket connections grouped by room and cleans up disconnected clients.</p>
<div class="code-window">
<div class="code-titlebar">
<span></span><span></span><span></span>
<strong>app/websocket/manager.py</strong>
</div>
<pre><code><span class="line"><span class="kw">class</span> <span class="fn">ConnectionManager</span>:</span>
<span class="line"> <span class="kw">def</span> <span class="fn">__init__</span>(self):</span>
<span class="line"> self.active_connections: dict[str, list[ClientConnection]] = {}</span>
<span class="line"></span>
<span class="line"> <span class="kw">async def</span> <span class="fn">connect</span>(self, websocket: WebSocket, room_id: str, client_id: str):</span>
<span class="line"> <span class="kw">await</span> websocket.accept()</span>
<span class="line"> self.active_connections.setdefault(room_id, [])</span>
<span class="line"> self.active_connections[room_id].append(ClientConnection(websocket=websocket, client_id=client_id))</span>
<span class="line"></span>
<span class="line"> <span class="kw">def</span> <span class="fn">disconnect</span>(self, websocket: WebSocket, room_id: str):</span>
<span class="line"> <span class="kw">if</span> room_id <span class="kw">not</span> <span class="kw">in</span> self.active_connections: <span class="kw">return</span></span>
<span class="line"></span>
<span class="line"> self.active_connections[room_id] = [</span>
<span class="line"> connection</span>
<span class="line"> <span class="kw">for</span> connection <span class="kw">in</span> self.active_connections[room_id] <span class="kw">if</span> connection.websocket <span class="kw">is</span> <span class="kw">not</span> websocket]</span>
<span class="line"></span>
<span class="line"> <span class="kw">if</span> <span class="kw">not</span> self.active_connections[room_id]:</span>
<span class="line"> <span class="kw">del</span> self.active_connections[room_id]</span></code></pre>
</div>
<aside class="notes">
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.
</aside>
</section>
<section class="slide code-slide" data-title="Backend Broadcast Manager">
<h2>Broadcast</h2>
<p class="subtitle">Sends messages to all clients in a room and removes failed WebSocket connections safely.</p>
<div class="code-window">
<div class="code-titlebar">
<span></span><span></span><span></span>
<strong>app/websocket/manager.py</strong>
</div>
<pre><code><span class="line"><span class="kw">class</span> <span class="fn">ConnectionManager</span>:</span>
<span class="line"></span>
<span class="line"> <span class="kw">async def</span> <span class="fn">broadcast_json</span>(self, payload: <span class="kw">dict</span>[str, Any], room_id: str):</span>
<span class="line"> <span class="kw">if</span> room_id <span class="kw">not</span> <span class="kw">in</span> self.active_connections:</span>
<span class="line"> <span class="kw">return</span></span>
<span class="line"></span>
<span class="line"> dead_connections: <span class="kw">list</span>[WebSocket] = []</span>
<span class="line"></span>
<span class="line"> <span class="com"># Iterate over a copy so disconnected clients can be removed safely later.</span></span>
<span class="line"> <span class="kw">for</span> connection <span class="kw">in</span> <span class="kw">list</span>(self.active_connections[room_id]):</span>
<span class="line"> <span class="kw">try</span>:</span>
<span class="line"> <span class="kw">await</span> connection.websocket.send_json(payload)</span>
<span class="line"> <span class="kw">except</span> Exception:</span>
<span class="line"> dead_connections.append(connection.websocket)</span>
<span class="line"></span>
<span class="line"> <span class="kw">for</span> websocket <span class="kw">in</span> dead_connections:</span>
<span class="line"> self.disconnect(websocket, room_id)</span></code></pre>
</div>
<aside class="notes">
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.
</aside>
</section>
<section class="slide code-slide" data-title="Frontend State">
<h2>Frontend Codes</h2>
<p class="subtitle">browser state and DOM references.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line"><span class="kw">const</span> usernameInput = document.querySelector(<span class="str">'#username'</span>);</span>
<span class="line"><span class="kw">const</span> roomInput = document.querySelector(<span class="str">'#room'</span>);</span>
<span class="line"><span class="kw">const</span> connectBtn = document.querySelector(<span class="str">'#connectBtn'</span>);</span>
<span class="line"><span class="kw">const</span> messageForm = document.querySelector(<span class="str">'#messageForm'</span>);</span>
<span class="line"><span class="kw">const</span> messages = document.querySelector(<span class="str">'#messages'</span>);</span>
<span class="line"><span class="kw">const</span> usersList = document.querySelector(<span class="str">'#usersList'</span>);</span>
<span class="line"></span>
<span class="line"><span class="kw">let</span> socket = <span class="num">null</span>;</span>
<span class="line"><span class="kw">let</span> currentUser = <span class="str">''</span>;</span>
<span class="line"><span class="kw">let</span> currentRoom = <span class="str">''</span>;</span></code></pre>
</div>
<aside class="notes">You can introduce the frontend file by showing that the UI stores one WebSocket object and the current room/user state.</aside>
</section>
<section class="slide code-slide" data-title="Frontend URL">
<h2>Building the WebSocket URL</h2>
<p class="subtitle">The browser automatically switches between ws and wss.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line"><span class="kw">function</span> <span class="fn">connect</span>() {</span>
<span class="line"> currentUser = safeName(usernameInput.value, <span class="str">'anonymous'</span>);</span>
<span class="line"> currentRoom = safeName(roomInput.value, <span class="str">'general'</span>);</span>
<span class="line"></span>
<span class="line"> <span class="kw">const</span> wsProtocol = window.location.protocol === <span class="str">'https:'</span> ? <span class="str">'wss'</span> : <span class="str">'ws'</span>;</span>
<span class="line"> <span class="kw">const</span> room = encodeURIComponent(currentRoom);</span>
<span class="line"> <span class="kw">const</span> user = encodeURIComponent(currentUser);</span>
<span class="line"> <span class="kw">const</span> wsUrl = <span class="str">`${wsProtocol}://${window.location.host}/ws/${room}/${user}`</span>;</span>
<span class="line"></span>
<span class="line"> socket = <span class="kw">new</span> WebSocket(wsUrl);</span>
<span class="line">}</span></code></pre>
</div>
<aside class="notes">This is why the same frontend works locally and behind HTTPS on the server.</aside>
</section>
<section class="slide code-slide" data-title="Frontend Open Event">
<h2>Connection Open Handler</h2>
<p class="subtitle">When the socket opens, the UI moves into connected mode.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line">socket.addEventListener(<span class="str">'open'</span>, () =&gt; {</span>
<span class="line"> setConnectedState(<span class="num">true</span>);</span>
<span class="line"></span>
<span class="line"> roomTitle.textContent = <span class="str">`Room: ${currentRoom}`</span>;</span>
<span class="line"> roomSubtitle.textContent = <span class="str">`You are chatting as ${currentUser}.`</span>;</span>
<span class="line"></span>
<span class="line"> messageInput.focus();</span>
<span class="line">});</span></code></pre>
</div>
<aside class="notes">This slide connects WebSocket state to UI state: buttons, labels, and the message input change after the connection opens.</aside>
</section>
<section class="slide code-slide" data-title="Frontend Message Event">
<h2>Handling Incoming Messages</h2>
<p class="subtitle">Presence updates and chat messages are separated by type.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line">socket.addEventListener(<span class="str">'message'</span>, (event) =&gt; {</span>
<span class="line"> <span class="kw">const</span> payload = JSON.parse(event.data);</span>
<span class="line"></span>
<span class="line"> <span class="kw">if</span> (payload.type === <span class="str">'presence'</span>) {</span>
<span class="line"> updateUsers(payload.users || []);</span>
<span class="line"> <span class="kw">return</span>;</span>
<span class="line"> }</span>
<span class="line"></span>
<span class="line"> addMessage(payload);</span>
<span class="line">});</span></code></pre>
</div>
<aside class="notes">The frontend treats presence as sidebar data and normal messages as chat feed data.</aside>
</section>
<section class="slide code-slide" data-title="Frontend Render">
<h2>Rendering a Chat Message</h2>
<p class="subtitle">The browser creates message elements without reloading the page.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line"><span class="kw">function</span> <span class="fn">addMessage</span>(payload) {</span>
<span class="line"> clearEmptyState();</span>
<span class="line"></span>
<span class="line"> <span class="kw">const</span> card = document.createElement(<span class="str">'article'</span>);</span>
<span class="line"> card.className = <span class="str">'message'</span>;</span>
<span class="line"></span>
<span class="line"> <span class="kw">if</span> (payload.client_id === currentUser) card.classList.add(<span class="str">'mine'</span>);</span>
<span class="line"></span>
<span class="line"> content.textContent = payload.content || <span class="str">''</span>;</span>
<span class="line"> card.append(meta, content);</span>
<span class="line"> messages.appendChild(card);</span>
<span class="line"> messages.scrollTop = messages.scrollHeight;</span>
<span class="line">}</span></code></pre>
</div>
<aside class="notes">This demonstrates DOM rendering and auto-scrolling. The UI changes because WebSocket data arrives, not because the page refreshes.</aside>
</section>
<section class="slide code-slide" data-title="Frontend Send">
<h2>Sending Messages</h2>
<p class="subtitle">The form sends JSON only when the socket is open.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line">messageForm.addEventListener(<span class="str">'submit'</span>, (event) =&gt; {</span>
<span class="line"> event.preventDefault();</span>
<span class="line"></span>
<span class="line"> <span class="kw">const</span> content = messageInput.value.trim();</span>
<span class="line"> <span class="kw">if</span> (!content || !socket || socket.readyState !== WebSocket.OPEN) <span class="kw">return</span>;</span>
<span class="line"></span>
<span class="line"> socket.send(JSON.stringify({ content }));</span>
<span class="line"> messageInput.value = <span class="str">''</span>;</span>
<span class="line"> messageInput.focus();</span>
<span class="line">});</span></code></pre>
</div>
<aside class="notes">This is the browser-to-server direction. The server receives this JSON and broadcasts the content to the room.</aside>
</section>
<section class="slide" data-title="Takeaways">
<h2>Keynotes and Takeaways</h2>
<p class="subtitle">The essential points to remember from the project.</p>
<div class="takeaway-grid point-cards">
<article><b>HTTP is request/response</b><ul><li>Simple</li><li>Stateless</li><li>Not ideal for live push</li></ul></article>
<article><b>Polling simulates real-time</b><ul><li>Easy</li><li>Wasteful</li><li>Interval-based latency</li></ul></article>
<article><b>WebSocket keeps a channel</b><ul><li>Persistent</li><li>Full-duplex</li><li>Low latency</li></ul></article>
<article><b>Server design matters</b><ul><li>Connection state</li><li>Disconnect handling</li><li>Scaling strategy</li></ul></article>
</div>
<aside class="notes">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.</aside>
</section>
</main>
<div class="controls" aria-hidden="true">
<button id="prev" title="Previous">&lt;</button>
<span id="counter">1 / 10</span>
<button id="next" title="Next">&gt;</button>
</div>
<div class="hint">Left / Right to move - N for notes - F for fullscreen</div>
<div class="progress"><span id="progressBar"></span></div>
<div class="speaker-notes" id="speakerNotes"></div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: "Vazirmatn"; font-family: "Vazirmatn";
src: url("../../app/static/Vazirmatn[wght].woff2") format("woff2"); src: url("Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900; font-weight: 100 900;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -93,7 +93,8 @@ body {
text-align: center; text-align: center;
} }
.slide.active.protocol-slide, .slide.active.protocol-slide,
.slide.active.image-slide { .slide.active.image-slide,
.slide.active.code-slide {
display: grid; display: grid;
grid-template-rows: auto auto minmax(0, 1fr); grid-template-rows: auto auto minmax(0, 1fr);
} }
@@ -131,7 +132,8 @@ h2 { font-size: clamp(27px, calc(46px * var(--slide-scale)), 66px); }
letter-spacing: .01em; letter-spacing: .01em;
} }
.protocol-slide .subtitle, .protocol-slide .subtitle,
.image-slide .subtitle { .image-slide .subtitle,
.code-slide .subtitle {
margin-bottom: clamp(10px, calc(14px * var(--slide-scale)), 22px); margin-bottom: clamp(10px, calc(14px * var(--slide-scale)), 22px);
} }
@@ -635,6 +637,89 @@ pre {
} }
.ltr { direction: ltr; text-align: left; } .ltr { direction: ltr; text-align: left; }
.code-window {
min-height: 0;
border-radius: clamp(10px, calc(14px * var(--slide-scale)), 18px);
background: #0d1117;
border: 1px solid rgba(255,255,255,.13);
box-shadow: 0 24px 70px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.08);
overflow: hidden;
direction: ltr;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
.code-titlebar {
height: clamp(38px, calc(44px * var(--slide-scale)), 56px);
background: linear-gradient(180deg, #1f2430, #151a24);
border-bottom: 1px solid rgba(255,255,255,.09);
display: flex;
align-items: center;
gap: 8px;
padding: 0 clamp(14px, calc(18px * var(--slide-scale)), 26px);
}
.code-titlebar span {
width: clamp(10px, calc(12px * var(--slide-scale)), 15px);
height: clamp(10px, calc(12px * var(--slide-scale)), 15px);
border-radius: 50%;
display: block;
}
.code-titlebar span:nth-child(1) { background: #ff5f57; }
.code-titlebar span:nth-child(2) { background: #febc2e; }
.code-titlebar span:nth-child(3) { background: #28c840; }
.code-titlebar strong {
margin-left: 10px;
color: #c9d1d9;
font-family: "Cascadia Code", "Consolas", "SFMono-Regular", monospace;
font-size: clamp(12px, calc(14px * var(--slide-scale)), 19px);
font-weight: 600;
}
.code-window pre {
min-height: 0;
overflow: hidden;
background:
linear-gradient(90deg, rgba(125, 211, 252, .035), transparent 36%),
#0d1117;
border-radius: 0;
box-shadow: none;
padding: clamp(14px, calc(20px * var(--slide-scale)), 28px) 0;
white-space: pre;
font-size: clamp(11px, calc(14px * var(--slide-scale)), 19px);
line-height: 0;
}
.code-window code {
display: block;
background: transparent;
border-radius: 0;
color: #d6deeb;
padding: 0;
font-family: "Cascadia Code", "Consolas", "SFMono-Regular", monospace;
direction: ltr;
unicode-bidi: normal;
}
.code-window .line {
counter-increment: code-line;
display: block;
min-height: 1.55em;
line-height: 1.45;
padding: 0 clamp(18px, calc(28px * var(--slide-scale)), 42px) 0 0;
}
.code-window code {
counter-reset: code-line;
}
.code-window .line::before {
content: counter(code-line);
display: inline-block;
width: clamp(34px, calc(44px * var(--slide-scale)), 56px);
margin-right: clamp(12px, calc(18px * var(--slide-scale)), 26px);
color: #6e7681;
text-align: right;
user-select: none;
}
.code-window .kw { color: #ff7b72; }
.code-window .fn { color: #d2a8ff; }
.code-window .str { color: #a5d6ff; }
.code-window .num { color: #79c0ff; }
.version-row { .version-row {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@@ -666,7 +751,7 @@ pre {
} }
.detailed-version-row li { .detailed-version-row li {
font-size: clamp(10px, calc(13px * var(--slide-scale)), 17px); font-size: clamp(10px, calc(13px * var(--slide-scale)), 17px);
line-height: 1.42; /* line-height: 1.42; */
} }
.detailed-version-row code { .detailed-version-row code {
color: #ffe8aa; color: #ffe8aa;
@@ -1007,7 +1092,8 @@ code {
.subtitle { font-size: clamp(14px, 4vw, 18px); } .subtitle { font-size: clamp(14px, 4vw, 18px); }
.kicker { font-size: clamp(12px, 3.7vw, 16px); } .kicker { font-size: clamp(12px, 3.7vw, 16px); }
.slide.active.protocol-slide, .slide.active.protocol-slide,
.slide.active.image-slide { display: block; } .slide.active.image-slide,
.slide.active.code-slide { display: block; }
.hero-grid, .split, .demo-layout, .version-row, .resource-grid, .mechanism-grid, .takeaway-grid { grid-template-columns: 1fr; } .hero-grid, .split, .demo-layout, .version-row, .resource-grid, .mechanism-grid, .takeaway-grid { grid-template-columns: 1fr; }
.takeaway-grid { grid-template-rows: none; min-height: 0; } .takeaway-grid { grid-template-rows: none; min-height: 0; }
.hero-grid, .split, .demo-layout, .mechanism-grid { gap: 18px; } .hero-grid, .split, .demo-layout, .mechanism-grid { gap: 18px; }
@@ -1047,6 +1133,19 @@ code {
.mini-points span { font-size: 12px; padding: 7px 10px; } .mini-points span { font-size: 12px; padding: 7px 10px; }
.quote { font-size: 15px; } .quote { font-size: 15px; }
pre { font-size: clamp(10px, 2.8vw, 13px); padding: 14px; } pre { font-size: clamp(10px, 2.8vw, 13px); padding: 14px; }
.code-window { display: block; }
.code-window pre {
overflow-x: auto;
font-size: clamp(10px, 2.7vw, 13px);
padding: 12px 0;
}
.code-window .line {
padding-right: 14px;
}
.code-window .line::before {
width: 32px;
margin-right: 12px;
}
ol { font-size: 14px; line-height: 1.9; } ol { font-size: 14px; line-height: 1.9; }
.controls { left: 14px; bottom: 14px; } .controls { left: 14px; bottom: 14px; }
.controls button { width: 36px; height: 36px; font-size: 24px; } .controls button { width: 36px; height: 36px; font-size: 24px; }