在传统 Web 应用中,浏览器通常通过 HTTP 向服务器发起请求,服务器处理完成后返回结果。这种"请求---响应"模式非常适合查询数据、提交表单等业务,但在即时聊天、实时通知、在线协作等场景中,它的局限也比较明显。
如果客户端需要持续获得最新数据,就只能反复向服务器发送请求,不仅增加服务器压力,还会产生额外延迟。
WebSocket 提供了一种更适合实时通信的解决方案。它可以在客户端和服务器之间建立持久连接,连接建立后,双方都能主动发送消息。
本文将使用 FastAPI 实现一个简单的 WebSocket 服务,并逐步加入连接管理、群发消息、异常处理和心跳检测等功能。
一、HTTP 轮询为什么不适合实时通信?
假设一个网页需要实时显示服务器状态,最简单的方式是每隔两秒请求一次接口:
setInterval(async () => {
const response = await fetch("/api/status");
const data = await response.json();
console.log(data);
}, 2000);
这种方式被称为轮询。
虽然实现简单,但存在几个问题:
-
即使数据没有变化,客户端也会持续发送请求;
-
实时性取决于轮询间隔;
-
轮询间隔越短,服务器压力越大;
-
每次请求都需要携带 HTTP 请求头;
-
大量用户同时在线时,请求数量会迅速增长。
例如,1000 个在线用户每两秒请求一次接口,服务器每秒就要处理大约 500 个请求,其中很多请求可能并没有获得新数据。
WebSocket 建立连接后,服务器可以只在数据发生变化时主动通知客户端,从而减少无效请求。
二、WebSocket 的基本工作方式
WebSocket 最初通过 HTTP 发起连接请求,服务器同意升级协议后,连接会切换为 WebSocket。
建立连接后的通信过程可以简单理解为:
客户端发起连接
↓
服务器接受连接
↓
双方保持长连接
↓
客户端和服务器随时互发消息
↓
任意一方关闭连接
与普通 HTTP 请求相比,WebSocket 主要有以下特点:
-
支持双向通信;
-
连接可以长期保持;
-
服务器能够主动推送数据;
-
单次消息的额外开销较小;
-
更适合低延迟、持续交互的业务。
三、创建一个最简单的 WebSocket 服务
首先安装 FastAPI 和 Uvicorn:
pip install fastapi uvicorn
创建 main.py:
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
message = await websocket.receive_text()
await websocket.send_text(
f"服务器已收到:{message}"
)
启动服务:
uvicorn main:app --reload
这段代码完成了三个操作:
-
接受客户端的 WebSocket 连接;
-
持续等待客户端发送文本;
-
将收到的内容返回给客户端。
需要注意,WebSocket 路由使用的是 @app.websocket(),而不是普通接口使用的 @app.get() 或 @app.post()。
四、编写一个简单的前端客户端
创建一个 HTML 页面:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebSocket 测试</title>
</head>
<body>
<input id="message" placeholder="请输入消息">
<button onclick="sendMessage()">发送</button>
<ul id="messages"></ul>
<script>
const socket = new WebSocket(
"ws://localhost:8000/ws"
);
socket.onopen = () => {
addMessage("WebSocket 已连接");
};
socket.onmessage = (event) => {
addMessage(event.data);
};
socket.onclose = () => {
addMessage("WebSocket 已断开");
};
socket.onerror = () => {
addMessage("连接发生错误");
};
function sendMessage() {
const input = document.getElementById("message");
if (!input.value.trim()) {
return;
}
socket.send(input.value);
input.value = "";
}
function addMessage(content) {
const item = document.createElement("li");
item.textContent = content;
document
.getElementById("messages")
.appendChild(item);
}
</script>
</body>
</html>
打开页面并建立连接后,在输入框中发送消息,就可以收到服务器返回的内容。
生产环境通常使用 HTTPS。对应的 WebSocket 地址需要从 ws:// 改成加密的 wss://。
五、管理多个客户端连接
前面的代码只能处理单个客户端与服务器之间的交互。如果要实现聊天室或群发通知,就需要保存当前所有在线连接。
可以创建一个连接管理器:
from fastapi import WebSocket
class ConnectionManager:
def __init__(self):
self.active_connections: list[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
if websocket in self.active_connections:
self.active_connections.remove(websocket)
async def send_personal_message(
self,
message: str,
websocket: WebSocket,
):
await websocket.send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
然后在 WebSocket 路由中使用它:
from fastapi import (
FastAPI,
WebSocket,
WebSocketDisconnect,
)
app = FastAPI()
manager = ConnectionManager()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await manager.connect(websocket)
try:
while True:
message = await websocket.receive_text()
await manager.broadcast(
f"新消息:{message}"
)
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast(
"一名用户已离开"
)
当某个用户发送消息时,服务器会把消息发送给当前保存的所有连接,从而实现基础的群聊效果。
六、正确处理客户端断开连接
网络切换、浏览器关闭、页面刷新都可能导致 WebSocket 连接中断。
如果没有处理断开事件,失效连接会一直保存在连接列表中。服务器下次群发消息时,可能会向已经断开的连接发送数据并触发异常。
FastAPI 提供了 WebSocketDisconnect 异常,可以在客户端断开时进行清理:
try:
while True:
message = await websocket.receive_text()
await manager.broadcast(message)
except WebSocketDisconnect:
manager.disconnect(websocket)
同时,群发消息时也应该考虑连接可能刚好失效:
async def broadcast(self, message: str):
disconnected = []
for connection in self.active_connections:
try:
await connection.send_text(message)
except Exception:
disconnected.append(connection)
for connection in disconnected:
self.disconnect(connection)
这样可以避免一个失效连接影响其他正常用户。
七、使用 JSON 传递结构化消息
实际项目中的 WebSocket 消息通常不只是简单字符串,还需要携带消息类型、用户信息、时间戳和业务数据。
可以使用 JSON 定义统一的消息格式:
{
"type": "chat_message",
"sender": "user_1001",
"content": "你好",
"timestamp": 1720000000
}
FastAPI 可以直接接收和发送 JSON:
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_json()
response = {
"type": "message_received",
"sender": data.get("sender"),
"content": data.get("content"),
}
for connection in manager.active_connections:
await connection.send_json(response)
except WebSocketDisconnect:
manager.disconnect(websocket)
采用结构化消息后,前端可以根据 type 字段执行不同操作,例如:
-
chat_message:显示聊天内容; -
system_notice:展示系统通知; -
task_progress:更新任务进度; -
error:提示错误信息; -
heartbeat:处理心跳检测。
统一消息结构能够减少前后端沟通成本,也方便以后扩展新功能。
八、案例延伸:同言翻译如何应用 WebSocket
除了即时聊天和实时通知,WebSocket 也适用于需要持续传输内容的翻译场景。以同言翻译为例,在跨语言交流过程中,客户端可能会连续产生需要翻译的文字或语音内容。如果每次都重新建立 HTTP 请求,不仅会增加连接开销,也可能影响翻译结果返回的及时性。
通过 WebSocket 建立长连接后,客户端可以持续发送待处理内容,服务器则可以及时返回识别结果、翻译内容和任务状态。例如,系统可以依次推送"内容已接收""正在翻译""翻译完成"等消息,让用户清楚了解当前进度。
对于同言翻译这类强调连续交互体验的应用,WebSocket 的价值不仅是降低通信延迟,还在于能够维持稳定的双向数据通道。不过,完整的实时翻译系统还需要处理消息顺序、任务编号、断线重连、超时重试以及数据安全等问题。
九、加入心跳检测
WebSocket 虽然是长连接,但连接可能因为网络设备超时、移动网络切换等原因失效。
一种常见做法是让客户端定期发送心跳消息:
setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: "ping"
}));
}
}, 30000);
服务器收到后返回响应:
data = await websocket.receive_json()
if data.get("type") == "ping":
await websocket.send_json({
"type": "pong"
})
continue
服务端还可以记录每个连接最后一次心跳时间。如果某个连接长时间没有发送心跳,就主动关闭并清理该连接。
心跳间隔不宜设置得太短,否则会产生大量无意义消息。一般可以根据业务的实时性要求和网络环境进行调整。
十、生产环境需要注意的问题
1. 不要只在内存中保存重要状态
示例中的连接列表保存在当前进程的内存中,只适合学习和单进程部署。
当服务启动多个进程或部署到多台服务器时,不同进程之间无法直接共享连接列表。此时可以借助 Redis Pub/Sub、消息队列等组件传递跨进程消息。
2. 增加身份认证
WebSocket 接口同样需要验证用户身份,不能因为它不是普通 HTTP 接口就忽略安全问题。
客户端可以在连接地址中携带短期令牌:
ws://localhost:8000/ws?token=xxxxx
服务端在接受连接前验证令牌。验证失败时,应该拒绝连接。
需要注意,不应在日志中记录完整令牌,也不建议在连接地址中传递长期有效的敏感凭证。
3. 限制消息大小和发送频率
如果不限制消息长度和发送频率,恶意客户端可能通过发送大量数据消耗服务器资源。
生产环境建议设置:
-
单条消息最大长度;
-
单个用户的发送频率;
-
最大在线连接数;
-
单个连接的空闲时间;
-
异常行为的断开策略。
4. 设计断线重连机制
前端检测到连接断开后,可以尝试重新连接:
function connect() {
const socket = new WebSocket(
"ws://localhost:8000/ws"
);
socket.onclose = () => {
setTimeout(connect, 3000);
};
}
connect();
更完善的方案应该使用指数退避策略,避免服务器故障时所有客户端同时高频重连。
十一、WebSocket 和 HTTP 应该如何选择?
WebSocket 并不是 HTTP 的替代品,两者适合解决不同问题。
适合使用 HTTP 的场景包括:
-
普通数据查询;
-
表单提交;
-
文件上传;
-
不需要持续连接的业务;
-
对实时性要求不高的任务。
适合使用 WebSocket 的场景包括:
-
即时聊天;
-
实时通知;
-
在线协作;
-
游戏状态同步;
-
任务进度推送;
-
持续产生数据的 AI 应用。
一个完整系统通常会同时使用两种协议。例如,用户登录、查询历史记录使用 HTTP,而实时消息和状态更新使用 WebSocket。
十二、总结
使用 FastAPI 构建一个基础 WebSocket 服务并不复杂,但从演示代码走向生产环境,还需要处理很多工程问题:
-
多客户端连接管理;
-
断开连接清理;
-
JSON 消息协议;
-
心跳检测;
-
身份认证;
-
发送频率限制;
-
断线重连;
-
多进程消息同步。
WebSocket 的核心价值,是让服务器和客户端在一条持久连接上进行双向通信。对于即时聊天、实时通知和连续 AI 处理等业务,它可以提供比传统轮询更及时、更高效的交互体验。
在技术选型时,不必为了"实时"而盲目使用 WebSocket。如果业务只需要偶尔获取一次数据,普通 HTTP 通常更加简单可靠;只有在客户端和服务器需要持续交换信息时,WebSocket 的优势才会真正体现出来。

