NodeJS与ReactJS部署后WebSocket反复连接断开问题排查
问题场景
已在服务器部署React.js PWA前端和NodeJS后端,通过Nginx和PM2运行正常。后端创建的WebSocket在本地环境运行正常,但部署到服务器后,连接反复建立又断开,控制台仅打印“WebSocket connected”和“WebSocket disconnected”,无错误信息。
现有配置信息
NodeJS WebSocket配置
const clients = new Map(); exports.setupWebSocket = (server) => { const wss = new WebSocket.Server({ server }); wss.on("connection", (ws, req) => { const cookie = decodeURIComponent(req.headers.cookie); const startTokenIndex = cookie.indexOf("token=") + "token=".length; const endTokenIndex = cookie.indexOf(";"); const tokenData = cookie.substring(startTokenIndex, endTokenIndex); // Extract the token information const decodedToken = decodeURIComponent(tokenData); const parsedToken = JSON.parse(decodedToken); // Use the user ID const userId = parsedToken._id; if (!clients.has(userId)) { clients.set(userId, new Set()); } clients.get(userId).add(ws); ws.on("close", () => { const userSet = clients.get(userId); if (userSet) { userSet.delete(ws); // Remove specific WebSocket connection if (userSet.size === 0) { clients.delete(userId); // Remove the user if no connections left } } }); }); };
app.js配置
const express = require("express"); const cors = require("cors"); const config = require("./config/server"); const swaggerUi = require("swagger-ui-express"); const swaggerOutput = require("./doc/swagger.json"); const { setupWebSocket } = require("./controllers/log"); const app = express(); app.use(cors(config.cors.corsOption)); app.use((req, res, next) => { res.header( "Access-Control-Allow-Headers", "x-access-token, Origin, Content-Type, Accept" ); next(); }); app.use(express.json({ limit: "50mb" })); app.use(express.urlencoded({ limit: "50mb", extended: false })); app.use("/doc", swaggerUi.serve, swaggerUi.setup(swaggerOutput)); app.get("/", (req, res) => { res.send("Welcome"); }); module.exports = { app, setupWebSocket, };
server.js配置
const { app, setupWebSocket } = require("../app"); const config = require("../config/server"); const http = require("http"); const server = http.createServer(app); // Configure the websocket setupWebSocket(server); module.exports = (err) => { if (err) { console.log("Error connecting to the DB"); console.log(err); } server.listen(process.env.PORT || config.app.port, (err) => { if (err) { console.log(err); } console.log(`Starts in ${config.app.url}:${process.env.PORT || config.app.port}`); }); };
Nginx配置
server { server_name subdomain.domain.com; location / { proxy_pass http://MYIP:MYPORT; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } # SSL CONFIG listen 443 ssl http2; ssl_certificate /etc/ssl/cert.crt; ssl_certificate_key /etc/ssl/cert.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; }
React前端连接代码
const ws = new ReconnectingWebSocket( `wss://${process.env.REACT_APP_DOMAIN}` ); ws.onopen = () => { console.log("WebSocket connected"); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); }; ws.onclose = () => { console.log("WebSocket disconnected"); }; ws.onerror = (error) => { console.error("Error:", error); };
注:process.env.REACT_APP_DOMAIN的值为http://MYIP:MYPORT
问题原因与解决方案
1. 前端WebSocket连接地址不匹配
当前前端使用wss://前缀拼接http://MYIP:MYPORT,这会导致协议冲突:wss对应HTTPS的WebSocket协议,需要通过配置了SSL的域名(即subdomain.domain.com)访问,而非直接使用IP+端口的HTTP地址。
修复方案:
修改前端连接地址为域名的WSS地址,同时更新REACT_APP_DOMAIN环境变量为subdomain.domain.com:
const ws = new ReconnectingWebSocket( `wss://${process.env.REACT_APP_DOMAIN}` );
2. 后端Cookie解析逻辑存在健壮性问题
原代码中如果Cookie里没有分号,cookie.indexOf(";")会返回-1,导致substring(startTokenIndex, endTokenIndex)截取到无效字符串,后续JSON.parse会抛出未捕获异常,直接导致WebSocket连接被断开。
修复方案:
修改WebSocket连接的Cookie解析逻辑,添加错误捕获和正则匹配,避免解析失败:
wss.on("connection", (ws, req) => { try { const cookie = req.headers.cookie; if (!cookie) { ws.close(401, "No cookie provided"); return; } const decodedCookie = decodeURIComponent(cookie); // 用正则匹配token值,兼容有无分号的情况 const tokenMatch = decodedCookie.match(/token=([^;]+)/); if (!tokenMatch) { ws.close(401, "Token not found in cookie"); return; } const tokenData = tokenMatch[1]; const decodedToken = decodeURIComponent(tokenData); const parsedToken = JSON.parse(decodedToken); const userId = parsedToken._id; if (!clients.has(userId)) { clients.set(userId, new Set()); } clients.get(userId).add(ws); ws.on("close", () => { const userSet = clients.get(userId); if (userSet) { userSet.delete(ws); if (userSet.size === 0) { clients.delete(userId); } } }); } catch (error) { console.error("WebSocket connection error:", error); ws.close(400, "Invalid token or cookie"); } });
3. Nginx代理超时时间过短
默认Nginx代理的超时时间较短,若WebSocket长时间无数据传输,Nginx会主动断开连接。
修复方案:
在Nginx的location /块中添加超时配置,延长空闲连接存活时间:
location / { proxy_pass http://MYIP:MYPORT; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; # 添加超时配置 proxy_connect_timeout 7d; proxy_send_timeout 7d; proxy_read_timeout 7d; }
4. 缺少WebSocket心跳机制
即使延长了Nginx超时时间,服务器防火墙或网络中间件仍可能断开长时间空闲的连接,需要通过心跳机制保持连接活跃。
修复方案:
后端添加心跳发送逻辑
wss.on("connection", (ws, req) => { // ... 之前的解析和客户端存储逻辑 // 每30秒发送一次心跳 const heartbeatInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "heartbeat" })); } }, 30000); ws.on("close", () => { clearInterval(heartbeatInterval); // ... 之前的关闭清理逻辑 }); });
前端添加心跳回复逻辑
ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === "heartbeat") { // 回复心跳,保持连接活跃 ws.send(JSON.stringify({ type: "heartbeat-ack" })); } else { // 处理业务消息 } };
内容的提问来源于stack exchange,提问作者Guilherme Hepfener

