fastapi sse websocket 智能家居实时控制台
本文档以"智能家居控制台"项目为载体,按章节讲解 FastAPI + SSE + WebSocket 的完整开发流程。
项目后端文件为
main.py,前端为index.html,使用uv管理依赖。

目录
- 项目概述与架构
- [环境搭建与 uv 依赖管理](#环境搭建与 uv 依赖管理)
- 后端:设备状态模型
- 后端:设备管理器与并发控制
- [后端:HTTP 路由与静态文件](#后端:HTTP 路由与静态文件)
- [后端:SSE 传感器数据流](#后端:SSE 传感器数据流)
- [后端:WebSocket 控制通道](#后端:WebSocket 控制通道)
- [前端:页面结构与 CSS 设计系统](#前端:页面结构与 CSS 设计系统)
- [前端:SSE 客户端实现](#前端:SSE 客户端实现)
- [前端:WebSocket 客户端实现](#前端:WebSocket 客户端实现)
- 前端:实时数据渲染与交互
- 部署与运行
第 1 章 · 项目概述与架构
1.1 项目目标
构建一个 IoT 场景的智能家居控制台,实现:
- SSE(Server-Sent Events) 单向推送传感器数据:温度、湿度、门锁状态。
- WebSocket 双向通道下发控制命令:开灯、关灯、调温。
- 虚拟设备 / 硬件演示 两种数据源切换,模拟硬件/虚拟设备切换。
1.2 技术栈
| 层级 | 技术 | 说明 |
|---|---|---|
| 后端框架 | FastAPI 0.141+ | 现代 Python 异步 Web 框架 |
| ASGI 服务器 | uvicornstandard | 高性能异步服务器,含 uvloop 加速 |
| 实时通信 | SSE + WebSocket | SSE 推数据,WS 发命令 |
| 前端 | 原生 HTML/CSS/JS | 无构建依赖,单文件部署 |
| 包管理 | uv | Rust 编写的极速 Python 包管理器 |
1.3 架构图

1.4 数据流
传感器数据流(SSE,每秒一次):
后端 DeviceHub.read_both()
→ 生成 {virtual: {...}, hardware: {...}} 双模快照
→ SSE 事件帧: event: telemetry\ndata: {...}\n\n
→ 前端 fetch ReadableStream 接收
→ renderTelemetry() 按当前模式过滤渲染
控制命令流(WebSocket,按需):
前端 sendCommand("开灯")
→ WS 发送: {"command":"开灯","mode":"virtual"}
→ 后端 hub.command() 执行
→ WS 返回: {"type":"ack","command":"开灯","state":{...}}
→ 前端 renderTelemetry() 更新 UI
第 2 章 · 环境搭建与 uv 依赖管理
2.1 为什么用 uv
uv 是 Astral 公司用 Rust 开发的 Python 包管理器,特点:
- 速度极快:比 pip 快 10-100 倍。
- 兼容 pip :
uv pip install与pip install语法一致。 - 项目管理 :
uv init+uv add类似 npm 的流程,自动管理pyproject.toml和虚拟环境。
2.2 初始化项目
bash
# 进入项目目录
cd fastapi-sse-ws
# 初始化 uv 项目(生成 pyproject.toml)
uv init --no-readme --no-pin-python
2.3 安装依赖
bash
# 安装 FastAPI 和 uvicorn(不指定版本,自动获取最新)
uv add fastapi "uvicorn[standard]"
uv add 会自动:
- 创建
.venv虚拟环境(如果不存在)。 - 解析依赖树并安装到虚拟环境。
- 更新
pyproject.toml的dependencies字段。 - 生成
uv.lock锁定文件。
2.4 pyproject.toml 结构
toml
[project]
name = "fastapi-sse-ws"
version = "0.1.0"
description = "智能家居实时控制台 --- SSE 推传感器数据, WS 发控制命令"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.141.1",
"uvicorn[standard]>=0.52.1",
]
requires-python:最低 Python 版本要求。dependencies:生产依赖列表,uv add自动维护。"uvicorn[standard]":安装 uvicorn 及其可选依赖(uvloop、httptools 等)。
2.5 常用 uv 命令
| 命令 | 作用 |
|---|---|
uv init |
初始化新项目 |
uv add <包名> |
添加依赖 |
uv add --dev <包名> |
添加开发依赖 |
uv remove <包名> |
移除依赖 |
uv sync |
按 lock 文件同步安装依赖 |
uv run <命令> |
在项目虚拟环境中执行命令 |
uv lock |
更新锁文件 |
第 3 章 · 后端:设备状态模型
对应代码:
main.py第 1 章 ·DeviceState类
3.1 dataclass 简介
Python 的 @dataclass 装饰器自动生成 __init__、__repr__ 等方法,适合定义纯数据容器:
python
from dataclasses import dataclass
@dataclass
class DeviceState:
mode: str
temperature: float
humidity: float
target_temperature: float = 24.0 # 带默认值的字段
light_on: bool = False
door_locked: bool = True
sequence: int = 0
3.2 传感器模拟逻辑(advance 方法)
advance() 方法每秒被调用一次,模拟传感器数据的自然漂移:
python
def advance(self) -> None:
self.sequence += 1
# 虚拟设备:快速趋近 + 大噪声
if self.mode == "virtual":
self.temperature += (self.target - self.temperature) * 0.045
self.temperature += random.uniform(-0.10, 0.10)
# 硬件演示:缓慢趋近 + 小噪声
else:
self.temperature += (self.target - self.temperature) * 0.025
self.temperature += random.uniform(-0.035, 0.035)
# 钳制到合理范围
self.temperature = min(30.0, max(16.0, self.temperature))
关键设计: 虚拟和硬件模式使用不同的参数(趋近系数、噪声幅度),模拟真实硬件传感器数据更平滑的特性。
3.3 状态快照(snapshot 方法)
snapshot() 将内部状态导出为 JSON 可序列化的字典,附加元信息(数据源 ID、延迟、信号强度等):
python
def snapshot(self, active_streams: int = 0) -> dict[str, Any]:
return {
"sequence": self.sequence,
"temperature": round(self.temperature, 1),
"source_id": "SIM-HOME-01" if is_virtual else "GW-BUPT-01",
"rssi": None if is_virtual else random.randint(-68, -55),
# ... 其他字段
}
第 4 章 · 后端:设备管理器与并发控制
对应代码:
main.py第 2 章 ·DeviceHub类
4.1 为什么需要锁
SSE 端点和 WebSocket 端点可能同时读写设备状态。asyncio.Lock 确保同一时刻只有一个协程操作状态:
python
class DeviceHub:
def __init__(self) -> None:
self.states = {"virtual": DeviceState(...), "hardware": DeviceState(...)}
self._lock = asyncio.Lock()
async def read_both(self) -> dict[str, dict[str, Any]]:
async with self._lock: # 获取锁
for state in self.states.values():
state.advance()
return {mode: state.snapshot() for ...}
4.2 双模快照设计
SSE 端点每帧推送两种模式的数据,而非按 query 参数过滤:
python
async def read_both(self):
# 返回 {"virtual": {...}, "hardware": {...}}
为什么这样设计? 前端切换模式时只需改变 appState.mode,SSE 连接保持不变,避免 EventSource.close() / fetch.abort() 产生的 net::ERR_ABORTED 控制台日志。
4.3 命令执行(command 方法)
python
async def command(self, mode, command, payload):
if mode == "hardware":
await asyncio.sleep(0.12) # 模拟硬件链路延迟
async with self._lock:
state = self.states[mode]
if command == "开灯":
state.light_on = True
elif command == "调温":
target = float(payload["target"])
if not 16 <= target <= 30:
raise ValueError("目标温度须在 16--30°C 之间")
state.target_temperature = round(target, 1)
return {"type": "ack", "state": state.snapshot()}
第 5 章 · 后端:HTTP 路由与静态文件
对应代码:
main.py第 3 章
5.1 路由装饰器
FastAPI 使用装饰器声明路由:
python
@app.get("/", include_in_schema=False) # 不出现在 /docs 中
async def index() -> FileResponse:
return FileResponse(BASE_DIR / "index.html")
@app.get("/api/health")
async def health() -> dict[str, Any]:
return {"status": "ok", "sse_clients": hub.active_streams}
5.2 FastAPI 最新语法要点
- 异步优先 :所有路由使用
async def,FastAPI 0.100+ 推荐 async-first。 - 类型提示 :返回类型注解(如
-> dict[str, Any])自动生成 OpenAPI schema。 - Pydantic V2:FastAPI 0.128+ 移除了 Pydantic V1 支持,当前使用 Pydantic V2。
- lifespan :
@app.on_event("startup")已废弃,推荐使用lifespan上下文管理器(本项目无全局资源需管理,故未使用)。
第 6 章 · 后端:SSE 传感器数据流
对应代码:
main.py第 4 章 ·sensor_events
6.1 SSE 协议简介
Server-Sent Events 是 HTTP 长连接上的单向推送协议。报文格式:
event: telemetry\n
data: {"temperature": 24.5}\n
\n
每个事件以 \n\n 结尾。浏览器通过 EventSource 或 fetch + ReadableStream 接收。
6.2 StreamingResponse
FastAPI 的 StreamingResponse 接受一个异步生成器,逐块发送响应体:
python
@app.get("/api/events")
async def sensor_events(request: Request) -> StreamingResponse:
async def event_stream():
hub.active_streams += 1
try:
while True:
if await request.is_disconnected():
break
payload = await hub.read_both()
yield f"event: telemetry\ndata: {json.dumps(payload)}\n\n"
await asyncio.sleep(1.0)
finally:
hub.active_streams = max(0, hub.active_streams - 1)
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
)
6.3 关键响应头
| 头部 | 作用 |
|---|---|
Cache-Control: no-cache |
禁用代理缓存 |
Connection: keep-alive |
保持长连接 |
X-Accel-Buffering: no |
禁止 Nginx 缓冲(确保实时推送) |
6.4 断开检测
request.is_disconnected() 是 Starlette 提供的异步方法,检测客户端是否已断开。在 finally 中递减计数器,确保异常退出也能正确清理。
第 7 章 · 后端:WebSocket 控制通道
对应代码:
main.py第 5 章 ·control_socket
7.1 WebSocket 端点定义
python
@app.websocket("/ws/control")
async def control_socket(websocket: WebSocket) -> None:
await websocket.accept() # 完成握手
await websocket.send_json({"type": "connected", ...})
while True:
try:
data = await websocket.receive_json()
response = await hub.command(data["mode"], data["command"], data)
await websocket.send_json(response)
except WebSocketDisconnect:
break
7.2 消息协议
| 方向 | 类型 | 格式 |
|---|---|---|
| 客户端→服务端 | 命令 | {"command":"开灯","mode":"virtual"} |
| 服务端→客户端 | 连接确认 | {"type":"connected","message":"..."} |
| 服务端→客户端 | 命令回执 | {"type":"ack","command":"开灯","state":{...}} |
| 服务端→客户端 | 错误 | {"type":"error","message":"..."} |
7.3 错误处理
python
except WebSocketDisconnect: # 客户端正常断开
break
except (json.JSONDecodeError, TypeError): # 无效 JSON
await websocket.send_json({"type": "error", "message": "命令必须是有效 JSON"})
第 8 章 · 前端:页面结构与 CSS 设计系统
对应代码:
index.html第 1-15 章 CSS
8.1 设计令牌
使用 CSS 自定义属性集中管理颜色、字体:
css
:root {
--bg: #090b0c;
--acid: #b6f36b; /* 强调色 */
--font-display: "Bahnschrift", sans-serif;
}
8.2 三栏布局
css
.console {
display: grid;
grid-template-columns: 290px minmax(520px, 1fr) 330px;
}
- 左栏:传感器数据(温度曲线、湿度、门锁、数据源信息)
- 中栏:住宅数字孪生平面图(CSS Grid 绘制的三房间)
- 右栏:控制终端(灯光开关、温控步进、命令日志)
8.3 响应式断点
| 断点 | 布局变化 |
|---|---|
| ≤1120px | 缩小列宽,隐藏连接状态 |
| ≤900px | 平面图占满上方,左右面板下方并排 |
| ≤620px | 单列堆叠 |
第 9 章 · 前端:SSE 客户端实现
对应代码:
index.htmlJS 第 6 节 ·connectSSE
9.1 为什么不用 EventSource
EventSource.close() 会在 Chrome 控制台产生 net::ERR_ABORTED 网络日志。改用 fetch + ReadableStream 可以干净地管理连接:
javascript
fetch('/api/events')
.then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const pump = () => reader.read().then(({ done, value }) => {
if (done) return;
buffer += decoder.decode(value, { stream: true });
// 按 \n\n 分割 SSE 事件
let boundary;
while ((boundary = buffer.indexOf('\n\n')) !== -1) {
const chunk = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
chunk.split('\n').forEach(line => {
if (line.startsWith('data:')) {
renderTelemetry(JSON.parse(line.slice(5).trim()));
}
});
}
return pump();
});
return pump();
});
9.2 双模数据过滤
后端每帧推送 {virtual: {...}, hardware: {...}},前端按当前模式取值:
javascript
function renderTelemetry(data) {
const target = data[appState.mode]; // 只取当前模式的数据
if (!target) return;
// 更新 UI...
}
切换模式时只需 appState.mode = 'hardware',SSE 连接保持不变。
第 10 章 · 前端:WebSocket 客户端实现
对应代码:
index.htmlJS 第 8-10 节
10.1 连接管理
javascript
function connectWebSocket() {
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const socket = new WebSocket(`${protocol}//${location.host}/ws/control`);
socket.onopen = () => setChannel('ws', true, '控制链在线');
socket.onmessage = event => { /* 处理 ACK / error */ };
socket.onclose = () => setTimeout(connectWebSocket, 1600); // 自动重连
}
10.2 命令发送
javascript
function sendCommand(command, payload = {}) {
if (!appState.socket || appState.socket.readyState !== WebSocket.OPEN) return;
appState.socket.send(JSON.stringify({ command, mode: appState.mode, ...payload }));
}
10.3 ACK 处理
收到 ACK 后,如果回执的设备模式与当前选中模式一致,则更新 UI:
javascript
if (data.type === 'ack') {
if (data.mode === appState.mode) renderTelemetry({ [data.mode]: data.state });
}
第 11 章 · 前端:实时数据渲染与交互
对应代码:
index.htmlJS 第 4-5 节
11.1 温度折线图
使用原生 SVG <path> 绘制最近 24 个采样点:
javascript
function renderChart(value) {
appState.temperatures.push(value);
if (appState.temperatures.length > 24) appState.temperatures.shift();
// 将温度映射为 SVG 坐标 → 生成 path 字符串 → 更新 DOM
}
11.2 数值闪烁反馈
每次温度更新时触发 CSS 动画,提供视觉反馈:
javascript
function pulse(element) {
element.classList.remove('bump');
void element.offsetWidth; // 强制重排,重置动画
element.classList.add('bump');
}
11.3 平面图灯光联动
通过 CSS 类切换实现灯光效果:
javascript
ui.floorplan.classList.toggle('light-on', target.light_on);
.light-on 类会触发光晕放大、灯具高亮、家具提亮等一系列 CSS 过渡动画。
第 12 章 · 部署与运行
12.1 开发模式
bash
# 安装依赖
uv sync
# 启动开发服务器(支持热重载)
uv run uvicorn main:app --reload --host 0.0.0.0 --port 8000
12.2 生产模式
bash
# 多 worker 启动
uv run uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
注意: 多 worker 模式下,每个 worker 进程持有独立的
DeviceHub实例,状态不共享。生产环境如需共享状态,需引入 Redis Pub/Sub。
12.3 Nginx 反向代理
nginx
location /api/events {
proxy_pass http://127.0.0.1:8000;
proxy_buffering off; # SSE 必须关闭缓冲
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
}
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
12.4 验证
打开浏览器访问 http://127.0.0.1:8000:
- SSE 指示灯变绿,温度每秒更新。
- WS 指示灯变绿,日志显示"控制通道已建立"。
- 点击灯光开关,平面图灯亮,日志出现
ACK 001。 - 切换"硬件演示",数据源 ID 变为
GW-BUPT-01,出现 RSSI 信号值。