AI实现在线聊天室--03

133 阅读6分钟

上篇实现了基本的聊天室:AI实现在线聊天室--02 这篇在上次基础上做个完善(代码逻辑优化,优化发送消息显示多次和用户显示不准确的问题)

客户端

问题分析

  1. 重复连接:WebSocket连接可能被初始化了两次
  2. 重复消息:每个消息被发送/接收两次
  3. 根本原因
    • 可能是由于页面刷新或重新连接逻辑不当
    • 事件监听器被重复绑定

修复方案

以下是修改后的关键代码部分,主要改动在JavaScript部分:

javascript

// 在全局范围添加这些变量
let socket = null;
let isConnecting = false;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;

// 修改后的initWebSocket函数
function initWebSocket() {
    if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
        return; // 如果已有活跃连接或正在连接,则不再创建新连接
    }

    isConnecting = true;
    socket = new WebSocket('ws://localhost:8080');
    
    // 清除旧的事件监听器(如果存在)
    socket.onopen = null;
    socket.onmessage = null;
    socket.onclose = null;
    socket.onerror = null;
    
    socket.onopen = (event) => {
        console.log('Connected to WebSocket server');
        isConnected = true;
        isConnecting = false;
        reconnectAttempts = 0;
        connectionStatus.textContent = '在线';
        
        // 如果已有用户名,重新发送用户信息
        if (username) {
            socket.send(JSON.stringify({
                type: 'setUsername',
                username: username,
                color: userColor
            }));
        }
    };
    
    socket.onmessage = (event) => {
        const data = JSON.parse(event.data);
        
        if (data.type === 'message') {
            addMessage(data);
        } else if (data.type === 'userList') {
            updateUserList(data.users);
            onlineCount.textContent = data.users.length;
        }
    };
    
    socket.onclose = (event) => {
        console.log('Disconnected from WebSocket server');
        isConnected = false;
        isConnecting = false;
        connectionStatus.textContent = '离线';
        
        addSystemMessage('与服务器的连接已断开');
        
        // 禁用输入
        messageInput.disabled = true;
        sendButton.disabled = true;
        
        // 尝试重新连接(带指数退避)
        if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
            const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); // 最大30秒
            reconnectAttempts++;
            console.log(`尝试重新连接 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);
            setTimeout(initWebSocket, delay);
        } else {
            addSystemMessage('无法连接到服务器,请刷新页面重试');
        }
    };
    
    socket.onerror = (error) => {
        console.error('WebSocket error:', error);
        isConnecting = false;
    };
}

// 修改sendMessage函数
function sendMessage() {
    const message = messageInput.value.trim();
    if (message && socket && socket.readyState === WebSocket.OPEN) {
        // 先显示自己的消息(乐观更新)
        const tempMessage = {
            type: 'message',
            username: username,
            content: message,
            timestamp: new Date().toISOString(),
            color: userColor
        };
        addMessage(tempMessage);
        
        try {
            // 发送到服务器
            socket.send(JSON.stringify({
                type: 'message',
                content: message
            }));
            
            // 清空输入框
            messageInput.value = '';
        } catch (error) {
            console.error('发送消息失败:', error);
            addSystemMessage('消息发送失败,请重试');
        }
    }
}

// 在页面加载时初始化
document.addEventListener('DOMContentLoaded', () => {
    initWebSocket();
    
    // 确保事件监听器只绑定一次
    setUsernameButton.onclick = setUsername;
    usernameInput.onkeypress = (e) => {
        if (e.key === 'Enter') setUsername();
    };
    
    sendButton.onclick = sendMessage;
    messageInput.onkeypress = (e) => {
        if (e.key === 'Enter') sendMessage();
    };
});

主要修改点

  1. 单例WebSocket连接

    • 使用全局socket变量确保只有一个连接
    • 在创建新连接前检查现有连接状态
  2. 改进的重连逻辑

    • 添加最大重试次数限制
    • 使用指数退避算法避免频繁重连
    • 添加连接状态标志(isConnecting)
  3. 事件监听器管理

    • 使用on而不是addEventListener避免重复绑定
    • 在DOMContentLoaded中一次性绑定事件
  4. 错误处理增强

    • 添加更多错误处理和状态反馈
    • 发送消息时添加try-catch
  5. 用户名同步

    • 在重新连接时自动重新发送用户名信息

完整修复后的HTML文件

将之前的<script>部分替换为以下代码:

html

<script>
    // 连接状态管理
    let socket = null;
    let isConnected = false;
    let isConnecting = false;
    let reconnectAttempts = 0;
    const MAX_RECONNECT_ATTEMPTS = 5;
    
    // 用户信息
    let username = '';
    let userColor = '';
    
    // DOM元素
    const messagesContainer = document.getElementById('messages-container');
    const messageInput = document.getElementById('message-input');
    const sendButton = document.getElementById('send-button');
    const usernameInput = document.getElementById('username-input');
    const setUsernameButton = document.getElementById('set-username');
    const usernameModal = document.getElementById('username-modal');
    const userListContainer = document.getElementById('user-list-container');
    const sidebarUsername = document.getElementById('sidebar-username');
    const userAvatar = document.getElementById('user-avatar');
    const connectionStatus = document.getElementById('connection-status');
    const onlineCount = document.getElementById('online-count');

    // 初始化WebSocket连接
    function initWebSocket() {
        if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
            return; // 如果已有活跃连接或正在连接,则不再创建新连接
        }

        isConnecting = true;
        socket = new WebSocket('ws://localhost:8080');
        
        // 清除旧的事件监听器(如果存在)
        socket.onopen = null;
        socket.onmessage = null;
        socket.onclose = null;
        socket.onerror = null;
        
        socket.onopen = (event) => {
            console.log('Connected to WebSocket server');
            isConnected = true;
            isConnecting = false;
            reconnectAttempts = 0;
            connectionStatus.textContent = '在线';
            
            // 如果已有用户名,重新发送用户信息
            if (username) {
                socket.send(JSON.stringify({
                    type: 'setUsername',
                    username: username,
                    color: userColor
                }));
            }
        };
        
        socket.onmessage = (event) => {
            const data = JSON.parse(event.data);
            
            if (data.type === 'message') {
                addMessage(data);
            } else if (data.type === 'userList') {
                updateUserList(data.users);
                onlineCount.textContent = data.users.length;
            }
        };
        
        socket.onclose = (event) => {
            console.log('Disconnected from WebSocket server');
            isConnected = false;
            isConnecting = false;
            connectionStatus.textContent = '离线';
            
            addSystemMessage('与服务器的连接已断开');
            
            // 禁用输入
            messageInput.disabled = true;
            sendButton.disabled = true;
            
            // 尝试重新连接(带指数退避)
            if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
                const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); // 最大30秒
                reconnectAttempts++;
                console.log(`尝试重新连接 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);
                setTimeout(initWebSocket, delay);
            } else {
                addSystemMessage('无法连接到服务器,请刷新页面重试');
            }
        };
        
        socket.onerror = (error) => {
            console.error('WebSocket error:', error);
            isConnecting = false;
        };
    }

    // 添加消息到聊天界面
    function addMessage(data) {
        const messageEl = document.createElement('div');
        
        if (data.username === '系统') {
            messageEl.className = 'message system-message';
            messageEl.innerHTML = `
                <div class="message-text">${data.content}</div>
            `;
        } else {
            const isOwnMessage = data.username === username;
            messageEl.className = `message ${isOwnMessage ? 'own-message' : ''}`;
            
            const avatarText = data.username.charAt(0).toUpperCase();
            const time = formatTime(data.timestamp);
            
            messageEl.innerHTML = `
                <div class="message-avatar" style="background-color: ${data.color}">${avatarText}</div>
                <div class="message-content">
                    <div class="message-info">
                        <div class="message-username" style="color: ${data.color}">${data.username}</div>
                        <div class="message-time">${time}</div>
                    </div>
                    <div class="message-text">${data.content}</div>
                </div>
            `;
        }
        
        messagesContainer.appendChild(messageEl);
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }

    // 添加系统消息
    function addSystemMessage(text) {
        const messageEl = document.createElement('div');
        messageEl.className = 'message system-message';
        messageEl.innerHTML = `
            <div class="message-text">${text}</div>
        `;
        messagesContainer.appendChild(messageEl);
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }

    // 更新用户列表
    function updateUserList(users) {
        userListContainer.innerHTML = '';
        
        users.forEach(user => {
            if (user.username === username) return; // 不显示自己
            
            const userEl = document.createElement('div');
            userEl.className = 'user-item';
            userEl.innerHTML = `
                <div class="avatar" style="background-color: ${user.color}">${user.username.charAt(0).toUpperCase()}</div>
                <div class="user-info">
                    <div class="username">${user.username}</div>
                </div>
            `;
            userListContainer.appendChild(userEl);
        });
    }

    // 格式化时间
    function formatTime(timestamp) {
        const date = new Date(timestamp);
        return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    }

    // 生成随机颜色
    function getRandomColor() {
        const colors = [
            '#4361ee', '#3f37c9', '#4895ef', '#4cc9f0', 
            '#7209b7', '#b5179e', '#f72585', '#560bad',
            '#3a0ca3', '#480ca8', '#3a5a40', '#588157'
        ];
        return colors[Math.floor(Math.random() * colors.length)];
    }

    // 设置用户名
    function setUsername() {
        const newUsername = usernameInput.value.trim();
        if (newUsername) {
            username = newUsername;
            userColor = getRandomColor();
            
            // 更新UI
            sidebarUsername.textContent = username;
            userAvatar.textContent = username.charAt(0).toUpperCase();
            userAvatar.style.backgroundColor = userColor;
            
            // 发送用户名到服务器
            if (socket && socket.readyState === WebSocket.OPEN) {
                socket.send(JSON.stringify({
                    type: 'setUsername',
                    username: newUsername,
                    color: userColor
                }));
            }
            
            // 关闭弹窗
            usernameModal.style.display = 'none';
            
            // 启用消息输入
            messageInput.disabled = false;
            sendButton.disabled = false;
            messageInput.focus();
            
            addSystemMessage(`您已设置用户名为: ${username}`);
        }
    }

    // 发送消息
    function sendMessage() {
        const message = messageInput.value.trim();
        if (message && socket && socket.readyState === WebSocket.OPEN) {
            // 先显示自己的消息(乐观更新)
            const tempMessage = {
                type: 'message',
                username: username,
                content: message,
                timestamp: new Date().toISOString(),
                color: userColor
            };
            addMessage(tempMessage);
            
            try {
                // 发送到服务器
                socket.send(JSON.stringify({
                    type: 'message',
                    content: message
                }));
                
                // 清空输入框
                messageInput.value = '';
            } catch (error) {
                console.error('发送消息失败:', error);
                addSystemMessage('消息发送失败,请重试');
            }
        }
    }

    // 初始化
    document.addEventListener('DOMContentLoaded', () => {
        initWebSocket();
        
        // 确保事件监听器只绑定一次
        setUsernameButton.onclick = setUsername;
        usernameInput.onkeypress = (e) => {
            if (e.key === 'Enter') setUsername();
        };
        
        sendButton.onclick = sendMessage;
        messageInput.onkeypress = (e) => {
            if (e.key === 'Enter') sendMessage();
        };
    });
