12.4.3 构建在线聊天室——实时消息传递:房间管理与消息广播
一句话破题
房间机制让你可以把用户分组,只向特定群组广播消息——这是构建多聊天室应用的基础。
房间管理
typescript
// 服务端
io.on('connection', (socket) => {
// 加入房间
socket.on('joinRoom', (roomId: string) => {
socket.join(roomId);
socket.to(roomId).emit('userJoined', { userId: socket.id });
console.log(`${socket.id} 加入房间 ${roomId}`);
});
// 离开房间
socket.on('leaveRoom', (roomId: string) => {
socket.leave(roomId);
socket.to(roomId).emit('userLeft', { userId: socket.id });
});
// 发送消息到房间
socket.on('roomMessage', ({ roomId, message }) => {
io.to(roomId).emit('roomMessage', {
userId: socket.id,
message,
timestamp: Date.now(),
});
});
});广播方式对比
typescript
// 发送给所有人(包括自己)
io.emit('event', data);
// 发送给所有人(除了自己)
socket.broadcast.emit('event', data);
// 发送给特定房间所有人
io.to('roomId').emit('event', data);
// 发送给特定房间(除了自己)
socket.to('roomId').emit('event', data);
// 发送给特定用户
io.to(socketId).emit('event', data);完整聊天室实现
服务端
typescript
interface Message {
id: string;
userId: string;
username: string;
content: string;
timestamp: number;
}
interface Room {
id: string;
name: string;
users: Set<string>;
}
const rooms = new Map<string, Room>();
const users = new Map<string, { socketId: string; username: string }>();
io.on('connection', (socket) => {
// 用户登录
socket.on('login', (username: string) => {
users.set(socket.id, { socketId: socket.id, username });
socket.emit('loginSuccess', { userId: socket.id });
});
// 获取房间列表
socket.on('getRooms', () => {
const roomList = Array.from(rooms.values()).map((room) => ({
id: room.id,
name: room.name,
userCount: room.users.size,
}));
socket.emit('roomList', roomList);
});
// 加入房间
socket.on('joinRoom', (roomId: string) => {
const room = rooms.get(roomId);
if (room) {
socket.join(roomId);
room.users.add(socket.id);
const user = users.get(socket.id);
io.to(roomId).emit('userJoined', {
userId: socket.id,
username: user?.username,
});
}
});
// 发送消息
socket.on('sendMessage', ({ roomId, content }) => {
const user = users.get(socket.id);
const message: Message = {
id: Date.now().toString(),
userId: socket.id,
username: user?.username || '匿名',
content,
timestamp: Date.now(),
};
io.to(roomId).emit('newMessage', message);
});
// 断开连接
socket.on('disconnect', () => {
users.delete(socket.id);
rooms.forEach((room) => {
if (room.users.has(socket.id)) {
room.users.delete(socket.id);
io.to(room.id).emit('userLeft', { userId: socket.id });
}
});
});
});客户端
tsx
'use client';
import { useEffect, useState, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';
interface Message {
id: string;
userId: string;
username: string;
content: string;
timestamp: number;
}
export default function ChatRoom({ roomId }: { roomId: string }) {
const [socket, setSocket] = useState<Socket | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [users, setUsers] = useState<string[]>([]);
useEffect(() => {
const newSocket = io('http://localhost:3001');
setSocket(newSocket);
newSocket.emit('joinRoom', roomId);
newSocket.on('newMessage', (message: Message) => {
setMessages((prev) => [...prev, message]);
});
newSocket.on('userJoined', ({ username }) => {
setUsers((prev) => [...prev, username]);
});
newSocket.on('userLeft', ({ username }) => {
setUsers((prev) => prev.filter((u) => u !== username));
});
return () => {
newSocket.emit('leaveRoom', roomId);
newSocket.close();
};
}, [roomId]);
const sendMessage = useCallback(() => {
if (socket && input.trim()) {
socket.emit('sendMessage', { roomId, content: input });
setInput('');
}
}, [socket, input, roomId]);
return (
<div className="flex h-screen">
{/* 用户列表 */}
<aside className="w-48 bg-gray-100 p-4">
<h3 className="font-bold mb-2">在线用户</h3>
{users.map((user) => (
<div key={user}>{user}</div>
))}
</aside>
{/* 聊天区域 */}
<main className="flex-1 flex flex-col">
<div className="flex-1 overflow-auto p-4">
{messages.map((msg) => (
<div key={msg.id} className="mb-2">
<span className="font-bold">{msg.username}: </span>
<span>{msg.content}</span>
</div>
))}
</div>
<div className="p-4 border-t flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
className="flex-1 p-2 border rounded"
placeholder="输入消息..."
/>
<button onClick={sendMessage} className="px-4 py-2 bg-blue-500 text-white rounded">
发送
</button>
</div>
</main>
</div>
);
}AI 协作指南
- 核心意图:让 AI 帮你实现完整的聊天功能。
- 需求定义公式:
"请帮我实现一个支持多房间的聊天应用,包括用户加入/离开通知、消息历史和在线用户列表。" - 关键术语:
room、join、leave、broadcast、消息广播
避坑指南
- 消息顺序:网络延迟可能导致消息乱序,考虑使用时间戳排序。
- 消息去重:重连时可能收到重复消息,需要客户端去重。
- 用户状态同步:断线重连后需要重新同步房间状态。
