Compare commits

...

9 Commits

21 changed files with 2346 additions and 1 deletions

16
.dockerignore Normal file
View File

@@ -0,0 +1,16 @@
.git/
.venv/
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
.mypy_cache/
.ruff_cache/
.uvicorn.pid
docs/images/
docs/screenshots/
docs/*.pdf
docs/presentation.md
README.md

85
DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,85 @@
# Demo Deployment
This project can run behind the existing Caddy edge service with this temporary route:
```text
Caddy -> chatroom-nginx -> chatroom-app
```
The chatroom compose stack joins the existing external Docker network named `caddy_proxy`. No app ports are published to the host.
## Start the Chatroom Stack
From this project directory:
```powershell
docker compose up -d --build
docker compose ps
```
Test the FastAPI health endpoint through the Nginx container:
```powershell
docker compose exec chatroom-nginx wget -qO- http://chatroom-app:8000/health
```
## Temporary Caddy Change
Do this manually in:
```text
D:\Programing\Scripts\Home Lab\Interanet\caddy-deployment\config\caddy\Caddyfile
```
Add this temporary block:
```caddy
chat.amiirkhl.ir {
import {$CADDY_TLS_MODE:custom_tls}
reverse_proxy chatroom-nginx:80
}
```
If the current custom certificate does not include `chat.amiirkhl.ir`, use this instead:
```caddy
chat.amiirkhl.ir {
import letsencrypt_tls
reverse_proxy chatroom-nginx:80
}
```
Reload Caddy from the Caddy deployment directory:
```powershell
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
```
## Browser Test
Open:
```text
https://chat.amiirkhl.ir
```
Use two browser tabs, join the same room, and send messages both ways. The frontend builds its WebSocket URL from the current page, so HTTPS should connect with:
```text
wss://chat.amiirkhl.ir/ws/{room}/{user}
```
## Undo After the Demo
1. Remove the `chat.amiirkhl.ir` block from the Caddyfile.
2. Reload Caddy:
```powershell
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
```
3. Stop and remove the chatroom stack from this project directory:
```powershell
docker compose down
```

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY 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 docs/slides ./docs/slides
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips", "*"]

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."""

30
docker-compose.yml Normal file
View File

@@ -0,0 +1,30 @@
services:
chatroom-app:
build:
context: .
image: chatroom-app:demo
restart: unless-stopped
expose:
- "8000"
networks:
- chatroom_internal
chatroom-nginx:
image: nginx:alpine
restart: unless-stopped
depends_on:
- chatroom-app
expose:
- "80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- chatroom_internal
- caddy_proxy
networks:
chatroom_internal:
driver: bridge
caddy_proxy:
external: true
name: caddy_proxy

8
docs/slides/README.md Normal file
View File

@@ -0,0 +1,8 @@
# WebSocket Presentation
- Open `index.html` in a browser.
- Use Left/Right arrows to move between slides.
- Press `N` to show speaker notes.
- Press `F` for fullscreen mode.
- The deck uses the local Vazirmatn font file.
- Visible slide content is limited to titles, subtitles, diagrams, images, and bullet points.

Binary file not shown.

View File

@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540" role="img" aria-labelledby="title desc">
<title id="title">Chatroom app mockup</title>
<desc id="desc">English mockup of the WebSocket chatroom user interface.</desc>
<defs>
<style>
@font-face {
font-family: "Vazirmatn";
src: url("../../../app/static/Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900;
}
text { font-family: "Vazirmatn", Arial, sans-serif; }
</style>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#0f172a" flood-opacity=".16"/>
</filter>
</defs>
<rect width="960" height="540" fill="#262a3a"/>
<path d="M42 430 L260 322 L358 488 L96 548 Z" fill="#f4b740" opacity=".13"/>
<path d="M720 34 L930 96 L820 228 L640 140 Z" fill="#6d8cff" opacity=".14"/>
<g id="diagram-content-shift" transform="translate(0,-28)">
<g filter="url(#shadow)">
<rect x="88" y="74" width="784" height="392" rx="28" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
</g>
<text x="132" y="122" fill="#f8fafc" font-size="28" font-weight="800">Simple WebSocket Chatroom</text>
<rect x="710" y="98" width="96" height="34" rx="17" fill="#43d9a3" opacity=".22" stroke="#43d9a3"/>
<text x="758" y="121" text-anchor="middle" fill="#9ff4d5" font-size="14" font-weight="800">Connected</text>
<rect x="132" y="164" width="496" height="232" rx="18" fill="#ffffff" opacity=".07" stroke="#ffffff"/>
<text x="162" y="204" fill="#f8fafc" font-size="20" font-weight="800">Room: internet-engineering</text>
<text x="162" y="232" fill="#a8adbd" font-size="15">You are chatting as Ali.</text>
<rect x="230" y="266" width="244" height="48" rx="14" fill="#ffffff" opacity=".10" stroke="#ffffff"/>
<text x="352" y="296" text-anchor="middle" fill="#f8fafc" font-size="15">Hello from WebSocket!</text>
<rect x="362" y="332" width="210" height="48" rx="14" fill="#6d8cff" opacity=".32" stroke="#6d8cff"/>
<text x="467" y="362" text-anchor="middle" fill="#f8fafc" font-size="15">Instant delivery</text>
<rect x="132" y="412" width="496" height="34" rx="12" fill="#ffffff" opacity=".09" stroke="#ffffff"/>
<text x="162" y="434" fill="#a8adbd" font-size="14">Type a message...</text>
<rect x="548" y="412" width="80" height="34" rx="12" fill="#f4b740"/>
<text x="588" y="434" text-anchor="middle" fill="#1f2937" font-size="14" font-weight="800">Send</text>
<rect x="660" y="164" width="166" height="282" rx="18" fill="#ffffff" opacity=".07" stroke="#ffffff"/>
<text x="743" y="204" text-anchor="middle" fill="#f8fafc" font-size="20" font-weight="800">Online Users</text>
<text x="704" y="252" fill="#a8adbd" font-size="16">Ali</text>
<text x="704" y="292" fill="#a8adbd" font-size="16">Sara</text>
<text x="704" y="332" fill="#a8adbd" font-size="16">Reza</text>
<circle cx="684" cy="246" r="6" fill="#43d9a3"/>
<circle cx="684" cy="286" r="6" fill="#43d9a3"/>
<circle cx="684" cy="326" r="6" fill="#43d9a3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500" viewBox="0 0 1200 500" role="img" aria-labelledby="title desc">
<title id="title">Chatroom architecture</title>
<desc id="desc">A compact architecture diagram for the FastAPI WebSocket chatroom project.</desc>
<defs>
<style>
@font-face {
font-family: "Vazirmatn";
src: url("../../../app/static/Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900;
}
text { font-family: "Vazirmatn", Arial, sans-serif; }
.title { fill: #f8fafc; font-size: 38px; font-weight: 850; }
.sub { fill: #a8adbd; font-size: 18px; font-weight: 500; }
.node-title { fill: #f8fafc; font-size: 22px; font-weight: 850; }
.node-sub { fill: #a8adbd; font-size: 16px; }
.label { font-size: 16px; font-weight: 850; }
</style>
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#2563eb"/></marker>
<marker id="arrow-green" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#10b981"/></marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#0f172a" flood-opacity=".16"/>
</filter>
</defs>
<rect width="1200" height="500" fill="#262a3a"/>
<path d="M42 396 L258 286 L374 438 L92 532 Z" fill="#f4b740" opacity=".13"/>
<path d="M880 42 L1146 112 L1030 262 L812 182 Z" fill="#6d8cff" opacity=".16"/>
<g id="diagram-content-shift" transform="translate(0,-28)">
<g filter="url(#shadow)">
<rect x="106" y="174" width="236" height="88" rx="22" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
<rect x="106" y="306" width="236" height="88" rx="22" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
<rect x="476" y="164" width="248" height="230" rx="28" fill="#2563eb"/>
<rect x="858" y="164" width="236" height="96" rx="24" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
<rect x="858" y="316" width="236" height="96" rx="24" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
</g>
<text x="224" y="212" text-anchor="middle" class="node-title">Client A</text>
<text x="224" y="240" text-anchor="middle" class="node-sub">Browser tab</text>
<text x="224" y="344" text-anchor="middle" class="node-title">Client B</text>
<text x="224" y="372" text-anchor="middle" class="node-sub">Browser tab</text>
<text x="600" y="232" text-anchor="middle" fill="#ffffff" font-size="30" font-weight="850">Server</text>
<text x="600" y="272" text-anchor="middle" fill="#dbeafe" font-size="17">WebSocket Endpoint</text>
<text x="600" y="306" text-anchor="middle" fill="#dbeafe" font-size="17">ConnectionManager</text>
<text x="600" y="342" text-anchor="middle" fill="#ffffff" font-size="17" font-weight="850">/ws/{room}/{user}</text>
<text x="976" y="204" text-anchor="middle" class="node-title">Room: general</text>
<text x="976" y="234" text-anchor="middle" class="node-sub">A - B - C</text>
<text x="976" y="356" text-anchor="middle" class="node-title">Room: class</text>
<text x="976" y="386" text-anchor="middle" class="node-sub">D - E</text>
<path d="M342 218 C408 218 428 232 476 232" fill="none" stroke="#2563eb" stroke-width="5" marker-end="url(#arrow)"/>
<path d="M342 350 C408 350 428 326 476 326" fill="none" stroke="#2563eb" stroke-width="5" marker-end="url(#arrow)"/>
<path d="M724 238 C790 210 812 212 858 212" fill="none" stroke="#10b981" stroke-width="5" marker-end="url(#arrow-green)"/>
<path d="M724 320 C790 354 812 364 858 364" fill="none" stroke="#10b981" stroke-width="5" marker-end="url(#arrow-green)"/>
<text x="376" y="188" fill="#f7c75f" class="label">WebSocket Frames</text>
<text x="778" y="164" fill="#43d9a3" class="label">Broadcast by room</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
docs/slides/assets/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500" viewBox="0 0 1200 500" role="img" aria-labelledby="title desc">
<title id="title">HTTP mechanism</title>
<desc id="desc">A short HTTP request response exchange.</desc>
<defs>
<style>
@font-face {
font-family: "Vazirmatn";
src: url("../../../app/static/Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900;
}
text { font-family: "Vazirmatn", Arial, sans-serif; }
.title { fill: #f8fafc; font-size: 38px; font-weight: 850; }
.sub { fill: #a8adbd; font-size: 18px; font-weight: 500; }
.node-title { fill: #f8fafc; font-size: 25px; font-weight: 850; }
.node-sub { fill: #a8adbd; font-size: 16px; }
.label { font-size: 18px; font-weight: 850; }
</style>
<marker id="arrow-blue" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#2563eb"/></marker>
<marker id="arrow-green" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#10b981"/></marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#0f172a" flood-opacity=".16"/>
</filter>
</defs>
<rect width="1200" height="500" fill="#262a3a"/>
<path d="M42 396 L258 286 L374 438 L92 532 Z" fill="#f4b740" opacity=".13"/>
<path d="M880 42 L1146 112 L1030 262 L812 182 Z" fill="#6d8cff" opacity=".16"/>
<g id="diagram-content-shift" transform="translate(0,-28)">
<g filter="url(#shadow)">
<rect x="92" y="166" width="250" height="132" rx="26" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
<rect x="858" y="166" width="250" height="132" rx="26" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
</g>
<text x="217" y="220" text-anchor="middle" class="node-title">Browser</text>
<text x="217" y="254" text-anchor="middle" class="node-sub">HTTP Client</text>
<text x="983" y="220" text-anchor="middle" class="node-title">Server</text>
<text x="983" y="254" text-anchor="middle" class="node-sub">HTTP Endpoint</text>
<line x1="372" y1="188" x2="828" y2="188" stroke="#2563eb" stroke-width="5" marker-end="url(#arrow-blue)"/>
<text x="600" y="166" text-anchor="middle" fill="#f7c75f" class="label">1. GET /resource</text>
<line x1="828" y1="252" x2="372" y2="252" stroke="#10b981" stroke-width="5" marker-end="url(#arrow-green)"/>
<text x="600" y="236" text-anchor="middle" fill="#43d9a3" class="label">2. 200 OK Response</text>
<line x1="372" y1="304" x2="828" y2="304" stroke="#fb7185" stroke-width="4" stroke-dasharray="12 10"/>
<text x="600" y="334" text-anchor="middle" fill="#fb7185" class="label">3. Connection closes</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500" viewBox="0 0 1200 500" role="img" aria-labelledby="title desc">
<title id="title">Long Polling mechanism</title>
<desc id="desc">An HTTP request held by the server until data is ready.</desc>
<defs>
<style>
@font-face {
font-family: "Vazirmatn";
src: url("../../../app/static/Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900;
}
text { font-family: "Vazirmatn", Arial, sans-serif; }
.title { fill: #f8fafc; font-size: 38px; font-weight: 850; }
.sub { fill: #a8adbd; font-size: 18px; font-weight: 500; }
.node-title { fill: #f8fafc; font-size: 25px; font-weight: 850; }
.node-sub { fill: #a8adbd; font-size: 16px; }
.label { font-size: 18px; font-weight: 850; }
</style>
<marker id="arrow-blue" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#2563eb"/></marker>
<marker id="arrow-green" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#10b981"/></marker>
<marker id="arrow-purple" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#7c3aed"/></marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#0f172a" flood-opacity=".16"/>
</filter>
</defs>
<rect width="1200" height="500" fill="#262a3a"/>
<path d="M42 396 L258 286 L374 438 L92 532 Z" fill="#f4b740" opacity=".13"/>
<path d="M880 42 L1146 112 L1030 262 L812 182 Z" fill="#6d8cff" opacity=".16"/>
<g id="diagram-content-shift" transform="translate(0,-28)">
<g filter="url(#shadow)">
<rect x="92" y="166" width="250" height="132" rx="26" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
<rect x="858" y="166" width="250" height="132" rx="26" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
</g>
<text x="217" y="220" text-anchor="middle" class="node-title">Browser</text>
<text x="217" y="254" text-anchor="middle" class="node-sub">Waiting request</text>
<text x="983" y="220" text-anchor="middle" class="node-title">Server</text>
<text x="983" y="254" text-anchor="middle" class="node-sub">Holds response</text>
<line x1="372" y1="174" x2="828" y2="174" stroke="#2563eb" stroke-width="5" marker-end="url(#arrow-blue)"/>
<text x="600" y="152" text-anchor="middle" fill="#f7c75f" class="label">1. HTTP Request</text>
<line x1="468" y1="230" x2="732" y2="230" stroke="#f4b740" stroke-width="8" stroke-linecap="round" stroke-dasharray="14 12"/>
<text x="600" y="216" text-anchor="middle" fill="#f7c75f" class="label">2. Hold Open</text>
<line x1="828" y1="292" x2="372" y2="292" stroke="#10b981" stroke-width="5" marker-end="url(#arrow-green)"/>
<text x="600" y="278" text-anchor="middle" fill="#43d9a3" class="label">3. Message Response</text>
<line x1="372" y1="332" x2="828" y2="332" stroke="#7c3aed" stroke-width="4" stroke-dasharray="10 9" marker-end="url(#arrow-purple)"/>
<text x="600" y="362" text-anchor="middle" fill="#aebdff" class="label">4. New Request</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,49 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500" viewBox="0 0 1200 500" role="img" aria-labelledby="title desc">
<title id="title">Polling mechanism</title>
<desc id="desc">Repeated HTTP requests on a timer.</desc>
<defs>
<style>
@font-face {
font-family: "Vazirmatn";
src: url("../../../app/static/Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900;
}
text { font-family: "Vazirmatn", Arial, sans-serif; }
.title { fill: #f8fafc; font-size: 38px; font-weight: 850; }
.sub { fill: #a8adbd; font-size: 18px; font-weight: 500; }
.node-title { fill: #f8fafc; font-size: 25px; font-weight: 850; }
.node-sub { fill: #a8adbd; font-size: 16px; }
.label { font-size: 17px; font-weight: 850; }
</style>
<marker id="arrow-blue" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#2563eb"/></marker>
<marker id="arrow-green" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#10b981"/></marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#0f172a" flood-opacity=".16"/>
</filter>
</defs>
<rect width="1200" height="500" fill="#262a3a"/>
<path d="M42 396 L258 286 L374 438 L92 532 Z" fill="#f4b740" opacity=".13"/>
<path d="M880 42 L1146 112 L1030 262 L812 182 Z" fill="#6d8cff" opacity=".16"/>
<g id="diagram-content-shift" transform="translate(0,-28)">
<g filter="url(#shadow)">
<rect x="92" y="166" width="250" height="132" rx="26" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
<rect x="858" y="166" width="250" height="132" rx="26" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
</g>
<text x="217" y="220" text-anchor="middle" class="node-title">Browser</text>
<text x="217" y="254" text-anchor="middle" class="node-sub">Timer based checks</text>
<text x="983" y="220" text-anchor="middle" class="node-title">Server</text>
<text x="983" y="254" text-anchor="middle" class="node-sub">HTTP Endpoint</text>
<line x1="372" y1="146" x2="828" y2="146" stroke="#2563eb" stroke-width="5" marker-end="url(#arrow-blue)"/>
<text x="600" y="132" text-anchor="middle" fill="#f7c75f" class="label">Request 1</text>
<line x1="828" y1="182" x2="372" y2="182" stroke="#10b981" stroke-width="5" marker-end="url(#arrow-green)"/>
<text x="600" y="176" text-anchor="middle" fill="#43d9a3" class="label">Empty Response</text>
<line x1="372" y1="224" x2="828" y2="224" stroke="#2563eb" stroke-width="5" marker-end="url(#arrow-blue)"/>
<text x="600" y="218" text-anchor="middle" fill="#f7c75f" class="label">Request 2</text>
<line x1="828" y1="260" x2="372" y2="260" stroke="#10b981" stroke-width="5" marker-end="url(#arrow-green)"/>
<text x="600" y="254" text-anchor="middle" fill="#43d9a3" class="label">Empty Response</text>
<line x1="372" y1="302" x2="828" y2="302" stroke="#2563eb" stroke-width="5" marker-end="url(#arrow-blue)"/>
<text x="600" y="296" text-anchor="middle" fill="#f7c75f" class="label">Request 3</text>
<line x1="828" y1="338" x2="372" y2="338" stroke="#10b981" stroke-width="5" marker-end="url(#arrow-green)"/>
<text x="600" y="332" text-anchor="middle" fill="#43d9a3" class="label">Message Response</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500" viewBox="0 0 1200 500" role="img" aria-labelledby="title desc">
<title id="title">WebSocket handshake</title>
<desc id="desc">A compact HTTP Upgrade handshake diagram.</desc>
<defs>
<style>
@font-face {
font-family: "Vazirmatn";
src: url("../../../app/static/Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900;
}
text { font-family: "Vazirmatn", Arial, sans-serif; }
.title { fill: #f8fafc; font-size: 38px; font-weight: 850; }
.sub { fill: #a8adbd; font-size: 18px; font-weight: 500; }
.node-title { fill: #f8fafc; font-size: 25px; font-weight: 850; }
.node-sub { fill: #a8adbd; font-size: 16px; }
.label { font-size: 18px; font-weight: 850; }
</style>
<marker id="arrow-blue" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#2563eb"/></marker>
<marker id="arrow-green" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#10b981"/></marker>
<marker id="arrow-purple" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M2,2 L10,6 L2,10 Z" fill="#7c3aed"/></marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#0f172a" flood-opacity=".16"/>
</filter>
</defs>
<rect width="1200" height="500" fill="#262a3a"/>
<path d="M42 396 L258 286 L374 438 L92 532 Z" fill="#f4b740" opacity=".13"/>
<path d="M880 42 L1146 112 L1030 262 L812 182 Z" fill="#6d8cff" opacity=".16"/>
<g id="diagram-content-shift" transform="translate(0,-28)">
<g filter="url(#shadow)">
<rect x="92" y="166" width="250" height="132" rx="26" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
<rect x="858" y="166" width="250" height="132" rx="26" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
</g>
<text x="217" y="220" text-anchor="middle" class="node-title">Browser</text>
<text x="217" y="254" text-anchor="middle" class="node-sub">JavaScript WebSocket</text>
<text x="983" y="220" text-anchor="middle" class="node-title">Server</text>
<text x="983" y="254" text-anchor="middle" class="node-sub">/ws/{room}/{user}</text>
<line x1="372" y1="176" x2="828" y2="176" stroke="#2563eb" stroke-width="5" marker-end="url(#arrow-blue)"/>
<text x="600" y="154" text-anchor="middle" fill="#f7c75f" class="label">1. GET + Upgrade</text>
<line x1="828" y1="240" x2="372" y2="240" stroke="#10b981" stroke-width="5" marker-end="url(#arrow-green)"/>
<text x="600" y="224" text-anchor="middle" fill="#43d9a3" class="label">2. 101 Switching Protocols</text>
<line x1="372" y1="298" x2="828" y2="298" stroke="#7c3aed" stroke-width="5" stroke-dasharray="10 9" marker-end="url(#arrow-purple)"/>
<line x1="828" y1="336" x2="372" y2="336" stroke="#7c3aed" stroke-width="5" stroke-dasharray="10 9" marker-end="url(#arrow-purple)"/>
<text x="600" y="360" text-anchor="middle" fill="#aebdff" class="label">3. Full-duplex WebSocket Frames</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540" role="img" aria-labelledby="title desc">
<title id="title">WebSocket history</title>
<desc id="desc">A compact Persian timeline with English technical names.</desc>
<defs>
<style>
@font-face {
font-family: "Vazirmatn";
src: url("../../../app/static/Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900;
}
text { font-family: "Vazirmatn", Arial, sans-serif; }
</style>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#0f172a" flood-opacity=".12"/>
</filter>
</defs>
<rect width="960" height="540" fill="#262a3a"/>
<path d="M34 112 L238 34 L340 146 L156 238 Z" fill="#f4b740" opacity=".12"/>
<path d="M700 410 L930 318 L982 504 L746 548 Z" fill="#6d8cff" opacity=".16"/>
<g id="diagram-content-shift" transform="translate(0,-28)">
<line x1="128" y1="282" x2="832" y2="282" stroke="#4a5168" stroke-width="10" stroke-linecap="round"/>
<g filter="url(#shadow)">
<rect x="76" y="164" width="808" height="238" rx="26" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
</g>
<g text-anchor="middle">
<g>
<circle cx="150" cy="282" r="24" fill="#2563eb"/>
<text x="150" y="289" fill="#fff" font-size="16" font-weight="800">1</text>
<text x="150" y="338" fill="#f8fafc" font-size="18" font-weight="800">Classic HTTP</text>
<text x="150" y="365" fill="#a8adbd" font-size="15">Request / Response</text>
<text x="150" y="390" fill="#a8adbd" font-size="15">No Server Push</text>
</g>
<g>
<circle cx="330" cy="282" r="24" fill="#2563eb"/>
<text x="330" y="289" fill="#fff" font-size="16" font-weight="800">2</text>
<text x="330" y="338" fill="#f8fafc" font-size="18" font-weight="800">Polling</text>
<text x="330" y="365" fill="#a8adbd" font-size="15">Repeated request</text>
<text x="330" y="390" fill="#a8adbd" font-size="15">More overhead</text>
</g>
<g>
<circle cx="510" cy="282" r="24" fill="#2563eb"/>
<text x="510" y="289" fill="#fff" font-size="16" font-weight="800">3</text>
<text x="510" y="338" fill="#f8fafc" font-size="18" font-weight="800">Long Polling</text>
<text x="510" y="365" fill="#a8adbd" font-size="15">Open request</text>
<text x="510" y="390" fill="#a8adbd" font-size="15">Long wait</text>
</g>
<g>
<circle cx="690" cy="282" r="24" fill="#2563eb"/>
<text x="690" y="289" fill="#fff" font-size="16" font-weight="800">4</text>
<text x="690" y="338" fill="#f8fafc" font-size="18" font-weight="800">WebSocket</text>
<text x="690" y="365" fill="#a8adbd" font-size="15">Persistent connection</text>
<text x="690" y="390" fill="#a8adbd" font-size="15">Full-duplex</text>
</g>
<g>
<circle cx="830" cy="282" r="24" fill="#10b981"/>
<text x="830" y="289" fill="#fff" font-size="16" font-weight="800">5</text>
<text x="830" y="338" fill="#f8fafc" font-size="18" font-weight="800">Real-time Web</text>
<text x="830" y="365" fill="#a8adbd" font-size="15">Chat - Game</text>
<text x="830" y="390" fill="#a8adbd" font-size="15">Dashboard - IoT</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,57 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500" viewBox="0 0 1200 500" role="img" aria-labelledby="title desc">
<title id="title">WebSocket in OSI model</title>
<desc id="desc">A compact OSI stack showing WebSocket at the application layer over TCP/IP.</desc>
<defs>
<style>
@font-face {
font-family: "Vazirmatn";
src: url("../../../app/static/Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900;
}
text { font-family: "Vazirmatn", Arial, sans-serif; }
.layer { fill: #f8fafc; font-size: 18px; font-weight: 850; }
.value { fill: #a8adbd; font-size: 18px; font-weight: 500; }
.strong { fill: #ffffff; font-size: 19px; font-weight: 850; }
.strongValue { fill: #ffffff; font-size: 19px; font-weight: 500; }
.mutedRow { fill: #ffffff; opacity: .08; stroke: #ffffff; }
</style>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#0f172a" flood-opacity=".16"/>
</filter>
</defs>
<rect width="1200" height="500" fill="#262a3a"/>
<path d="M42 396 L258 286 L374 438 L92 532 Z" fill="#f4b740" opacity=".13"/>
<path d="M880 42 L1146 112 L1030 262 L812 182 Z" fill="#6d8cff" opacity=".16"/>
<!-- Diagram is intentionally placed slightly above center, with all rows inside the outer panel. -->
<g id="diagram-content">
<g filter="url(#shadow)">
<rect x="150" y="80" width="900" height="350" rx="30" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
</g>
<rect x="190" y="105" width="820" height="48" rx="15" fill="#2563eb"/>
<text x="222" y="136" class="strong">Layer 7 - Application</text>
<text x="712" y="136" class="strongValue">WebSocket - JSON - Chat</text>
<rect x="190" y="165" width="820" height="40" rx="14" class="mutedRow"/>
<text x="222" y="191" class="layer">Layer 6 - Presentation</text>
<text x="712" y="191" class="value">UTF-8 - Text</text>
<rect x="190" y="215" width="820" height="40" rx="14" class="mutedRow"/>
<text x="222" y="241" class="layer">Layer 5 - Session</text>
<text x="712" y="241" class="value">Long-lived conversation</text>
<rect x="190" y="265" width="820" height="48" rx="15" fill="#10b981"/>
<text x="222" y="296" class="strong">Layer 4 - Transport</text>
<text x="712" y="296" class="strongValue">TCP reliable stream</text>
<rect x="190" y="325" width="820" height="40" rx="14" class="mutedRow"/>
<text x="222" y="351" class="layer">Layer 3 - Network</text>
<text x="712" y="351" class="value">IP routing</text>
<rect x="190" y="375" width="820" height="40" rx="14" class="mutedRow"/>
<text x="222" y="401" class="layer">Layers 2-1 - Link / Physical</text>
<text x="712" y="401" class="value">Ethernet - Wi-Fi</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540" role="img" aria-labelledby="title desc">
<title id="title">WebSocket pros and cons</title>
<desc id="desc">A compact pros and cons diagram for WebSocket.</desc>
<defs>
<style>
@font-face {
font-family: "Vazirmatn";
src: url("../../../app/static/Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900;
}
text { font-family: "Vazirmatn", Arial, sans-serif; }
</style>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#0f172a" flood-opacity=".12"/>
</filter>
</defs>
<rect width="960" height="540" fill="#262a3a"/>
<path d="M38 412 L244 320 L330 484 L96 548 Z" fill="#43d9a3" opacity=".12"/>
<path d="M720 34 L936 112 L828 236 L640 136 Z" fill="#fb7185" opacity=".12"/>
<g id="diagram-content-shift" transform="translate(0,-28)">
<g filter="url(#shadow)">
<rect x="92" y="154" width="350" height="286" rx="28" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
<rect x="518" y="154" width="350" height="286" rx="28" fill="#ffffff" opacity=".08" stroke="#ffffff"/>
</g>
<text x="130" y="206" fill="#047857" font-size="25" font-weight="800">Pros</text>
<text x="158" y="256" fill="#f8fafc" font-size="17">Low latency</text>
<text x="158" y="304" fill="#f8fafc" font-size="17">Full-duplex</text>
<text x="158" y="352" fill="#f8fafc" font-size="17">Less HTTP overhead</text>
<text x="158" y="400" fill="#f8fafc" font-size="17">Good for Chat / Dashboard</text>
<circle cx="136" cy="250" r="7" fill="#10b981"/>
<circle cx="136" cy="298" r="7" fill="#10b981"/>
<circle cx="136" cy="346" r="7" fill="#10b981"/>
<circle cx="136" cy="394" r="7" fill="#10b981"/>
<text x="556" y="206" fill="#b42318" font-size="25" font-weight="800">Cons</text>
<text x="584" y="256" fill="#f8fafc" font-size="17">Server state</text>
<text x="584" y="304" fill="#f8fafc" font-size="17">Open socket memory</text>
<text x="584" y="352" fill="#f8fafc" font-size="17">Reconnect handling</text>
<text x="584" y="400" fill="#f8fafc" font-size="17">Scaling needs design</text>
<circle cx="562" cy="250" r="7" fill="#ef4444"/>
<circle cx="562" cy="298" r="7" fill="#ef4444"/>
<circle cx="562" cy="346" r="7" fill="#ef4444"/>
<circle cx="562" cy="394" r="7" fill="#ef4444"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

465
docs/slides/index.html Normal file
View File

@@ -0,0 +1,465 @@
<!doctype html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebSocket Protocol</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="deck" id="deck">
<section class="slide active center-slide" data-title="Title">
<div class="center-content">
<h1>Websocket Protocol</h1>
<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>
</div>
<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 class="slide" data-title="Timeline">
<h2>WebSocket History Timeline</h2>
<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-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>Long Polling</span><small>2006</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>
<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 class="slide protocol-slide" data-title="HTTP">
<h2>HTTP</h2>
<p class="subtitle">A short-lived request/response cycle with no native live push.</p>
<div class="protocol-layout">
<div class="protocol-visual glass-panel">
<img src="assets/http-mechanism.svg" alt="HTTP request response mechanism" />
</div>
<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>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>
<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 class="slide protocol-slide" data-title="Polling">
<h2>Polling</h2>
<p class="subtitle">The browser repeatedly asks the server for new data.</p>
<div class="protocol-layout">
<div class="protocol-visual glass-panel">
<img src="assets/polling-mechanism.svg" alt="Polling repeated request mechanism" />
</div>
<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>Cost</b><ul><li>Many empty requests waste network work</li><li>Latency depends on the timer interval</li></ul></article>
</div>
</div>
<aside class="notes">Polling is simple but wasteful. Short intervals improve latency but increase server and network cost.</aside>
</section>
<section class="slide protocol-slide" data-title="Long Polling">
<h2>Long Polling</h2>
<p class="subtitle">The server holds the request until an update is available.</p>
<div class="protocol-layout">
<div class="protocol-visual glass-panel">
<img src="assets/long-polling-mechanism.svg" alt="Long Polling held request mechanism" />
</div>
<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>Cost</b><ul><li>Open requests consume server state</li><li>Reconnect and timeout handling is required</li></ul></article>
</div>
</div>
<aside class="notes">Long Polling reduces empty responses compared with Polling, but it still works through repeated HTTP request cycles.</aside>
</section>
<section class="slide protocol-slide" data-title="WebSocket">
<h2>WebSocket</h2>
<p class="subtitle">An HTTP Upgrade creates one persistent Full-duplex channel.</p>
<div class="protocol-layout">
<div class="protocol-visual glass-panel">
<img src="assets/websocket-handshake.svg" alt="HTTP Upgrade Handshake" />
</div>
<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>Cost</b><ul><li>Each client keeps a live connection</li><li>Scaling needs connection-aware design</li></ul></article>
</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>
</section>
<section class="slide image-slide" data-title="OSI">
<h2>WebSocket in the OSI Model</h2>
<p class="subtitle">Application semantics at Layer 7, reliable byte delivery through TCP/IP.</p>
<div class="single-visual glass-panel">
<img src="assets/websocket-osi.svg" alt="WebSocket in OSI model" />
</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>
</section>
<section class="slide" data-title="HTTP Versions">
<h2>HTTP Versions in WebSocket</h2>
<p class="subtitle">Different HTTP versions start WebSocket connections differently.</p>
<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/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>
</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>
</section>
<section class="slide image-slide" data-title="FastAPI Architecture">
<h2>Project Architecture</h2>
<p class="subtitle">Browsers connect to one WebSocket endpoint; ConnectionManager groups sockets by room.</p>
<div class="single-visual glass-panel">
<img src="assets/chatroom-architecture.svg" alt="WebSocket chatroom architecture" />
</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>
</section>
<section class="slide code-slide" data-title="Backend Router">
<h2>Backend Codes</h2>
<p class="subtitle">WebSocket router setup in FastAPI.</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">import</span> json</span>
<span class="line"><span class="kw">from</span> datetime <span class="kw">import</span> datetime, timezone</span>
<span class="line"></span>
<span class="line"><span class="kw">from</span> fastapi <span class="kw">import</span> APIRouter, WebSocket, WebSocketDisconnect</span>
<span class="line"></span>
<span class="line"><span class="kw">from</span> app.websocket.manager <span class="kw">import</span> manager</span>
<span class="line"></span>
<span class="line">router = APIRouter()</span></code></pre>
</div>
<aside class="notes">You can start by explaining that this module owns the WebSocket endpoint and delegates connection storage to the shared manager.</aside>
</section>
<section class="slide code-slide" data-title="Backend Payload">
<h2>Message Payload Shape</h2>
<p class="subtitle">Every server message uses one small JSON structure.</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">def</span> <span class="fn">now_iso</span>() -&gt; str:</span>
<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>

69
docs/slides/script.js Normal file
View File

@@ -0,0 +1,69 @@
const slides = [...document.querySelectorAll('.slide')];
const counter = document.getElementById('counter');
const progressBar = document.getElementById('progressBar');
const notesBox = document.getElementById('speakerNotes');
let index = 0;
function fitDeck() {
const vw = window.innerWidth;
const vh = window.innerHeight;
const root = document.documentElement;
// Mobile uses normal document flow, so text can shrink and content can scroll.
if (vw <= 900) {
const mobileScale = Math.max(0.64, Math.min(0.9, vw / 900));
root.style.setProperty('--slide-scale', mobileScale.toFixed(3));
root.style.removeProperty('--slide-w');
root.style.removeProperty('--slide-h');
return;
}
// Desktop/tablet: keep a 16:9 slide and fill as much height as the viewport allows.
const pad = Math.max(24, Math.min(72, Math.min(vw, vh) * 0.065));
const availableW = vw - pad;
const availableH = vh - pad;
let slideW = Math.min(availableW, availableH * (16 / 9));
let slideH = slideW * (9 / 16);
// If the width-bound calculation is too tall, bind by height instead.
if (slideH > availableH) {
slideH = availableH;
slideW = slideH * (16 / 9);
}
const scale = Math.max(0.78, Math.min(1.55, Math.min(slideW / 1200, slideH / 675)));
root.style.setProperty('--slide-w', `${Math.floor(slideW)}px`);
root.style.setProperty('--slide-h', `${Math.floor(slideH)}px`);
root.style.setProperty('--slide-scale', scale.toFixed(3));
}
function update() {
slides.forEach((slide, i) => slide.classList.toggle('active', i === index));
counter.textContent = `${index + 1} / ${slides.length}`;
progressBar.style.width = `${((index + 1) / slides.length) * 100}%`;
const note = slides[index].querySelector('.notes')?.textContent.trim() || '';
notesBox.textContent = note;
}
function go(delta) {
index = Math.min(slides.length - 1, Math.max(0, index + delta));
update();
}
document.getElementById('prev').addEventListener('click', () => go(-1));
document.getElementById('next').addEventListener('click', () => go(1));
document.addEventListener('keydown', (e) => {
if (['ArrowLeft', 'PageUp'].includes(e.key)) go(-1);
if (['ArrowRight', 'PageDown', ' '].includes(e.key)) go(1);
if (e.key.toLowerCase() === 'n') notesBox.classList.toggle('show');
if (e.key.toLowerCase() === 'f') {
if (!document.fullscreenElement) document.documentElement.requestFullscreen?.();
else document.exitFullscreen?.();
}
});
window.addEventListener('resize', fitDeck);
window.addEventListener('orientationchange', fitDeck);
fitDeck();
update();

1160
docs/slides/styles.css Normal file

File diff suppressed because it is too large Load Diff

35
nginx/default.conf Normal file
View File

@@ -0,0 +1,35 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
upstream chatroom_app {
server chatroom-app:8000;
}
server {
listen 80;
server_name _;
location / {
proxy_pass http://chatroom_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}