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

Next.js+Socket.io聊天应用:客户端无法接收服务端message事件

问题:Next.js + Socket.io 聊天应用无法接收message事件

我正在用Next.js和Socket.io开发双人密码验证聊天房间,服务端通过API路由实现。预期流程:
密码验证通过 → 客户端触发joinRoom事件(服务端已成功接收) → 用户发送消息触发sendMessage事件(服务端已成功接收) → 服务端向房间广播message事件 → 客户端接收并展示消息

但目前客户端无法收到message事件,socket连接状态正常,服务端日志显示已编码发送该事件。


相关代码

pages/room/[id].tsx 页面代码

export default function ChatRoomID({
    id
}: {
    id: string
}) {
    const [socket, setSocket] = useState<Socket | null>(null);
    useEffect(() => {
        const socket = io();
        setSocket(socket);

        return () => {
            socket.disconnect();
        };
    }, []);

    function handleSubmit(password: string) {
        // ...客户端密码验证逻辑
        if (socket) {
            socket.emit(
                'joinRoom',
                id,
                (error: string | null, message: string) => {
                    if (error) {
                        toast({
                            title: 'Error',
                            description: error,
                        });
                    }
                }
            );
        }
    }

    return (
      <Chatbox id={id} socket={socket} />
    )
}

ChatBox组件代码

export default function ChatBox({ socket, id }: { socket: Socket | null; id: string }) {
  const [messages, setMessages] = useState<string[]>([]);

  useEffect(() => {
    if (!socket) return;

    console.log(socket.connected) // 输出true
    
    socket.on('message', (data: { message: string; roomId: string }) => {
      const { message, roomId } = data;
      console.log('Received message:', message); // 无输出
      setMessages((prevMessages) => [...prevMessages, message]);
    });

    return () => {
      socket.off('message');
    };
  }, [socket]);

  const handleMessageSend = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    if (socket) {
      const { message } = Object.fromEntries(new FormData(e.currentTarget));
      socket.emit('sendMessage', id, message); // 服务端能收到该事件
      e.currentTarget.reset();
      return;
    }
  };

  return (
    // 聊天框UI代码
  )
}

Socket.io服务端API路由代码

export default function handler(req: NextApiRequest, res: CustomApiResponse) {
  if (!res.socket.server.io) {
    const io = new Server(res.socket.server);
    res.socket.server.io = io;

    io.on('connection', (socket: Socket) => {
      console.log('io connection received');

      socket.on(
        'joinRoom',
        async (roomId: string, callback: Function) => {
          const roomDetails = await kv.get(roomId);

          if (!roomDetails) {
            return callback('Room not found', null);
          }

          const roomClients = io.sockets.adapter.rooms.get(roomId);
          if (roomClients && roomClients.size >= 2) {
            return callback('Room is full', null);
          }

          socket.join(roomId);
          callback(null, 'Successfully joined the room');
        }
      );

      socket.on('sendMessage', (roomId: string, message: string) => {
        console.log(`send message emitted`, roomId, message) // 服务端能打印该日志
        socket.to(roomId).emit('message', { message, roomId });
      });

    });
  }
  res.end();
}

调试日志

socket.io:socket emitting event ["sendMessage","3aa166ee-9c8c-419d-b213-6413bfef5e6f","a"] +1ms
socket.io:socket dispatching an event ["sendMessage","3aa166ee-9c8c-419d-b213-6413bfef5e6f","a"] +1ms
Send message emitted 3aa166ee-9c8c-419d-b213-6413bfef5e6f a
socket.io-parser encoding packet {"type":2,"data":["message",{"message":"a","roomId":"3aa166ee-9c8c-419d-b213-6413bfef5e6f"}],"nsp":"/"} +4ms
socket.io-parser encoded {"type":2,"data":["message",{"message":"a","roomId":"3aa166ee-9c8c-419d-b213-6413bfef5e6f"}],"nsp":"/"} as 2["message",{"message":"a","roomId":"3aa166ee-9c8c-419d-b213-6413bfef5e6f"}] +1ms
engine:transport readyState updated from closing to closed (polling) +30s
engine:socket writing ping packet - expecting pong within 20000ms +659ms
engine:socket sending packet "ping" (undefined) +0ms
engine:socket flushing buffer to transport +1ms
engine:ws writing "2" +662ms
engine:ws received "3" +2ms
engine:socket received packet pong +3ms
engine:socket got pong +1ms

问题排查与修复方案

1. 核心问题:socket.to(roomId)的广播范围

socket.to(roomId)只会向房间内除发送者自身外的客户端广播消息。如果测试时只有单个客户端在房间里,自然收不到消息。

