You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于ReactJS与Socket.io的在线聊天应用:断连消息重复问题求助

解决断开连接消息重复发送的问题

看起来你遇到的重复消息问题主要来自重复创建Socket连接、错误的房间操作以及不必要的自定义disconnect事件这几个点,我来一步步帮你修正:

问题根源分析

  1. 客户端重复创建Socket实例:你在组件顶层直接声明const socket = io(ENDPOINT),这会导致组件每次渲染(比如location.search变化时)都创建新的Socket连接,最终多个连接同时存在。当组件卸载或清理时,每个连接都会触发服务端的disconnect事件,自然会重复发送消息。
  2. 错误的房间操作:服务端里socket.join({roomCode})和io.to({roomCode})是错误的——Socket.io的房间名需要是字符串,不是对象,这会导致房间无法正确创建/发送消息。
  3. 不必要的自定义disconnect事件:Socket.io会自动处理客户端断开,你手动emit的disconnect事件和内置事件重名,反而会引发额外的触发。

修正后的客户端代码

const ChatRoom = ({ location }) => {
  const { userName, setUserName } = useContext(ChatContext)
  const { roomCode, setRoomCode } = useContext(ChatContext)
  const [message, setMessage] = useState('')
  const [chats, setChats] = useState([])
  const ENDPOINT = 'localhost:5000'
  // 使用useRef保存socket实例,避免重复创建
  const socketRef = useRef(null)

  useEffect(() => {
    // 仅当socket未初始化时创建连接
    if (!socketRef.current) {
      socketRef.current = io(ENDPOINT)
    }
    const socket = socketRef.current
    const { userName: queryUserName, roomCode: queryRoomCode } = queryString.parse(location.search)
    
    setUserName(queryUserName)
    setRoomCode(queryRoomCode)
    socket.emit('chatroom-join', {userName: queryUserName, roomCode: queryRoomCode})

    // 清理函数:断开连接并移除监听
    return () => {
      socket.disconnect()
      socket.off()
      socketRef.current = null
    }
  }, [location.search, ENDPOINT, setUserName, setRoomCode])

  useEffect(() => {
    const socket = socketRef.current
    if (!socket) return
    
    const handleMessage = ({userName, message}) => {
      setChats(chats => [...chats, {userName, message}])
    }
    socket.on('message', handleMessage)
    
    // 清理消息监听
    return () => {
      socket.off('message', handleMessage)
    }
  }, [])

  // 其他代码...
}

修正后的服务端代码

// Socket.io API
io.on('connection', (socket) => {
  socket.on('chatroom-join', ({userName, roomCode}) => {
    console.log('User joined')
    // 保存用户信息到socket对象,断开时可以使用
    socket.userName = userName
    socket.roomCode = roomCode
    // 正确加入房间:传入字符串roomCode
    socket.join(roomCode)
    // 仅向当前房间发送加入消息
    io.to(roomCode).emit('message', {userName: 'admin', message: `${userName} has joined`})
  })

  socket.on('message', ({userName, message}) => {
    // 同样,只向当前房间发送用户消息
    io.to(socket.roomCode).emit('message', {userName, message})
  })

  socket.on('disconnect', () => {
    if (socket.roomCode && socket.userName) {
      // 仅向用户所在房间发送离开消息,并且显示具体用户名
      io.to(socket.roomCode).emit('message', {userName: 'admin', message: `${socket.userName} has left`})
    }
    console.log('User disconnected')
    // 不需要手动调用socket.disconnect(true),内置事件已经处理
  })
})

额外说明

  • 使用useRef保存Socket实例可以确保整个组件生命周期内只有一个连接,避免重复连接导致的重复事件触发。
  • 服务端在用户加入时把userName和roomCode绑定到socket对象上,断开时就能精准发送带有用户名的离开消息,而不是模糊的"An user has left"。
  • 清理函数里移除对应的事件监听和断开连接,避免内存泄漏和残留连接。

内容的提问来源于stack exchange,提问作者Henry

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 06:27:30