TypeScript上的WebSocket:构建高级实时应用
\n\n实时应用已成为现代网络不可或缺的一部分。从聊天和通知到在线游戏和交易平台——WebSocket技术确保了客户端与服务器之间的持续双向连接。在高级层面,开发此类系统不仅需要理解协议,还需要能够构建高可用、可扩展的架构。在本文中,我们将探讨如何使用最佳实践和模式在TypeScript上创建生产级WebSocket应用。
\n\n1. WebSocket服务器的架构与集群
\n\n单个WebSocket服务器无法承受真实项目的负载。关键任务是组织集群并在实例之间交换消息。我们将探讨使用ws库和Redis事件总线的架构。
安装依赖:
\nnpm install ws redis ioredis @types/ws @types/ioredis\n\n创建一个基本的集群服务器:
\n\nimport { WebSocketServer, WebSocket } from 'ws';\nimport Redis from 'ioredis';\n\nconst redisPub = new Redis();\nconst redisSub = new Redis();\n\nconst wss = new WebSocketServer({ port: 8080 });\n\n// 用于服务器间通信的频道\nconst CHANNEL = 'ws:messages';\n\nredisSub.subscribe(CHANNEL, (err, count) => {\n if (err) console.error('订阅错误:', err);\n console.log(`已订阅 ${count} 个频道`);\n});\n\n// 从其他实例接收消息\nredisSub.on('message', (channel, message) => {\n const parsed = JSON.parse(message);\n wss.clients.forEach((client) => {\n if (client.readyState === WebSocket.OPEN) {\n client.send(parsed.data);\n }\n });\n});\n\nwss.on('connection', (ws) => {\n console.log('新连接');\n\n ws.on('message', (data) => {\n const message = data.toString();\n // 发布到Redis供所有实例使用\n redisPub.publish(CHANNEL, JSON.stringify({ data: message }));\n });\n});\n\n优势:
\n- \n
- 水平扩展——可以添加任意数量的服务器 \n
- 高可用性——当一个实例宕机时,其他实例继续工作 \n
- 通过Redis Pub/Sub实现统一消息总线 \n
2. 认证与会话管理
\n\n在生产级应用中,不能信任未经认证的连接。我们将在建立连接时通过JWT实现令牌验证。
\n\n安装jsonwebtoken:
npm install jsonwebtoken @types/jsonwebtoken\n\n创建WebSocket认证中间件:
\n\nimport { IncomingMessage } from 'http';\nimport jwt from 'jsonwebtoken';\n\nconst JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';\n\ninterface AuthenticatedSocket extends WebSocket {\n userId?: string;\n}\n\nfunction authenticate(ws: AuthenticatedSocket, req: IncomingMessage): boolean {\n const url = new URL(req.url || '', 'http://localhost');\n const token = url.searchParams.get('token');\n\n if (!token) {\n ws.close(4001, '未提供令牌');\n return false;\n }\n\n try {\n const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };\n ws.userId = decoded.userId;\n console.log(`用户 ${decoded.userId} 已认证`);\n return true;\n } catch (err) {\n ws.close(4002, '无效令牌');\n return false;\n }\n}\n\n// 使用\nwss.on('connection', (ws: AuthenticatedSocket, req) => {\n if (!authenticate(ws, req)) return;\n\n ws.on('message', (data) => {\n // 现在我们知道了userId\n console.log(`来自 ${ws.userId} 的消息: ${data}`);\n });\n});\n\n建议:
\n- \n
- 在建立连接时通过查询参数或头部传递令牌 \n
- 使用短生命周期的访问令牌和刷新令牌 \n
- 将会话存储在Redis中