刷新浏览器时Socket.io触发栈溢出致Node.js服务崩溃
问题:Node.js+Socket.io刷新浏览器导致服务器栈溢出崩溃
初始WebSocket连接正常,但刷新浏览器时服务器立即崩溃,抛出「RangeError: Maximum call stack size exceeded」错误。已将问题范围缩小到断开连接的逻辑部分,移除io.emit('updateUserList', this.connectedUsers)语句后崩溃现象停止。期望刷新浏览器时,客户端能正常断开并重连,而非导致服务器崩溃。
相关代码
server.js
export const io = new Server(4006, { cors: { origin: "http://192.168.0.19:5173", credentials: true } }) const getSocketByUserId = (userId) => { let socket = ''; for (let i = 0; i < clientSocketIds.length; i++) { if (clientSocketIds[i].userId == userId) { socket = clientSocketIds[i].socket; break; } } return socket; } io.use(async (socket, next) => { console.log(socket.handshake.auth.userId) if(socket.handshake.auth.userId) { messageController.socketUserAuthentication(socket, next) } else { console.log('Already connected') } console.log('Authenticated') }).on('connection', socket => { console.log('conected') messageController.storeConnectedUser(socket) socket.on('disconnect', () => { messageController.handleUserDisconnect(socket) }); socket.on('create', function (data) { console.log("create room") socket.join(data.room); let withSocket = getSocketByUserId(data.withUserId); socket.broadcast.to(withSocket.id).emit("invite", { room: data }) }); socket.on('joinRoom', function (data) { socket.join(data.room.room); }); socket.on('message', function (data) { socket.broadcast.to(data.room).emit('message', data); }) });
message.controller.js
import userModel from '../user/user.model.js' import emailModel from '../emails/email.model.js' import calendarModel from '../calendar/calendar.model.js' import { HttpException } from '../../utility/HttpException.utility.js' import { validationResult } from 'express-validator' import bcrypt from 'bcryptjs' import jwt from 'jsonwebtoken' import * as dotenv from 'dotenv' import { logger } from '../../utility/logger.js' import { isEmpty } from '../../utility/common.utility.js' import { addHours, parseISO } from 'date-fns' import _ from 'lodash' import { io } from '../../server.js' import { v4 as uuidv4 } from 'uuid' import { user_type } from '../../utility/userRoles.utility.js' dotenv.config() class messageController { constructor() { this.connectedUsers = [] this.clientSocketIds = [] } test = () => { console.log('CLIENT SOCKET IDS',this.clientSocketIds) console.log('CONNECTED USERS',this.connectedUsers) } getSocketByUserId = (userId) => { let socket = '' for (let i = 0; i < this.clientSocketIds.length; i++) { if (clientSocketIds[i].userId == userId) { socket = clientSocketIds[i].socket break } } return socket } socketUserAuthentication = async (socket, next) => { try { this.test() console.log('Authenticate') const authHeader = socket.handshake.auth.token const id = socket.handshake.auth.userId console.log(authHeader) console.log(id) const bearer = 'Bearer ' if (!authHeader) { console.log(authHeader) return new HttpException(401, 'Access denied. No credentials sent!') } const token = authHeader.replace(bearer, '') const secretKey = process.env.SECRET_JWT || "" const decoded = jwt.verify(token, secretKey) const user = await userModel.findUser({ userId: decoded.user_id }) if (!user) { return new HttpException(401, 'Authentication failed!') } const ownerAuthorized = id == user.userId if (!ownerAuthorized && roles.length && !roles.includes(user.user_type)) { return new HttpException(401, 'Unauthorized') } socket.currentUser = user; return next(); } catch (e) { e.status = 401; console.log(e) next(e); } } storeConnectedUser = (socket) => { this.clientSocketIds.push({ socket: socket, userId: socket.currentUser.userId, username: socket.currentUser.username }) this.connectedUsers = this.connectedUsers.filter(item => item.userId != socket.currentUser.userId) this.connectedUsers.push({ username: socket.currentUser.username, userId: socket.currentUser.userId, uuid: uuidv4(), socket: socket }) return socket } handleUserDisconnect = (socket) => { this.connectedUsers = this.connectedUsers.filter(item => item.socketId != socket.id) io.emit('updateUserList', this.connectedUsers) return socket } } export default new messageController
解决方案
问题根源
- 循环引用导致序列化栈溢出:
this.connectedUsers中存储了完整的socket对象,该对象内部包含大量循环引用(比如socket引用io实例,io又引用所有socket连接、事件监听器等)。当io.emit尝试将这个数组序列化为JSON时,JSON.stringify会陷入无限递归,最终触发栈溢出错误。 - 过滤条件错误:
handleUserDisconnect中的filter(item => item.socketId != socket.id)是无效的——存储的用户项里并没有socketId字段,只有socket对象。这导致断开的用户无法从数组中移除,无效socket对象越积越多,进一步加重序列化负担。 - 作用域错误:
getSocketByUserId方法中错误引用了全局的clientSocketIds,而非类实例的this.clientSocketIds,导致无法正确查找用户对应的socket。
修复步骤
1. 修正断开连接逻辑,移除socket对象的序列化
修改handleUserDisconnect方法,正确过滤断开的用户,并仅向前端发送必要的业务字段(不包含socket对象):
handleUserDisconnect = (socket) => { // 用socket.id匹配存储项中的socket.id,正确移除断开的用户 this.connectedUsers = this.connectedUsers.filter(item => item.socket.id !== socket.id) // 同时清理clientSocketIds中的无效记录 this.clientSocketIds = this.clientSocketIds.filter(item => item.socket.id !== socket.id) // 仅发送前端需要的字段,避免序列化socket对象 io.emit('updateUserList', this.connectedUsers.map(user => ({ username: user.username, userId: user.userId, uuid: user.uuid }))) return socket }
2. 优化连接存储逻辑,发送用户列表时避免socket序列化
修改storeConnectedUser方法,确保重复连接的用户被正确覆盖,并且发送用户列表时只传必要字段:
storeConnectedUser = (socket) => { // 先清理该用户已有的socket记录,避免重复 this.clientSocketIds = this.clientSocketIds.filter(item => item.userId !== socket.currentUser.userId) this.clientSocketIds.push({ socket: socket, userId: socket.currentUser.userId, username: socket.currentUser.username }) this.connectedUsers = this.connectedUsers.filter(item => item.userId !== socket.currentUser.userId) this.connectedUsers.push({ username: socket.currentUser.username, userId: socket.currentUser.userId, uuid: uuidv4(), socket: socket }) // 仅发送前端需要的字段 io.emit('updateUserList', this.connectedUsers.map(user => ({ username: user.username, userId: user.userId, uuid: user.uuid }))) return socket }
3. 修复getSocketByUserId的作用域错误
修改方法内的引用,使用类实例的this.clientSocketIds:
getSocketByUserId = (userId) => { let socket = '' for (let i = 0; i < this.clientSocketIds.length; i++) { // 修正为this.clientSocketIds if (this.clientSocketIds[i].userId == userId) { socket = this.clientSocketIds[i].socket break } } return socket }
验证效果
修复后,刷新浏览器时:
- 断开连接逻辑会正确移除用户的socket记录
- 发送用户列表时仅传递业务字段,避免了循环引用的序列化问题
- 服务器不会再出现栈溢出崩溃,客户端可正常断开并重连
内容的提问来源于stack exchange,提问作者MantasDev
相关产品推荐
相关产品推荐

