MERN Stack中Socket.IO私聊消息投递异常问题求助
MERN Stack中Socket.IO私聊消息投递错误问题
问题描述
在Node.js、Express.js和React.js(MERN Stack)中使用Socket.IO实现私聊功能时遇到以下问题:
- 前后端Socket.IO连接已成功建立,能正常发送和接收消息
- 消息无法投递到选定的目标用户:比如用户Saim给Rob发消息,在线的Stephen却收到该消息;Stephen给Frank发消息,Saim也会收到不属于自己的消息
后端代码
const io = new socketIo.Server(server, { cors: { origin: "http://localhost:3000", methods: ["GET", "POST"], credentials: true, }, }); app.set("socketIo", io); const onlineUsers = new Set(); io.on("connection", (socket) => { const userId = socket.id; if (userId) { socket.join(userId); console.log("A user connected with ID", socket.id); socket.on("createNotification", (newNotification) => { console.log("New notification ====>", newNotification); socket.broadcast.emit("newNotification", newNotification); }); // Listen for a user going online socket.on("userOnline", (userId) => { console.log("User ID online ====>", userId); onlineUsers.add(userId); io.emit("updateUserStatus", Array.from(onlineUsers)); }); // Listen for a user going offline socket.on("userOffline", (userId) => { onlineUsers.delete(userId); io.emit("updateUserStatus", Array.from(onlineUsers)); }); socket.on("sendChatMessage", (newMessage) => { const recipientSocketId = newMessage.receiverId; console.log("recipientSocketId", recipientSocketId); console.log("user socket.id ====>", userId); if (recipientSocketId) { console.log("New Message ====>", recipientSocketId, newMessage); io.to(recipientSocketId).emit("receive_message", newMessage); } else { console.log("Message not sent to the receiver"); } }); } socket.on("disconnect", () => { console.log("A user disconnected with id ", socket.id); }); }); io.on("error", (error) => { console.error("Socket.IO error:", error); });
前端代码
import React, { useState, useEffect } from "react"; import { useQuery } from "react-query"; import io from "socket.io-client"; import axios from "axios"; import { SendHorizontal } from "lucide-react"; const socket = io.connect("http://localhost:5000"); const Chat = () => { const fetchUsers = async () => { const response = await fetch("/api/user"); const data = await response.json(); return data; }; const { data: users, isLoading, isError } = useQuery("users", fetchUsers); const loginUser = JSON.parse(localStorage.getItem("user")); const [selectedUser, setSelectedUser] = useState(null); const [messages, setMessages] = useState([]); const [messageText, setMessageText] = useState(""); const { data: messageData } = useQuery( ["messages", selectedUser?._id], () => fetchMessages(selectedUser?._id, loginUser?.result?._id), { enabled: !!selectedUser?._id, } ); useEffect(() => { if (messageData) { setMessages(messageData); } }, [messageData]); const [onlineUsers, setOnlineUsers] = useState([]); // Initialize onlineUsers state const isUserOnline = (userId) => { return onlineUsers.includes(userId); }; useEffect(() => { socket.emit("userOnline", loginUser.result._id); return () => { socket.emit("userOffline", loginUser.result._id); }; }, [loginUser.result._id]); useEffect(() => { const updateUserStatusListener = (onlineUserIds) => { setOnlineUsers(onlineUserIds); }; socket.on("updateUserStatus", updateUserStatusListener); return () => { socket.off("updateUserStatus", updateUserStatusListener); }; }, []); useEffect(() => { socket.on("receive_message", (newMessage) => { setMessages((prevMessages) => [...prevMessages, newMessage]); }); return () => { socket.off("receive_message"); }; }, []); const handleSendMessage = async (e) => { e.preventDefault(); if (messageText.trim() !== "") { const newMessage = { senderId: loginUser.result._id, receiverId: selectedUser._id, content: messageText, }; setMessageText(""); try { const response = await axios.post("/api/send", newMessage); if (response.status === 201) { socket.emit("sendChatMessage", newMessage); setMessages([...messages, newMessage]); } else { console.log("Failed to send message"); } } catch (error) { console.error("Error sending message:", error); } } }; if (isLoading) { return <div>Loading...</div>; } if (isError) { return <div>Error fetching users</div>; } const filteredUsers = users.filter( (user) => user._id !== loginUser?.result?._id ); const fetchMessages = async (receiverId, currentUserId) => { try { const response = await axios.get( `/api/messages/${currentUserId}/${receiverId}` ); return response.data; } catch (error) { console.error("Error fetching messages:", error); throw error; } }; return ( <div className="flex h-screen bg-gray-100"> <div className="w-1/4 bg-white p-4 overflow-y-auto"> <h2 className="text-xl text-center font-semibold mb-8">Users</h2> <ul> {filteredUsers.map((user) => ( <li key={user._id} onClick={() => setSelectedUser(user)} className={`mb-4 border border-spacing-2 rounded-lg p-4 cursor-pointer text-xl ${ selectedUser && selectedUser._id === user._id ? "bg-blue-300" : "" }`} > {user.username} {isUserOnline(user._id) ? ( <span className="ml-2 text-green-500">●</span> ) : ( <span className="ml-2 text-black">●</span> )} </li> ))} </ul> </div> <div className="w-3/4 p-4 flex flex-col justify-between"> {selectedUser && ( <div> <h2 className="text-2xl text-white bg-gray-600 p-4 text-center w-full rounded-xl font-semibold"> Chat With {selectedUser.username} </h2> <div className="messages mb-4 mt-2"> {loginUser.result._id && messages.map((message, index) => ( <div key={index} className={`message mt-3 ${ message.senderId === loginUser.result._id ? "sent text-right" : "received" }`} > <span className={`${ message.senderId === loginUser.result._id ? " bg-emerald-300 " : " bg-amber-300" } align-middle rounded-2xl mr-1 px-2 py-1`} > {message.senderId === loginUser.result._id ? loginUser.result.username .substring(0, 1) .toUpperCase() : selectedUser.username.substring(0, 1).toUpperCase()} </span> <p className={`message-content ${ message.senderId === loginUser.result._id ? "bg-blue-500 text-white block ml" : "bg-gray-300 text-gray-700" } rounded-lg p-2 inline-block max-w-[70%] break-words`} > {message.content} </p> </div> ))} </div> </div> )} {selectedUser && ( <div className="message-input flex"> <input type="text" className="w-full border border-gray-400 rounded-md h-10 outline-none" value={messageText} onChange={(e) => setMessageText(e.target.value)} placeholder="Type your message..." onKeyPress={(e) => { if (e.key === "Enter") { handleSendMessage(e); } }} /> <SendHorizontal type="submit" onClick={(e) => handleSendMessage(e)} className="mt-1 w-8 h-8 text-gray-600" /> </div> )} </div> </div> ); }; export default Chat;
问题截图

场景说明
Noor选择Stephen发送消息,但登录Amir的标签页却收到了这条消息。
解决方案
核心问题分析
代码的关键错误是混淆了Socket.IO的socket.id和业务层用户ID:
- 后端连接时用
socket.id(Socket.IO生成的临时连接ID)作为房间名,但前端传递的receiverId是业务用户ID(如MongoDB的_id),两者完全不匹配 - 调用
io.to(recipientSocketId).emit(...)时,实际是往不存在的房间发消息,Socket.IO静默失败;同时之前的createNotification用了broadcast.emit,会导致所有在线用户收到消息,最终出现消息串线
修复步骤
1. 后端:维护业务用户ID与Socket连接的映射
替换原onlineUsers集合,用Map存储用户ID到socket.id的对应关系:
const userSocketMap = new Map(); // 键:业务用户ID,值:socket.id io.on("connection", (socket) => { console.log("A user connected with Socket ID", socket.id); // 用户上线时存储映射,并让socket加入以业务用户ID命名的房间 socket.on("userOnline", (userId) => { console.log("User online:", userId); userSocketMap.set(userId, socket.id); socket.join(userId); io.emit("updateUserStatus", Array.from(userSocketMap.keys())); }); // 用户下线时删除映射 socket.on("userOffline", (userId) => { userSocketMap.delete(userId); io.emit("updateUserStatus", Array.from(userSocketMap.keys())); }); // 处理私聊消息:通过用户ID找到对应的socket.id发送 socket.on("sendChatMessage", (newMessage) => { const receiverUserId = newMessage.receiverId; const receiverSocketId = userSocketMap.get(receiverUserId); console.log("Sending message to user:", receiverUserId, "Socket ID:", receiverSocketId); if (receiverSocketId) { io.to(receiverSocketId).emit("receive_message", newMessage); // 也可以用房间发送:io.to(receiverUserId).emit("receive_message", newMessage); } else { console.log("Receiver is offline or not found"); } }); // 断开连接时自动清理映射 socket.on("disconnect", () => { console.log("User disconnected with Socket ID:", socket.id); for (const [userId, sid] of userSocketMap.entries()) { if (sid === socket.id) { userSocketMap.delete(userId); io.emit("updateUserStatus", Array.from(userSocketMap.keys())); break; } } }); // 保留其他原有逻辑 });
2. 前端:增加调试日志(可选)
在发送消息时增加日志,方便确认传递的用户ID是否正确:
const handleSendMessage = async (e) => { e.preventDefault(); if (messageText.trim() === "") return; const newMessage = { senderId: loginUser.result._id, receiverId: selectedUser._id, content: messageText, }; console.log("Sending message to user ID:", selectedUser._id); setMessageText(""); try { const response = await axios.post("/api/send", newMessage); if (response.status === 201) { socket.emit("sendChatMessage", newMessage); setMessages(prev => [...prev, newMessage]); } } catch (error) { console.error("Error sending message:", error); } };
3. 额外优化建议
- 支持同一用户多设备登录:将
userSocketMap的值改为数组,存储该用户的所有socket.id - 前端监听
socket.disconnect事件,自动触发用户下线 - 给消息增加时间戳字段,用于消息排序和展示
内容的提问来源于stack exchange,提问作者Aamir Shahzad
相关产品推荐
相关产品推荐

