⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

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 帮你实现完整的聊天功能。
  • 需求定义公式"请帮我实现一个支持多房间的聊天应用,包括用户加入/离开通知、消息历史和在线用户列表。"
  • 关键术语roomjoinleavebroadcast消息广播

避坑指南

  • 消息顺序:网络延迟可能导致消息乱序,考虑使用时间戳排序。
  • 消息去重:重连时可能收到重复消息,需要客户端去重。
  • 用户状态同步:断线重连后需要重新同步房间状态。