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}`);
架构优化建议
- 服务端初始化优化
将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(); }
- 房间状态管理增强
- 给房间添加过期时间,定期清理无效房间
- 客户端断开连接时自动更新房间状态,空房间直接删除:
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); // 删除空房间 } } }); });
- 密码验证移至服务端
避免客户端泄露密码逻辑,服务端存储密码哈希,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'); });
- 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
相关产品推荐
相关产品推荐