修复:

  • 测试时必须用两个独立客户端(比如两个浏览器窗口/隐身窗口)加入同一房间
  • 如果需要给自己也发送消息,将服务端广播逻辑改为:
    socket.on('sendMessage', (roomId: string, message: string) => {
      console.log(`send message emitted`, roomId, message);
      // 向房间内所有客户端广播(包括发送者)
      io.to(roomId).emit('message', { message, roomId });
    });
    

2. 事件监听与房间加入时机匹配

当前页面直接渲染ChatBox,未等待joinRoom成功回调,可能出现「客户端已注册监听,但socket还未加入房间」的情况。

修复:
在页面中加入状态控制,仅当joinRoom成功后才渲染聊天框:

// pages/room/[id].tsx
const [isJoined, setIsJoined] = useState(false);

function handleSubmit(password: string) {
  // ...密码验证
  if (socket) {
    socket.emit(
      'joinRoom',
      id,
      (error: string | null, message: string) => {
        if (error) {
          toast({ title: 'Error', description: error });
        } else {
          setIsJoined(true); // 加入成功后再显示聊天框
        }
      }
    );
  }
}

return isJoined ? <Chatbox id={id} socket={socket} /> : <PasswordForm onSubmit={handleSubmit} />;

3. 确认房间成员有效性

在服务端joinRoom和sendMessage中打印房间成员数,确保两个客户端都成功加入:

// joinRoom中
const roomClients = io.sockets.adapter.rooms.get(roomId);
console.log(`Room ${roomId} has ${roomClients?.size || 0} clients`);

// sendMessage中
const roomClients = io.sockets.adapter.rooms.get(roomId);
console.log(`Broadcasting to room ${roomId}, clients count: ${roomClients?.size || 0}`);

架构优化建议

  1. 服务端初始化优化
    将Socket.io服务逻辑抽离为单独文件,避免API路由代码臃肿,确保服务端仅初始化一次:
// lib/socket.ts
import { Server } from 'socket.io';

let io: Server | null = null;

export function initSocketServer(server: any) {
  if (!io) {
    io = new Server(server);
    // 连接、事件监听逻辑放在这里
    io.on('connection', (socket) => {
      // ...原有的joinRoom、sendMessage、disconnect逻辑
    });
  }
  return io;
}

export function getSocketServer() {
  if (!io) throw new Error('Socket server not initialized');
  return io;
}

在API路由中调用:

// pages/api/socket.ts
import { initSocketServer } from '../../lib/socket';

export default function handler(req: NextApiRequest, res: CustomApiResponse) {
  initSocketServer(res.socket.server);
  res.end();
}
  1. 房间状态管理增强
  • 给房间添加过期时间,定期清理无效房间
  • 客户端断开连接时自动更新房间状态,空房间直接删除:
    io.on('connection', (socket) => {
      // ...其他逻辑
      socket.on('disconnect', async () => {
        const rooms = Array.from(socket.rooms).filter(room => room !== socket.id);
        for (const roomId of rooms) {
          const roomClients = io.sockets.adapter.rooms.get(roomId);
          if (!roomClients || roomClients.size === 0) {
            await kv.del(roomId); // 删除空房间
          }
        }
      });
    });
    
  1. 密码验证移至服务端
    避免客户端泄露密码逻辑,服务端存储密码哈希,joinRoom时验证客户端传入的密码:
// joinRoom逻辑
socket.on('joinRoom', async (roomId: string, password: string, callback: Function) => {
  const roomDetails = await kv.get(roomId);
  if (!roomDetails) return callback('Room not found', null);
  
  // 验证密码哈希(示例用bcrypt)
  const isPasswordValid = await bcrypt.compare(password, roomDetails.passwordHash);
  if (!isPasswordValid) return callback('Invalid password', null);
  
  // ...房间人数检查逻辑
  socket.join(roomId);
  callback(null, 'Successfully joined the room');
});
  1. TypeScript类型安全
    定义Socket.io事件类型,避免事件名和数据类型不匹配:
// types/socket.ts
export type ServerToClientEvents = {
  message: (data: { message: string; roomId: string }) => void;
};

export type ClientToServerEvents = {
  joinRoom: (roomId: string, password: string, callback: (error: string | null, message: string) => void) => void;
  sendMessage: (roomId: string, message: string) => void;
};

// 服务端
import { Server } from 'socket.io';
import type { ClientToServerEvents, ServerToClientEvents } from '../types/socket';
const io = new Server<ClientToServerEvents, ServerToClientEvents>(res.socket.server);

// 客户端
import { io, Socket } from 'socket.io-client';
import type { ClientToServerEvents, ServerToClientEvents } from '../types/socket';
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 00:45:00