WebSocket多人在线聊天实战:HTML5实时通信技术从零搭建即时通讯应用完整教程
哈喽!今天咱们来玩点有意思的——从零开始搭建一个真正的多人在线聊天室。别被”实时通信”这几个字吓到,WebSocket其实比你想的要简单得多,我保证你看完这篇能动手写出一个能跑起来的聊天应用。
先说个场景:想象你在玩一款网络游戏,队友在聊天框里打字,你几乎瞬间就能看到他们发的消息。这种”秒到”的体验,靠的就是WebSocket技术。在WebSocket出现之前,我们只能用一种笨办法——不断刷新页面或者用轮询(每隔几秒问一次服务器”有新消息吗?”),既浪费资源又延迟严重。WebSocket的出现,让浏览器和服务器之间建立了一条”专用电话线”,消息可以双向实时传输。
好,理论说完了,咱们直接动手。我会用Node.js做后端,纯前端HTML+JavaScript做客户端,完全不依赖复杂框架,让你看清楚每一行代码在做什么。
第一章:先理解WebSocket是什么
你可以把WebSocket想象成一条持久连接的双向管道。传统的HTTP请求是”一问一答”模式——你发消息给服务器,服务器回应,然后连接就断了。下次要再发消息,得重新建立连接。而WebSocket一旦建立连接,双方就可以随时互相发消息,连接一直保持。
用生活化的例子来说:
| 传统HTTP轮询 | WebSocket |
|---|---|
| 你每隔30秒打电话问客服”有新消息吗?” | 你和客服开了一条专线,有消息直接推过来 |
| 浪费大量时间和资源 | 高效、实时 |
| 消息有延迟 | 几乎无延迟 |
在浏览器中,我们只需要用一行JavaScript就能建立连接:
const ws = new WebSocket('ws://localhost:8080');
注意协议头是ws://(非加密)或wss://(加密),就像HTTP和HTTPS的区别。
第二章:搭建Node.js服务器
咱们用Node.js来写服务器,因为它运行JavaScript,和我们前端代码风格一致,学起来没有障碍。先初始化项目:
mkdir websocket-chat
cd websocket-chat
npm init -y
npm install ws
ws是最流行的Node.js WebSocket库。现在创建server.js:
const WebSocket = require('ws');
const http = require('http');
// 创建HTTP服务器
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>服务器运行中</h1>');
});
// 创建WebSocket服务器,监听同一端口
const wss = new WebSocket.Server({ server });
// 存储所有在线用户:userId -> { ws, name }
const users = new Map();
let userIdCounter = 1;
console.log('聊天服务器已启动,端口 8080');
wss.on('connection', (ws, req) => {
console.log('有新用户连接');
// 为每个连接分配唯一ID
const userId = userIdCounter++;
let userName = `用户${userId}`;
// 注册到在线用户列表
users.set(userId, { ws, name: userName });
// 广播给所有人:有新用户加入了
broadcast({
type: 'system',
message: `${userName} 加入了聊天室`,
users: getUsersSnapshot()
});
// 监听客户端发来的消息
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
console.log(`收到来自 ${userName}(ID:${userId}) 的消息:`, msg);
switch (msg.type) {
case 'join':
// 用户设置了昵称
userName = msg.name || userName;
users.set(userId, { ws, name: userName });
broadcast({
type: 'system',
message: `${userName} 以新昵称加入了聊天室`,
users: getUsersSnapshot()
});
break;
case 'chat':
// 普通聊天消息,广播给所有人
broadcast({
type: 'chat',
userId,
userName,
message: msg.message,
timestamp: new Date().toLocaleTimeString()
});
break;
case 'private':
// 私聊消息
handlePrivateMessage(userId, userName, msg.targetUserId, msg.message);
break;
case 'typing':
// 正在输入状态(可选功能)
broadcast({
type: 'typing',
userId,
userName,
targetUserId: msg.targetUserId
});
break;
}
} catch (e) {
console.error('消息解析错误:', e);
}
});
// 用户断开连接
ws.on('close', () => {
console.log(`用户 ${userName}(ID:${userId}) 已断开连接`);
users.delete(userId);
broadcast({
type: 'system',
message: `${userName} 离开了聊天室`,
users: getUsersSnapshot()
});
});
// 连接出错
ws.on('error', (err) => {
console.error(`用户 ${userName} 连接出错:`, err.message);
users.delete(userId);
});
});
// 广播消息给所有连接的客户端
function broadcast(data) {
const message = JSON.stringify(data);
users.forEach((user) => {
if (user.ws.readyState === WebSocket.OPEN) {
user.ws.send(message);
}
});
}
// 发送私聊消息
function handlePrivateMessage(fromId, fromName, toId, message) {
const target = users.get(toId);
if (!target) {
// 目标不在线,发送系统提示给发送者
const sender = users.get(fromId);
if (sender && sender.ws.readyState === WebSocket.OPEN) {
sender.ws.send(JSON.stringify({
type: 'error',
message: `用户 ${toId} 不在线`
}));
}
return;
}
// 发送给目标用户
if (target.ws.readyState === WebSocket.OPEN) {
target.ws.send(JSON.stringify({
type: 'private',
fromId,
fromName,
message,
timestamp: new Date().toLocaleTimeString()
}));
}
// 发送者自己也知道发出去了(可选)
const sender = users.get(fromId);
if (sender && sender.ws.readyState === WebSocket.OPEN) {
sender.ws.send(JSON.stringify({
type: 'private_sent',
toId,
toName: target.name,
message,
timestamp: new Date().toLocaleTimeString()
}));
}
}
// 获取当前在线用户列表快照
function getUsersSnapshot() {
const result = [];
users.forEach((user) => {
result.push({ id: user.id || Array.from(users.keys()).find(k => users.get(k) === user), name: user.name });
});
// 重新获取正确的ID映射
const idMap = new Map();
let idx = 1;
users.forEach((user) => {
idMap.set(user, idx++);
});
return Array.from(users.values()).map((user) => ({
id: idMap.get(user),
name: user.name
}));
}
// 启动服务器
const PORT = 8080;
server.listen(PORT, () => {
console.log(`========================================`);
console.log(` WebSocket聊天服务器已启动`);
console.log(` 访问地址: http://localhost:${PORT}`);
console.log(` WebSocket: ws://localhost:${PORT}`);
console.log(`========================================`);
});
你看,服务器代码其实很直观。核心逻辑就是:
- 有用户连进来 → 给他分配ID,广播”有人加入了”
- 收到消息 → 根据消息类型处理(群聊/私聊/加入)
- 用户断开 → 从列表移除,广播”有人离开了”
第三章:前端页面实现
现在来写前端。创建一个index.html,把所有东西放在一个文件里方便你测试:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实时聊天室</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.chat-container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 900px;
height: 600px;
display: flex;
overflow: hidden;
}
/* 右侧用户列表 */
.sidebar {
width: 220px;
background: #f8f9fa;
border-right: 1px solid #e9ecef;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.sidebar-header h2 {
font-size: 18px;
margin-bottom: 5px;
}
.sidebar-header p {
font-size: 12px;
opacity: 0.8;
}
.online-users {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.user-item {
display: flex;
align-items: center;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
margin-bottom: 4px;
}
.user-item:hover {
background: #e9ecef;
}
.user-item.active {
background: #667eea;
color: white;
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 14px;
margin-right: 10px;
flex-shrink: 0;
}
.user-info {
flex: 1;
min-width: 0;
}
.user-name {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-status {
font-size: 11px;
opacity: 0.6;
}
/* 主聊天区域 */
.main-chat {
flex: 1;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 15px 20px;
background: white;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h3 {
font-size: 16px;
color: #333;
}
.connection-status {
display: flex;
align-items: center;
font-size: 12px;
color: #666;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-dot.connected { background: #28a745; }
.status-dot.disconnected { background: #dc3545; }
.status-dot.connecting { background: #ffc107; }
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #fafbfc;
}
.message {
margin-bottom: 16px;
display: flex;
flex-direction: column;
}
.message.system {
text-align: center;
padding: 8px 16px;
background: #e9ecef;
border-radius: 20px;
align-self: center;
font-size: 12px;
color: #666;
margin-bottom: 20px;
}
.message.chat {
align-items: flex-start;
}
.message.private {
align-items: flex-end;
}
.message-header {
display: flex;
align-items: center;
margin-bottom: 4px;
font-size: 12px;
color: #888;
}
.message.private .message-header {
flex-direction: row-reverse;
}
.message-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 11px;
font-weight: bold;
margin-right: 8px;
}
.message.private .message-avatar {
margin-right: 0;
margin-left: 8px;
}
.message-name {
font-weight: 500;
color: #555;
margin-right: 8px;
}
.message.private .message-name {
margin-right: 0;
margin-left: 8px;
}
.message-content {
max-width: 70%;
padding: 10px 16px;
border-radius: 18px;
font-size: 14px;
line-height: 1.5;
word-break: break-word;
}
.message.chat .message-content {
background: white;
border: 1px solid #e9ecef;
border-bottom-left-radius: 4px;
}
.message.private .message-content {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-bottom-right-radius: 4px;
}
.message-time {
font-size: 10px;
color: #aaa;
margin-top: 4px;
}
.message.private .message-time {
text-align: right;
}
/* 输入区域 */
.input-area {
padding: 15px 20px;
background: white;
border-top: 1px solid #e9ecef;
display: flex;
gap: 10px;
align-items: center;
}
.input-area input[type="text"] {
flex: 1;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 25px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.input-area input[type="text"]:focus {
border-color: #667eea;
}
.input-area button {
padding: 12px 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 25px;
font-size: 14px;
cursor: pointer;
transition: transform 0.1s, opacity 0.2s;
}
.input-area button:hover {
opacity: 0.9;
transform: scale(1.02);
}
.input-area button:active {
transform: scale(0.98);
}
/* 登录弹窗 */
.login-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.login-modal.hidden {
display: none;
}
.login-box {
background: white;
padding: 40px;
border-radius: 20px;
width: 360px;
text-align: center;
}
.login-box h2 {
margin-bottom: 8px;
color: #333;
}
.login-box p {
color: #888;
font-size: 14px;
margin-bottom: 24px;
}
.login-box input {
width: 100%;
padding: 14px 16px;
border: 2px solid #e9ecef;
border-radius: 12px;
font-size: 16px;
text-align: center;
outline: none;
margin-bottom: 16px;
transition: border-color 0.2s;
}
.login-box input:focus {
border-color: #667eea;
}
.login-box button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: opacity 0.2s;
}
.login-box button:hover {
opacity: 0.9;
}
/* 模式切换 */
.mode-switch {
display: flex;
gap: 8px;
margin-bottom: 15px;
}
.mode-btn {
flex: 1;
padding: 8px;
border: 2px solid #e9ecef;
background: white;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.mode-btn.active {
border-color: #667eea;
background: #667eea;
color: white;
}
/* 私聊面板 */
.private-panel {
padding: 10px 15px;
background: #f0f0ff;
border-bottom: 1px solid #e9ecef;
display: none;
align-items: center;
justify-content: space-between;
font-size: 13px;
color: #667eea;
}
.private-panel.active {
display: flex;
}
.private-panel .close-btn {
cursor: pointer;
font-size: 18px;
line-height: 1;
}
</style>
</head>
<body>
<!-- 登录弹窗 -->
<div class="login-modal" id="loginModal">
<div class="login-box">
<h2>欢迎来到聊天室</h2>
<p>请输入你的昵称开始聊天</p>
<input type="text" id="nicknameInput" placeholder="你的昵称..." maxlength="20" autocomplete="off">
<button id="joinBtn">加入聊天</button>
</div>
</div>
<!-- 主聊天界面 -->
<div class="chat-container">
<!-- 左侧用户列表 -->
<div class="sidebar">
<div class="sidebar-header">
<h2>💬 聊天室</h2>
<p id="userCount">在线: 0 人</p>
</div>
<div class="online-users" id="onlineUsers">
<!-- 用户列表动态生成 -->
</div>
</div>
<!-- 右侧聊天区域 -->
<div class="main-chat">
<div class="chat-header">
<h3 id="chatTitle">全体聊天</h3>
<div class="connection-status">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">连接中...</span>
</div>
</div>
<!-- 私聊面板 -->
<div class="private-panel" id="privatePanel">
<span>🔒 私聊中: <strong id="privateTargetName"></strong></span>
<span class="close-btn" id="closePrivate">✕</span>
</div>
<div class="messages" id="messages">
<!-- 消息动态生成 -->
</div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="输入消息..." maxlength="500" disabled>
<button id="sendBtn" disabled>发送</button>
</div>
</div>
</div>
<script>
// ========== 全局状态 ==========
let ws = null;
let myUserId = null;
let myName = '';
let currentMode = 'group'; // 'group' 或 'private'
let privateTargetId = null;
let privateTargetName = '';
let onlineUsers = [];
// ========== DOM元素 ==========
const loginModal = document.getElementById('loginModal');
const nicknameInput = document.getElementById('nicknameInput');
const joinBtn = document.getElementById('joinBtn');
const messages = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const onlineUsersEl = document.getElementById('onlineUsers');
const userCountEl = document.getElementById('userCount');
const chatTitle = document.getElementById('chatTitle');
const privatePanel = document.getElementById('privatePanel');
const privateTargetNameEl = document.getElementById('privateTargetName');
const closePrivateBtn = document.getElementById('closePrivate');
// ========== 初始化 ==========
function init() {
// 回车键加入
nicknameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') joinChat();
});
joinBtn.addEventListener('click', joinChat);
// 发送消息
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// 关闭私聊
closePrivateBtn.addEventListener('click', leavePrivate);
// 尝试自动连接
connect();
}
// ========== 连接WebSocket ==========
function connect() {
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${location.host}`);
ws.onopen = () => {
console.log('WebSocket连接成功');
setStatus('connected', '已连接');
messageInput.disabled = false;
sendBtn.disabled = false;
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleServerMessage(data);
};
ws.onclose = () => {
console.log('WebSocket连接关闭');
setStatus('disconnected', '已断开');
messageInput.disabled = true;
sendBtn.disabled = true;
// 5秒后自动重连
setTimeout(connect, 5000);
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
setStatus('disconnected', '连接错误');
};
}
// ========== 加入聊天室 ==========
function joinChat() {
const name = nicknameInput.value.trim();
if (!name) {
nicknameInput.style.borderColor = '#dc3545';
nicknameInput.placeholder = '请输入昵称!';
return;
}
myName = name;
loginModal.classList.add('hidden');
// 发送加入消息
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'join',
name: myName
}));
}
}
// ========== 处理服务端消息 ==========
function handleServerMessage(data) {
console.log('收到消息:', data);
switch (data.type) {
case 'system':
// 系统消息(加入/离开)
addSystemMessage(data.message);
if (data.users) {
onlineUsers = data.users;
renderOnlineUsers();
}
break;
case 'chat':
// 群聊消息
if (currentMode === 'group' || data.userId === myUserId) {
addChatMessage(data);
}
break;
case 'private':
// 收到私聊
addChatMessage(data, true);
break;
case 'private_sent':
// 私聊发送确认
addChatMessage(data, true);
break;
case 'typing':
// 对方正在输入(可扩展功能)
break;
case 'error':
alert(data.message);
break;
}
}
// ========== 发送消息 ==========
function sendMessage() {
const text = messageInput.value.trim();
if (!text) return;
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('尚未连接到服务器');
return;
}
const msg = {
type: currentMode === 'private' ? 'private' : 'chat',
message: text,
timestamp: new Date().toLocaleTimeString()
};
if (currentMode === 'private') {
msg.targetUserId = privateTargetId;
}
ws.send(JSON.stringify(msg));
messageInput.value = '';
// 本地显示(乐观更新)
const localData = {
userId: myUserId,
userName: myName,
message: text,
timestamp: new Date().toLocaleTimeString(),
isPrivate: currentMode === 'private'
};
addChatMessage(localData, currentMode === 'private');
}
// ========== 渲染消息 ==========
function addChatMessage(data, isPrivate = false) {
const msgEl = document.createElement('div');
msgEl.className = `message ${isPrivate ? 'private' : 'chat'}`;
const initial = data.userName ? data.userName.charAt(0).toUpperCase() : '?';
let headerHTML = '';
if (!isPrivate || currentMode === 'group') {
headerHTML = `
<div class="message-header">
<div class="message-avatar">${initial}</div>
<span class="message-name">${escapeHtml(data.userName || '未知')}</span>
<span class="message-time">${data.timestamp || ''}</span>
</div>
`;
}
msgEl.innerHTML = `
${headerHTML}
<div class="message-content">${escapeHtml(data.message)}</div>
`;
messages.appendChild(msgEl);
scrollToBottom();
}
function addSystemMessage(text) {
const msgEl = document.createElement('div');
msgEl.className = 'message system';
msgEl.textContent = text;
messages.appendChild(msgEl);
scrollToBottom();
}
function scrollToBottom() {
messages.scrollTop = messages.scrollHeight;
}
// ========== 渲染在线用户列表 ==========
function renderOnlineUsers() {
onlineUsersEl.innerHTML = '';
userCountEl.textContent = `在线: ${onlineUsers.length} 人`;
onlineUsers.forEach(user => {
const item = document.createElement('div');
item.className = 'user-item';
if (user.id === myUserId) {
item.classList.add('active');
}
// 双击进入私聊
item.addEventListener('dblclick', () => enterPrivate(user.id, user.name));
item.innerHTML = `
<div class="user-avatar">${user.name.charAt(0).toUpperCase()}</div>
<div class="user-info">
<div class="user-name">${escapeHtml(user.name)} ${user.id === myUserId ? '(我)' : ''}</div>
<div class="user-status">在线</div>
</div>
`;
onlineUsersEl.appendChild(item);
});
}
// ========== 私聊功能 ==========
function enterPrivate(userId, userName) {
if (userId === myUserId) return; // 不能私聊自己
currentMode = 'private';
privateTargetId = userId;
privateTargetName = userName;
chatTitle.textContent = `私聊: ${userName}`;
privatePanel.classList.add('active');
privateTargetNameEl.textContent = userName;
messageInput.placeholder = `给 ${userName} 发送私聊...`;
// 高亮当前私聊用户
document.querySelectorAll('.user-item').forEach(el => el.classList.remove('active'));
const targetEl = Array.from(document.querySelectorAll('.user-item'))
.find(el => el.textContent.includes(userName));
if (targetEl) targetEl.classList.add('active');
}
function leavePrivate() {
currentMode = 'group';
privateTargetId = null;
privateTargetName = '';
chatTitle.textContent = '全体聊天';
privatePanel.classList.remove('active');
messageInput.placeholder = '输入消息...';
document.querySelectorAll('.user-item').forEach(el => el.classList.remove('active'));
const myEl = Array.from(document.querySelectorAll('.user-item'))
.find(el => el.textContent.includes('(我)'));
if (myEl) myEl.classList.add('active');
}
// ========== 工具函数 ==========
function setStatus(type, text) {
statusDot.className = `status-dot ${type}`;
statusText.textContent = text;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 启动
init();
</script>
</body>
</html>
第四章:运行项目
把上面两个文件放在同一个文件夹里,然后:
# 启动服务器
node server.js
然后在浏览器打开 http://localhost:8080,开多个标签页(或让朋友也在同一网络下打开),就能看到实时聊天的效果了。
测试步骤:
- 第一个标签输入昵称”小明”加入
- 第二个标签输入昵称”小红”加入
- 第三个标签输入昵称”小刚”加入
- 你会发现所有人的消息实时同步,左侧用户列表也跟着变化
- 双击左侧任意用户,可以切换私聊模式
第五章:核心技术点详解
1. 心跳检测(防止连接僵死)
WebSocket连接可能因为网络波动看起来还连着,但实际上已经断了。加上心跳检测能解决这个问题:
// 在server.js中添加心跳
const HEARTBEAT_INTERVAL = 30000; // 30秒
wss.on('connection', (ws) => {
// 发送ping
const heartbeat = () => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
};
const timer = setInterval(heartbeat, HEARTBEAT_INTERVAL);
ws.on('pong', () => {
console.log('收到pong响应');
});
ws.on('close', () => {
clearInterval(timer);
});
});
客户端也可以加类似的逻辑,检测连接状态。
2. 断线重连
前面代码里已经加了简单的自动重连,但更健壮的做法是指数退避:
let reconnectDelay = 1000; // 初始重连延迟1秒
const MAX_DELAY = 30000; // 最大延迟30秒
function connect() {
ws = new WebSocket(`${protocol}//${location.host}`);
ws.onopen = () => {
reconnectDelay = 1000; // 重置延迟
setStatus('connected', '已连接');
};
ws.onclose = () => {
setStatus('disconnected', '已断开');
// 指数退避重连
setTimeout(() => {
reconnectDelay = Math.min(reconnectDelay * 2, MAX_DELAY);
connect();
}, reconnectDelay);
};
}
3. 消息持久化(可选进阶)
如果想让聊天记录不丢失,可以加个简单的存储。用SQLite或者甚至JSON文件都可以:
const fs = require('fs');
const path = require('path');
const MESSAGE_FILE = path.join(__dirname, 'messages.json');
// 加载历史消息
function loadMessages() {
if (fs.existsSync(MESSAGE_FILE)) {
return JSON.parse(fs.readFileSync(MESSAGE_FILE, 'utf8'));
}
return [];
}
// 保存消息
function saveMessage(msg) {
const messages = loadMessages();
messages.push(msg);
// 只保留最近1000条
if (messages.length > 1000) {
messages.splice(0, messages.length - 1000);
}
fs.writeFileSync(MESSAGE_FILE, JSON.stringify(messages, null, 2));
}
第六章:常见问题与调试技巧
Q: 连接失败怎么办?
检查:
- Node.js服务器是否在运行(看终端有没有打印启动日志)
- 端口是否被占用(
netstat -ano | findstr :8080) - 浏览器控制台(F12)有没有报错
Q: 消息收不到?
在浏览器控制台输入 console.log(ws.readyState) 查看连接状态:
0= CONNECTING1= OPEN(正常)2= CLOSING3= CLOSED
Q: 跨域问题?
如果前端和后端不在同一个域名/端口,需要在WebSocket握手时处理Origin。ws库默认允许所有Origin,生产环境建议限制:
const wss = new WebSocket.Server({
server,
verifyClient: (info, callback) => {
const allowedOrigins = ['http://localhost:3000', 'https://yourdomain.com'];
const originValid = allowedOrigins.includes(info.origin);
callback(originValid);
}
});
第七章:进阶方向
当你把这个聊天室跑起来之后,可以考虑以下扩展:
- 表情包支持:解析
:):(这样的文本,替换成图片 - 文件传输:WebSocket支持二进制消息,可以传图片文件
- 在线状态:用Redis做分布式存储,支持多服务器扩展
- 消息已读回执:客户端收到消息后回调服务端
- 加密通信:用Web Crypto API做端到端加密
- 移动端适配:用PWA技术让手机也能用
这篇教程从原理到实战都覆盖了,代码都是可以直接运行的。你不妨现在就动手试试,开两个浏览器标签页,感受一下”实时”的魅力。如果有哪里不清楚,随时问我!