</script>

修改总结

  1. WebSocket连接管理
    • 确保只有一个活跃连接
    • 添加连接状态检查
    • 改进重连逻辑
  2. 事件监听器
    • 使用on替代addEventListener防止重复绑定
    • 在DOMContentLoaded中一次性绑定
  3. 错误处理
    • 添加更多错误处理和用户反馈
    • 发送消息时添加try-catch
  4. 状态同步
    • 重新连接时自动同步用户名
    • 更好的连接状态管理

这些修改应该能解决重复用户和重复消息的问题,同时提高了应用的稳定性和用户体验。

服务端

问题分析

  1. 现象:消息被发送两次
  2. 可能原因
    • 服务端广播消息时没有检查消息来源
    • 客户端发送消息后被服务端再次广播回来
    • 服务端和客户端都进行了消息显示

服务端修复方案

修改 server.js 文件,主要调整消息处理逻辑:

javascript

// 修改后的消息处理逻辑
ws.on('message', (message) => {
    try {
        const data = JSON.parse(message);
        
        if (data.type === 'setUsername') {
            // 更新用户名逻辑保持不变
            const userData = clients.get(ws);
            userData.username = data.username;
            clients.set(ws, userData);
            broadcastUserList();
            
            const welcomeMsg = {
                type: 'message',
                username: '系统',
                content: `${data.username} 加入了聊天室`,
                timestamp: new Date().toISOString(),
                color: '#888'
            };
            broadcast(JSON.stringify(welcomeMsg), ws); // 不发送给消息来源的客户端
        } else if (data.type === 'message') {
            // 修改后的消息处理逻辑
            const userData = clients.get(ws);
            if (!userData) return;
            
            // 直接广播消息,不需要回发给发送者
            const msg = {
                type: 'message',
                username: userData.username,
                content: data.content,
                timestamp: new Date().toISOString(),
                color: userData.color
            };
            broadcast(JSON.stringify(msg), ws); // 不发送给消息来源的客户端
        }
    } catch (e) {
        console.error('Error parsing message:', e);
    }
});

