WebSocket 聊天室:多房间与消息广播
FreeGuideOnline
最新
2026-07-03
什么是 WebSocket 聊天室
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。与传统 HTTP 请求-响应模式不同,WebSocket 允许服务器主动向客户端推送数据,特别适合需要实时交互的应用场景,比如在线聊天室、协同编辑和游戏等。
本教程将带你从零构建一个支持多房间和消息广播的 WebSocket 聊天室。你将学会:
- 使用 Node.js 搭建 WebSocket 服务端
- 创建房间机制,让用户加入不同聊天频道
- 实现消息广播,将某房间的消息推送给该房间所有人
- 编写前端页面完成实时聊天交互
工具准备
你需要安装以下环境:
- Node.js (v14 或更高版本)
- npm (通常随 Node.js 一起安装)
- 现代浏览器 (Chrome / Firefox / Edge)
我们将使用原生 WebSocket API,无需额外框架。但服务端会用到 ws 这个轻量级 WebSocket 库。
搭建服务端
初始化项目并安装依赖
mkdir websocket-chat
cd websocket-chat
npm init -y
npm install ws
编写服务端代码 (server.js)
创建一个文件 server.js,以下代码实现了:
- 管理多个房间(每个房间是一个客户端集合)
- 处理客户端的加入房间、离开房间和发送消息请求
- 将消息广播给同一房间内的其他人
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 存储房间信息: Map<房间名, Set<客户端>>
const rooms = new Map();
wss.on('connection', (ws) => {
let currentRoom = null;
ws.on('message', (rawData) => {
let data;
try {
data = JSON.parse(rawData.toString());
} catch (e) {
return;
}
const { type, payload } = data;
switch (type) {
case 'join': {
const roomName = payload.room;
if (!roomName) return;
// 如果之前在其他房间,先离开
if (currentRoom && rooms.has(currentRoom)) {
rooms.get(currentRoom).delete(ws);
if (rooms.get(currentRoom).size === 0) {
rooms.delete(currentRoom);
}
broadcast(currentRoom, {
type: 'system',
payload: { message: `${payload.username} 离开了房间` }
});
}
// 加入新房间
currentRoom = roomName;
if (!rooms.has(roomName)) {
rooms.set(roomName, new Set());
}
rooms.get(roomName).add(ws);
// 将用户信息附加到 ws 对象上,方便后续使用
ws.username = payload.username;
// 向房间内的其他人广播加入消息(不包括自己)
broadcast(roomName, {
type: 'system',
payload: { message: `${payload.username} 加入了房间` }
}, ws);
// 发送加入成功的确认消息给自己
ws.send(JSON.stringify({
type: 'join_success',
payload: { room: roomName }
}));
break;
}
case 'message': {
if (!currentRoom) return;
// 向房间内所有人广播聊天消息(包括自己)
broadcast(currentRoom, {
type: 'chat',
payload: {
username: ws.username,
text: payload.text,
timestamp: new Date().toISOString()
}
});
break;
}
case 'leave': {
if (currentRoom && rooms.has(currentRoom)) {
rooms.get(currentRoom).delete(ws);
if (rooms.get(currentRoom).size === 0) {
rooms.delete(currentRoom);
}
broadcast(currentRoom, {
type: 'system',
payload: { message: `${ws.username} 离开了房间` }
});
currentRoom = null;
}
break;
}
}
});
ws.on('close', () => {
// 处理客户端意外关闭连接时的离开逻辑
if (currentRoom && rooms.has(currentRoom)) {
rooms.get(currentRoom).delete(ws);
if (rooms.get(currentRoom).size === 0) {
rooms.delete(currentRoom);
}
broadcast(currentRoom, {
type: 'system',
payload: { message: `${ws.username} 断开了连接` }
});
}
});
});
/**
* 向指定房间的所有客户端广播消息
* @param {string} roomName 房间名
* @param {object} message 要发送的消息对象
* @param {WebSocket} exclude 要排除的客户端(可选)
*/
function broadcast(roomName, message, exclude = null) {
if (!rooms.has(roomName)) return;
const clients = rooms.get(roomName);
const data = JSON.stringify(message);
clients.forEach((client) => {
if (client !== exclude && client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
console.log('WebSocket 服务器运行在 ws://localhost:8080');
关键设计说明
- 房间数据结构:使用
Map存储房间名到客户端Set的映射,方便快速增删查找。 - 消息协议:客户端与服务端通信采用 JSON 格式,
type字段标识动作,payload携带数据。 - 广播函数:
broadcast负责将消息发送给房间内所有有效连接,支持排除某个客户端(例如加入通知不应该发给加入者自己)。 - 断线处理:
ws.on('close')确保客户端异常断开时也能正确清理资源并通知其他用户。
编写前端页面
创建一个 index.html 文件,包含:
- 房间选择与加入界面
- 聊天消息显示区域
- 消息输入框
- 基于 WebSocket 的实时通信脚本
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket 多房间聊天室</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 2rem auto; }
.room-controls { margin-bottom: 1rem; }
#messages { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 0.5rem; margin-bottom: 1rem; }
.system-msg { color: #888; font-style: italic; }
.chat-msg { margin: 0.3rem 0; }
.chat-msg .user { font-weight: bold; }
.input-area { display: flex; gap: 0.5rem; }
#messageInput { flex: 1; padding: 0.5rem; }
</style>
</head>
<body>
<h1>多房间聊天室</h1>
<div class="room-controls">
<label>用户名:<input type="text" id="usernameInput" placeholder="输入昵称" /></label>
<label>房间:<input type="text" id="roomInput" placeholder="房间名" /></label>
<button id="joinBtn">加入房间</button>
<button id="leaveBtn" disabled>离开房间</button>
</div>
<div id="messages"></div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="输入消息..." disabled />
<button id="sendBtn" disabled>发送</button>
</div>
<script>
let ws = null;
let currentRoom = null;
const usernameInput = document.getElementById('usernameInput');
const roomInput = document.getElementById('roomInput');
const joinBtn = document.getElementById('joinBtn');
const leaveBtn = document.getElementById('leaveBtn');
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const messagesDiv = document.getElementById('messages');
function addMessage(type, payload) {
const div = document.createElement('div');
if (type === 'system') {
div.className = 'system-msg';
div.textContent = payload.message;
} else if (type === 'chat') {
div.className = 'chat-msg';
div.innerHTML = `<span class="user">${payload.username}:</span> ${payload.text}`;
}
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function connectWebSocket() {
ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
console.log('WebSocket 连接已建立');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'join_success':
currentRoom = data.payload.room;
document.getElementById('roomInput').disabled = true;
joinBtn.disabled = true;
leaveBtn.disabled = false;
messageInput.disabled = false;
sendBtn.disabled = false;
addMessage('system', { message: `你已加入房间 ${currentRoom}` });
break;
case 'system':
addMessage('system', data.payload);
break;
case 'chat':
addMessage('chat', data.payload);
break;
}
};
ws.onclose = () => {
console.log('WebSocket 连接关闭');
};
ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
}
joinBtn.addEventListener('click', () => {
const username = usernameInput.value.trim();
const room = roomInput.value.trim();
if (!username || !room) {
alert('请输入用户名和房间名');
return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
connectWebSocket();
// 等待连接成功后再发送加入请求
ws.addEventListener('open', () => {
ws.send(JSON.stringify({
type: 'join',
payload: { username, room }
}));
}, { once: true });
} else {
ws.send(JSON.stringify({
type: 'join',
payload: { username, room }
}));
}
});
leaveBtn.addEventListener('click', () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'leave' }));
}
resetUI();
});
sendBtn.addEventListener('click', () => {
const text = messageInput.value.trim();
if (text && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
payload: { text }
}));
messageInput.value = '';
}
});
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendBtn.click();
}
});
function resetUI() {
currentRoom = null;
document.getElementById('roomInput').disabled = false;
joinBtn.disabled = false;
leaveBtn.disabled = true;
messageInput.disabled = true;
sendBtn.disabled = true;
}
// 初始状态检查
leaveBtn.disabled = true;
messageInput.disabled = true;
sendBtn.disabled = true;
</script>
</body>
</html>
运行并测试多房间聊天
1. 启动服务端
在项目根目录执行:
node server.js
终端输出 WebSocket 服务器运行在 ws://localhost:8080 表示启动成功。
2. 打开前端页面
直接用浏览器打开 index.html 文件(或者通过 Live Server 等工具打开)。
3. 模拟多房间体验
- 打开两个浏览器标签页(或两个不同浏览器)。
- 在第一个页面输入用户名
Alice,房间general,点击“加入房间”。 - 在第二个页面输入用户名
Bob,房间general,点击“加入房间”。 - 此时双方都能看到系统消息提示加入。
- 输入消息并发送,两人都能收到对方的聊天内容。
- 再开第三个标签,用户名
Charlie,加入房间random。Charlie 和general房间的用户互相看不见消息,因为属于不同房间。
4. 广播验证
- Alice 发送一条消息,Bob 立即收到,而 Charlie 不会收到。
- 离开房间或关闭标签页时,系统会广播离开提示。
扩展与优化方向
增加房间列表查看
可以在服务端维护一个房间列表的查询接口,或者通过 WebSocket 自定义消息类型 list_rooms,返回 rooms Map 的所有键。
消息持久化
目前消息只存在于内存,刷新页面后历史消息丢失。可以引入 Redis 或数据库存储房间消息,用户加入时拉取最近的历史记录。
身份认证
使用 JWT 或 session 验证用户身份,避免随意修改用户名。
前端框架集成
用 React、Vue 等框架重构前端,实现更丰富的 UI 和状态管理。
生产环境部署
- 使用 Nginx 反向代理 WebSocket 连接
- 开启 SSL/TLS(wss://),保证通信安全
- 考虑负载均衡时的 WebSocket 会话保持(如 sticky session 或使用 Redis 广播)
总结
本教程通过一个完整示例展示了如何用原生 WebSocket 实现多房间聊天室,重点解决了房间管理和消息广播两个核心难题。你学到的不只是调用 API,更是实时通信系统的典型架构设计。动手实践后,你可以轻松将其改造为各类实时协作应用的基础。