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

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">&#9679;</span>
              ) : (
                <span className="ml-2 text-black">&#9679;</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却收到该消息

场景说明

Noor选择Stephen发送消息,但登录Amir的标签页却收到了这条消息。


解决方案

核心问题分析

代码的关键错误是混淆了Socket.IO的socket.id和业务层用户ID:

  1. 后端连接时用socket.id(Socket.IO生成的临时连接ID)作为房间名,但前端传递的receiverId是业务用户ID(如MongoDB的_id),两者完全不匹配
  2. 调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 18:32:02