Litestar WebSocket断线重连:客户端与服务端实现

Litestar WebSocket断线重连:客户端与服务端实现

【免费下载链接】litestar Production-ready, Light, Flexible and Extensible ASGI API framework | Effortlessly Build Performant APIs 【免费下载链接】litestar 项目地址: https://gitcode.com/GitHub_Trending/li/litestar

引言:实时通信的稳定性挑战

你是否曾遇到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框架的完整实现方案:

  1. 服务端:通过连接生命周期管理、会话持久化和心跳检测实现可靠连接
  2. 客户端:使用指数退避算法和状态管理实现智能重连
  3. 双向保障:消息确认机制和状态恢复确保数据不丢失

未来Litestar可能会内置重连支持,但目前通过本文介绍的方法已能构建生产级的实时应用。建议结合监控系统持续跟踪连接状态,不断优化重连策略。

收藏本文并关注后续更新,下一篇将介绍"WebSocket集群部署与水平扩展"。如有疑问或建议,请在评论区留言讨论。

【免费下载链接】litestar Production-ready, Light, Flexible and Extensible ASGI API framework | Effortlessly Build Performant APIs 【免费下载链接】litestar 项目地址: https://gitcode.com/GitHub_Trending/li/litestar

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值