上篇实现了基本的聊天室:AI实现在线聊天室--02 这篇在上次基础上做个完善(代码逻辑优化,优化发送消息显示多次和用户显示不准确的问题)
客户端
问题分析
- 重复连接:WebSocket连接可能被初始化了两次
- 重复消息:每个消息被发送/接收两次
- 根本原因:
- 可能是由于页面刷新或重新连接逻辑不当
- 事件监听器被重复绑定
修复方案
以下是修改后的关键代码部分,主要改动在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();
};
});
主要修改点
-
单例WebSocket连接:
- 使用全局
socket变量确保只有一个连接 - 在创建新连接前检查现有连接状态
- 使用全局
-
改进的重连逻辑:
- 添加最大重试次数限制
- 使用指数退避算法避免频繁重连
- 添加连接状态标志(
isConnecting)
-
事件监听器管理:
- 使用
on而不是addEventListener避免重复绑定 - 在DOMContentLoaded中一次性绑定事件
- 使用
-
错误处理增强:
- 添加更多错误处理和状态反馈
- 发送消息时添加try-catch
-
用户名同步:
- 在重新连接时自动重新发送用户名信息
完整修复后的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>
修改总结
- WebSocket连接管理:
- 确保只有一个活跃连接
- 添加连接状态检查
- 改进重连逻辑
- 事件监听器:
- 使用
on替代addEventListener防止重复绑定 - 在DOMContentLoaded中一次性绑定
- 使用
- 错误处理:
- 添加更多错误处理和用户反馈
- 发送消息时添加try-catch
- 状态同步:
- 重新连接时自动同步用户名
- 更好的连接状态管理
这些修改应该能解决重复用户和重复消息的问题,同时提高了应用的稳定性和用户体验。
服务端
问题分析
- 现象:消息被发送两次
- 可能原因:
- 服务端广播消息时没有检查消息来源
- 客户端发送消息后被服务端再次广播回来
- 服务端和客户端都进行了消息显示
服务端修复方案
修改 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);
}
});
}
关键修改点
- 广播函数改进:
- 添加
excludeSocket参数,可以排除特定客户端不接收广播 - 当消息来自某个客户端时,不将消息回发给该客户端
- 添加
- 消息处理逻辑:
- 服务端不再回发消息给发送者
- 客户端自行显示自己发送的消息(乐观更新)
- 用户列表更新:
- 保持原有逻辑不变
为什么这样修改
- 避免重复显示:
- 客户端已经通过"乐观更新"立即显示了自己发送的消息
- 服务端不需要再将消息回发给发送者
- 更高效的消息分发:
- 减少不必要的网络传输
- 减轻服务器负担
- 更符合聊天应用逻辑:
- 其他客户端正常接收消息
- 发送者客户端立即看到自己的消息
完整修复后的服务端代码
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}`);
});
测试验证
- 启动修改后的服务端
- 打开两个浏览器窗口模拟不同用户
- 发送消息验证:
- 发送者窗口应该只看到一次自己发送的消息
- 接收者窗口应该看到一次消息
- 没有重复消息出现
这样修改后,您的聊天室应该不会再出现重复消息的问题了。服务端和客户端的协作更加合理,消息显示也更加符合用户预期。