Litestar WebSocket断线重连:客户端与服务端实现
引言:实时通信的稳定性挑战
你是否曾遇到WebSocket连接意外中断导致实时数据传输中断的问题?在生产环境中,网络波动、服务器重启或负载均衡都会造成连接断开,而手动刷新页面的体验对用户来说是不可接受的。本文将系统讲解如何在Litestar框架中实现WebSocket断线自动重连机制,涵盖服务端连接状态管理、客户端智能重试策略及完整的双向通信示例。
读完本文你将掌握:
- 服务端连接生命周期管理与断开检测
- 客户端指数退避重连算法实现
- 连接状态持久化与会话恢复方案
- 生产级重连机制的错误处理与监控
WebSocket连接生命周期与断开检测
服务端连接状态管理
Litestar通过WebsocketListener类提供了完整的连接生命周期回调,我们可以通过重写这些方法实现连接跟踪:
from litestar import Litestar, WebSocket
from litestar.handlers import WebsocketListener
from typing import Dict
class ReconnectHandler(WebsocketListener):
path = "/reconnect-demo"
connections: Dict[str, WebSocket] = {} # 存储活跃连接
async def on_accept(self, socket: WebSocket) -> None:
"""新连接建立时触发"""
client_id = socket.headers.get("X-Client-ID", str(id(socket)))
self.connections[client_id] = socket
await socket.send_json({"status": "connected", "client_id": client_id})
print(f"Client {client_id} connected")
async def on_disconnect(self, socket: WebSocket) -> None:
"""连接断开时触发"""
client_id = socket.headers.get("X-Client-ID", str(id(socket)))
if client_id in self.connections:
del self.connections[client_id]
print(f"Client {client_id} disconnected")
async def on_receive(self, data: dict) -> dict:
"""处理客户端消息"""
return {"received": data, "timestamp": str(datetime.now())}
app = Litestar([ReconnectHandler])
连接断开的三种场景
| 断开类型 | 触发条件 | 服务端检测方式 | 客户端表现 |
|---|---|---|---|
| 正常关闭 | 客户端主动调用close() | on_disconnect回调 | 收到1000状态码 |
| 异常断开 | 网络中断或客户端崩溃 | 心跳超时检测 | 无关闭帧接收 |
| 服务器重启 | 服务端进程终止 | 连接重置错误 | 连接失败事件 |
服务端重连支持机制
会话状态持久化
为实现无缝重连,服务端需要保留客户端会话状态。使用Litestar的stores模块可以轻松实现:
from litestar.stores.redis import RedisStore
from litestar.config import AppConfig
config = AppConfig(
stores={
"session": RedisStore(url="redis://localhost:6379/0")
}
)
class ReconnectHandler(WebsocketListener):
# ... 之前的代码 ...
async def on_accept(self, socket: WebSocket) -> None:
client_id = socket.headers.get("X-Client-ID")
if client_id:
# 尝试恢复之前的会话状态
session_data = await socket.app.stores["session"].get(client_id)
if session_data:
await socket.send_json({"status": "reconnected", "session": session_data})
else:
await socket.send_json({"status": "connected", "client_id": client_id})
# ... 其余代码 ...
心跳检测实现
服务端可以通过定时发送心跳包检测无效连接:
class ReconnectHandler(WebsocketListener):
# ... 之前的代码 ...
async def on_accept(self, socket: WebSocket) -> None:
# ... 之前的代码 ...
self.connections[client_id] = socket
# 启动心跳检测任务
socket.app.loop.create_task(self.heartbeat_task(client_id, socket))
async def heartbeat_task(self, client_id: str, socket: WebSocket) -> None:
try:
while True:
await asyncio.sleep(30) # 每30秒发送一次心跳
await socket.send_json({"type": "heartbeat", "timestamp": str(datetime.now())})
except WebSocketDisconnect:
# 心跳发送失败,视为连接已断开
if client_id in self.connections:
del self.connections[client_id]
客户端重连策略实现
指数退避重连算法
客户端应采用渐进式延迟重试策略,避免服务器重启时的连接风暴:
class WebSocketClient {
constructor(url, clientId) {
this.url = url;
this.clientId = clientId || this.generateClientId();
this.socket = null;
this.attempts = 0;
this.maxAttempts = 10;
this.connect();
}
generateClientId() {
// 生成或从localStorage读取客户端ID
let id = localStorage.getItem('websocket_client_id');
if (!id) {
id = 'client_' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('websocket_client_id', id);
}
return id;
}
connect() {
if (this.attempts >= this.maxAttempts) {
console.error('Max reconnection attempts reached');
return;
}
this.socket = new WebSocket(`${this.url}?client_id=${this.clientId}`);
this.socket.onopen = () => {
console.log('Connected successfully');
this.attempts = 0; // 重置重试计数器
};
this.socket.onclose = (event) => {
console.log(`Disconnected: ${event.code}`);
if (event.code !== 1000) { // 非正常关闭才重连
this.reconnect();
}
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
this.socket.close();
};
}
reconnect() {
this.attempts++;
const delay = Math.min(1000 * Math.pow(2, this.attempts), 30000); // 指数退避,最大30秒
console.log(`Reconnecting in ${delay}ms (attempt ${this.attempts})`);
setTimeout(() => this.connect(), delay);
}
// 其他方法...
}
连接状态管理
使用状态机模式管理连接生命周期:
// 连接状态枚举
const ConnectionState = {
DISCONNECTED: "disconnected",
CONNECTING: "connecting",
CONNECTED: "connected",
RECONNECTING: "reconnecting"
};
class WebSocketClient {
constructor(url) {
this.state = ConnectionState.DISCONNECTED;
// ... 其余代码 ...
}
connect() {
if (this.state !== ConnectionState.DISCONNECTED) return;
this.state = ConnectionState.CONNECTING;
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
this.state = ConnectionState.CONNECTED;
// ... 其余代码 ...
};
this.socket.onclose = () => {
this.state = ConnectionState.DISCONNECTED;
this.reconnect();
};
this.reconnect = () => {
if (this.state === ConnectionState.RECONNECTING) return;
this.state = ConnectionState.RECONNECTING;
// ... 重连逻辑 ...
};
}
}
完整双向通信示例
服务端实现
from litestar import Litestar, WebSocket, websocket_listener
from litestar.stores.memory import MemoryStore
from datetime import datetime
import asyncio
store = MemoryStore()
@websocket_listener("/reconnect-demo")
async def reconnect_handler(data: dict, socket: WebSocket) -> dict:
client_id = socket.headers.get("X-Client-ID")
if socket.connection_state == "connect":
# 新连接或重连
if client_id:
# 尝试恢复会话
session_data = await store.get(client_id)
if session_data:
return {"status": "reconnected", "session": session_data, "timestamp": str(datetime.now())}
# 生成新的客户端ID
if not client_id:
client_id = f"client_{id(socket)}"
# 存储会话状态
await store.set(client_id, {"connected_at": str(datetime.now())})
return {"status": "connected", "client_id": client_id, "timestamp": str(datetime.now())}
# 处理常规消息
return {"received": data, "echo": data, "timestamp": str(datetime.now())}
app = Litestar([reconnect_handler], stores={"session": store})
客户端实现
<!DOCTYPE html>
<html>
<body>
<div id="status">Disconnected</div>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="Type message">
<button onclick="sendMessage()">Send</button>
<script>
const statusElement = document.getElementById('status');
const messagesElement = document.getElementById('messages');
const client = new WebSocketClient('ws://localhost:8000/reconnect-demo');
class WebSocketClient {
constructor(url) {
this.url = url;
this.clientId = localStorage.getItem('client_id');
this.attempts = 0;
this.connect();
}
connect() {
statusElement.textContent = 'Connecting...';
const headers = this.clientId ? `X-Client-ID: ${this.clientId}\r\n` : '';
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
statusElement.textContent = 'Connected';
this.attempts = 0;
};
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.client_id) {
this.clientId = data.client_id;
localStorage.setItem('client_id', this.clientId);
}
this.addMessage(`Server: ${JSON.stringify(data)}`);
};
this.socket.onclose = (event) => {
statusElement.textContent = 'Disconnected';
if (event.code !== 1000) {
this.reconnect();
}
};
this.socket.onerror = (error) => {
console.error('Error:', error);
};
}
reconnect() {
this.attempts++;
const delay = Math.min(1000 * Math.pow(2, this.attempts), 30000);
statusElement.textContent = `Reconnecting in ${delay}ms (attempt ${this.attempts})`;
setTimeout(() => this.connect(), delay);
}
send(data) {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
this.addMessage(`Client: ${JSON.stringify(data)}`);
} else {
this.addMessage('Not connected');
}
}
addMessage(text) {
const div = document.createElement('div');
div.textContent = text;
messagesElement.appendChild(div);
messagesElement.scrollTop = messagesElement.scrollHeight;
}
}
function sendMessage() {
const input = document.getElementById('messageInput');
client.send({ message: input.value });
input.value = '';
}
</script>
</body>
</html>
生产环境最佳实践
负载均衡环境配置
在多服务器环境中,需要确保重连请求路由到同一服务器:
# 配置Redis适配器实现会话粘性
from litestar.contrib.redis import RedisSessionBackend
from litestar.config import AppConfig
config = AppConfig(
session_backend=RedisSessionBackend(url="redis://localhost:6379/0"),
middleware=[
"litestar.middleware.session.SessionMiddleware"
]
)
监控与日志
实现完善的监控系统跟踪连接状态:
from litestar.logging import LoggingConfig
import logging
logging_config = LoggingConfig(
loggers={
"websocket": {
"level": "INFO",
"handlers": ["console"]
}
}
)
class ReconnectHandler(WebsocketListener):
async def on_accept(self, socket: WebSocket) -> None:
client_id = socket.headers.get("X-Client-ID")
logging.getLogger("websocket").info(f"Client connected: {client_id}")
async def on_disconnect(self, socket: WebSocket) -> None:
client_id = socket.headers.get("X-Client-ID")
logging.getLogger("websocket").info(f"Client disconnected: {client_id}")
常见问题解决方案
连接风暴防护
服务器重启后大量客户端同时重连可能导致过载:
# 服务端实现连接速率限制
from litestar.middleware.rate_limit import RateLimitMiddleware
from litestar.middleware.rate_limit.backends.memory import MemoryRateLimitBackend
config = AppConfig(
middleware=[
RateLimitMiddleware(
backend=MemoryRateLimitBackend(),
config={"websocket": {"rate": 10, "period": 60}} # 每分钟10个连接
)
]
)
消息丢失处理
实现消息确认机制确保数据可靠传输:
# 服务端消息确认
async def on_receive(self, data: dict) -> dict:
message_id = data.get("message_id")
# 处理消息...
return {"status": "received", "message_id": message_id}
# 客户端消息重发逻辑
class WebSocketClient:
constructor() {
this.pendingMessages = new Map(); // 存储待确认消息
this.messageId = 0;
}
send(data) {
const message = {
...data,
message_id: this.messageId++,
timestamp: Date.now()
};
this.pendingMessages.set(message.message_id, message);
this.socket.send(JSON.stringify(message));
// 设置超时重发
setTimeout(() => {
if (this.pendingMessages.has(message.message_id)) {
this.socket.send(JSON.stringify(message)); // 重发
}
}, 5000);
}
onmessage(event) {
const data = JSON.parse(event.data);
if (data.status === "received" && data.message_id) {
this.pendingMessages.delete(data.message_id);
}
}
}
总结与展望
WebSocket断线重连是确保实时应用稳定性的关键技术,本文介绍了基于Litestar框架的完整实现方案:
- 服务端:通过连接生命周期管理、会话持久化和心跳检测实现可靠连接
- 客户端:使用指数退避算法和状态管理实现智能重连
- 双向保障:消息确认机制和状态恢复确保数据不丢失
未来Litestar可能会内置重连支持,但目前通过本文介绍的方法已能构建生产级的实时应用。建议结合监控系统持续跟踪连接状态,不断优化重连策略。
收藏本文并关注后续更新,下一篇将介绍"WebSocket集群部署与水平扩展"。如有疑问或建议,请在评论区留言讨论。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