// 修改后的广播函数
function broadcast(message, excludeSocket = null) {
    wss.clients.forEach(client => {
        if (client !== excludeSocket && client.readyState === WebSocket.OPEN) {
            client.send(message);
        }
    });
}

关键修改点

  1. 广播函数改进
    • 添加 excludeSocket 参数,可以排除特定客户端不接收广播
    • 当消息来自某个客户端时,不将消息回发给该客户端
  2. 消息处理逻辑
    • 服务端不再回发消息给发送者
    • 客户端自行显示自己发送的消息(乐观更新)
  3. 用户列表更新
    • 保持原有逻辑不变

为什么这样修改

  1. 避免重复显示
    • 客户端已经通过"乐观更新"立即显示了自己发送的消息
    • 服务端不需要再将消息回发给发送者
  2. 更高效的消息分发
    • 减少不必要的网络传输
    • 减轻服务器负担
  3. 更符合聊天应用逻辑
    • 其他客户端正常接收消息
    • 发送者客户端立即看到自己的消息

完整修复后的服务端代码

javascript

const WebSocket = require('ws');
const http = require('http');

// 创建HTTP服务器
const server = http.createServer();
const wss = new WebSocket.Server({ server });

// 存储所有连接的客户端和用户名
const clients = new Map();

wss.on('connection', (ws) => {
  console.log('New client connected');
  
  // 为新连接分配一个默认用户名
  const defaultUsername = `User${Math.floor(Math.random() * 1000)}`;
  const userColor = getRandomColor();
  clients.set(ws, { username: defaultUsername, color: userColor });
  
  // 通知所有客户端有新用户加入
  broadcastUserList();
  
  ws.on('message', (message) => {
    try {
      const data = JSON.parse(message);
      
      if (data.type === 'setUsername') {
        // 更新用户名
        const userData = clients.get(ws);
        userData.username = data.username;
        userData.color = data.color || userColor;
        clients.set(ws, userData);
        broadcastUserList();
        
        // 发送欢迎消息(不发送给新加入的用户自己)
        const welcomeMsg = {
          type: 'message',
          username: '系统',
          content: `${data.username} 加入了聊天室`,
          timestamp: new Date().toISOString(),
          color: '#888'
        };
        broadcast(JSON.stringify(welcomeMsg), ws);
      } else if (data.type === 'message') {
        // 广播普通消息(不发送给消息来源的客户端)
        const userData = clients.get(ws);
        if (!userData) return;
        
        const msg = {
          type: 'message',
          username: userData.username,
          content: data.content,
          timestamp: new Date().toISOString(),
          color: userData.color
        };
        broadcast(JSON.stringify(msg), ws);
      }
    } catch (e) {
      console.error('Error parsing message:', e);
    }
  });
  
  ws.on('close', () => {
    console.log('Client disconnected');
    const userData = clients.get(ws);
    if (userData) {
      // 通知所有客户端有用户离开(不发送给已断开的客户端)
      const leaveMsg = {
        type: 'message',
        username: '系统',
        content: `${userData.username} 离开了聊天室`,
        timestamp: new Date().toISOString(),
        color: '#888'
      };
      broadcast(JSON.stringify(leaveMsg), ws);
      
      clients.delete(ws);
      broadcastUserList();
    }
  });
});

// 广播消息给所有客户端(可排除特定客户端)
function broadcast(message, excludeSocket = null) {
  wss.clients.forEach(client => {
    if (client !== excludeSocket && client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

// 广播用户列表给所有客户端
function broadcastUserList() {
  const userList = Array.from(clients.values()).map(user => ({
    username: user.username,
    color: user.color
  }));
  
  const message = {
    type: 'userList',
    users: userList
  };
  
  broadcast(JSON.stringify(message));
}

// 生成随机颜色
function getRandomColor() {
  const letters = '0123456789ABCDEF';
  let color = '#';
  for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)];
  }
  return color;
}

// 启动服务器
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

测试验证

  1. 启动修改后的服务端
  2. 打开两个浏览器窗口模拟不同用户
  3. 发送消息验证:
    • 发送者窗口应该只看到一次自己发送的消息
    • 接收者窗口应该看到一次消息
    • 没有重复消息出现

这样修改后,您的聊天室应该不会再出现重复消息的问题了。服务端和客户端的协作更加合理,消息显示也更加符合用户预期。