Socket.IO 房间与命名空间 #
房间(Rooms) #
什么是房间? #
房间是 Socket.IO 提供的一种将客户端分组的机制,方便向特定群组广播消息。
text
┌─────────────────────────────────────────────────────────────┐
│ 房间概念 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 服务端 │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ Room A │ │ Room B │ │ Room C │ │
│ ├─────────┤ ├─────────┤ ├─────────┤ │
│ │ socket1 │ │ socket3 │ │ socket5 │ │
│ │ socket2 │ │ socket4 │ │ socket6 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 特点: │
│ - 每个 socket 自动加入以自己 ID 命名的房间 │
│ - 一个 socket 可以加入多个房间 │
│ - 房间只在服务端存在 │
│ - 断开连接自动离开所有房间 │
│ │
└─────────────────────────────────────────────────────────────┘
加入房间 #
javascript
io.on('connection', (socket) => {
socket.join('room-1');
socket.join(['room-1', 'room-2']);
socket.join('room-1', () => {
console.log(`${socket.id} 已加入 room-1`);
});
socket.on('join room', (roomName) => {
socket.join(roomName);
socket.emit('joined', roomName);
socket.to(roomName).emit('user joined', socket.id);
});
});
离开房间 #
javascript
io.on('connection', (socket) => {
socket.leave('room-1');
socket.leave('room-1', () => {
console.log(`${socket.id} 已离开 room-1`);
});
socket.on('leave room', (roomName) => {
socket.leave(roomName);
socket.to(roomName).emit('user left', socket.id);
});
});
查看房间信息 #
javascript
io.on('connection', (socket) => {
console.log(socket.rooms);
console.log(socket.id);
const rooms = io.of('/').adapter.rooms;
console.log(rooms.get('room-1'));
const sockets = await io.in('room-1').fetchSockets();
console.log(`房间内有 ${sockets.length} 个用户`);
const allSockets = await io.fetchSockets();
console.log(`总共有 ${allSockets.length} 个连接`);
});
向房间发送消息 #
javascript
io.on('connection', (socket) => {
io.to('room-1').emit('message', 'Hello room 1');
io.to('room-1').to('room-2').emit('message', 'Hello room 1 and 2');
io.except('room-1').emit('message', 'Hello everyone except room 1');
socket.to('room-1').emit('message', 'Hello room 1 from another user');
socket.broadcast.to('room-1').emit('message', 'Same as above');
});
广播方式对比 #
text
┌─────────────────────────────────────────────────────────────┐
│ 广播方式对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ io.emit('event', data) │
│ ───────────────────────────────────────────────────────── │
│ 发送给所有连接的客户端 │
│ │
│ socket.broadcast.emit('event', data) │
│ ───────────────────────────────────────────────────────── │
│ 发送给除当前 socket 外的所有客户端 │
│ │
│ io.to('room').emit('event', data) │
│ ───────────────────────────────────────────────────────── │
│ 发送给特定房间的所有客户端 │
│ │
│ socket.to('room').emit('event', data) │
│ ───────────────────────────────────────────────────────── │
│ 发送给特定房间除当前 socket 外的客户端 │
│ │
│ io.to('room1').to('room2').emit('event', data) │
│ ───────────────────────────────────────────────────────── │
│ 发送给多个房间的客户端 │
│ │
│ io.except('room').emit('event', data) │
│ ───────────────────────────────────────────────────────── │
│ 发送给除特定房间外的所有客户端 │
│ │
└─────────────────────────────────────────────────────────────┘
房间应用示例 #
聊天室 #
javascript
io.on('connection', (socket) => {
socket.on('join', ({ username, room }) => {
socket.join(room);
socket.username = username;
socket.room = room;
socket.to(room).emit('user joined', {
username,
message: `${username} 加入了聊天室`
});
socket.emit('joined', {
room,
message: `欢迎来到 ${room} 聊天室`
});
});
socket.on('chat message', (message) => {
io.to(socket.room).emit('chat message', {
username: socket.username,
message,
timestamp: Date.now()
});
});
socket.on('disconnect', () => {
if (socket.room) {
socket.to(socket.room).emit('user left', {
username: socket.username,
message: `${socket.username} 离开了聊天室`
});
}
});
});
游戏房间 #
javascript
const gameRooms = new Map();
io.on('connection', (socket) => {
socket.on('create game', (gameId) => {
socket.join(`game-${gameId}`);
socket.gameId = gameId;
gameRooms.set(gameId, {
players: [socket.id],
state: 'waiting'
});
socket.emit('game created', { gameId });
});
socket.on('join game', (gameId) => {
const room = gameRooms.get(gameId);
if (!room) {
return socket.emit('error', { message: '游戏不存在' });
}
if (room.players.length >= 4) {
return socket.emit('error', { message: '游戏已满' });
}
socket.join(`game-${gameId}`);
socket.gameId = gameId;
room.players.push(socket.id);
io.to(`game-${gameId}`).emit('player joined', {
players: room.players
});
});
socket.on('game move', (move) => {
socket.to(`game-${socket.gameId}`).emit('game move', {
player: socket.id,
move
});
});
socket.on('disconnect', () => {
if (socket.gameId) {
const room = gameRooms.get(socket.gameId);
if (room) {
room.players = room.players.filter(id => id !== socket.id);
io.to(`game-${socket.gameId}`).emit('player left', {
player: socket.id
});
}
}
});
});
在线状态 #
javascript
const onlineUsers = new Map();
io.on('connection', (socket) => {
socket.on('set status', (status) => {
socket.status = status;
socket.rooms.forEach(room => {
if (room !== socket.id) {
io.to(room).emit('user status', {
userId: socket.id,
status
});
}
});
});
socket.on('join channel', (channelId) => {
socket.join(`channel-${channelId}`);
const sockets = io.sockets.adapter.rooms.get(`channel-${channelId}`);
const users = [];
for (const socketId of sockets) {
const s = io.sockets.sockets.get(socketId);
users.push({
id: socketId,
status: s.status || 'online'
});
}
socket.emit('channel users', users);
});
});
命名空间(Namespaces) #
什么是命名空间? #
命名空间是一种逻辑隔离机制,允许在同一个连接上创建多个独立的通信通道。
text
┌─────────────────────────────────────────────────────────────┐
│ 命名空间架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 服务端 │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ / │ │ /chat │ │ /game │ │
│ │ 默认 │ │ 聊天 │ │ 游戏 │ │
│ ├─────────┤ ├─────────┤ ├─────────┤ │
│ │ socket1 │ │ socket2 │ │ socket3 │ │
│ │ socket4 │ │ socket5 │ │ socket6 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 特点: │
│ - 默认命名空间是 / │
│ - 每个命名空间独立的事件处理 │
│ - 每个命名空间独立的中间件 │
│ - 每个命名空间独立的房间管理 │
│ │
└─────────────────────────────────────────────────────────────┘
创建命名空间 #
javascript
const io = require('socket.io')(httpServer);
io.on('connection', (socket) => {
console.log('默认命名空间连接:', socket.id);
});
const chatNamespace = io.of('/chat');
chatNamespace.on('connection', (socket) => {
console.log('聊天命名空间连接:', socket.id);
});
const gameNamespace = io.of('/game');
gameNamespace.on('connection', (socket) => {
console.log('游戏命名空间连接:', socket.id);
});
io.of(/^\/dynamic-\d+$/).on('connection', (socket) => {
console.log('动态命名空间:', socket.nsp.name);
});
客户端连接命名空间 #
javascript
const socket = io();
const chatSocket = io('/chat');
const gameSocket = io('/game');
const customSocket = io('http://localhost:3001/chat', {
auth: { token: 'xxx' }
});
命名空间操作 #
javascript
const chat = io.of('/chat');
chat.on('connection', (socket) => {
console.log('用户连接:', socket.id);
socket.emit('welcome', '欢迎来到聊天室');
chat.emit('user count', (await chat.allSockets()).size);
chat.to('room-1').emit('message', 'Hello');
});
chat.use((socket, next) => {
const token = socket.handshake.auth.token;
if (isValidToken(token)) {
next();
} else {
next(new Error('认证失败'));
}
});
chat.emit('announcement', '系统公告');
命名空间应用示例 #
多应用共用连接 #
javascript
const mainApp = io.of('/main');
const adminApp = io.of('/admin');
const notificationApp = io.of('/notifications');
mainApp.on('connection', (socket) => {
console.log('主应用用户连接');
socket.on('chat', (msg) => {
mainApp.emit('chat', msg);
});
});
adminApp.use((socket, next) => {
const token = socket.handshake.auth.token;
if (isAdmin(token)) {
next();
} else {
next(new Error('需要管理员权限'));
}
});
adminApp.on('connection', (socket) => {
console.log('管理员连接');
socket.on('broadcast', (msg) => {
mainApp.emit('admin message', msg);
});
});
notificationApp.on('connection', (socket) => {
console.log('通知服务连接');
socket.join(`user-${socket.userId}`);
});
function sendNotification(userId, notification) {
notificationApp.to(`user-${userId}`).emit('notification', notification);
}
租户隔离 #
javascript
const tenants = new Map();
io.of(/^\/tenant-.+$/).on('connection', (socket) => {
const tenantId = socket.nsp.name.replace('/tenant-', '');
console.log(`租户 ${tenantId} 用户连接`);
socket.on('message', (data) => {
socket.nsp.emit('message', data);
});
socket.on('join room', (roomId) => {
socket.join(`tenant-${tenantId}-room-${roomId}`);
});
});
app.post('/create-tenant', (req, res) => {
const tenantId = generateTenantId();
tenants.set(tenantId, { id: tenantId, createdAt: new Date() });
res.json({ tenantId, namespace: `/tenant-${tenantId}` });
});
房间与命名空间对比 #
text
┌─────────────────────────────────────────────────────────────┐
│ 房间 vs 命名空间 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 房间(Rooms): │
│ ───────────────────────────────────────────────────────── │
│ - 用于分组广播 │
│ - 同一命名空间内 │
│ - 动态创建和销毁 │
│ - 轻量级 │
│ - 适合:聊天室、游戏房间、频道 │
│ │
│ 命名空间(Namespaces): │
│ ───────────────────────────────────────────────────────── │
│ - 用于逻辑隔离 │
│ - 独立的事件处理 │
│ - 独立的中间件 │
│ - 需要预先定义 │
│ - 适合:多应用、租户隔离、权限分离 │
│ │
│ 组合使用: │
│ ───────────────────────────────────────────────────────── │
│ 命名空间 > 房间 > 客户端 │
│ /chat > room-1 > socket1, socket2 │
│ /chat > room-2 > socket3, socket4 │
│ /game > lobby > socket5, socket6 │
│ │
└─────────────────────────────────────────────────────────────┘
Adapter 适配器 #
默认内存适配器 #
javascript
const io = new Server(httpServer);
io.of('/').adapter.on('create-room', (room) => {
console.log(`房间创建: ${room}`);
});
io.of('/').adapter.on('join-room', (room, id) => {
console.log(`Socket ${id} 加入房间 ${room}`);
});
io.of('/').adapter.on('leave-room', (room, id) => {
console.log(`Socket ${id} 离开房间 ${room}`);
});
io.of('/').adapter.on('delete-room', (room) => {
console.log(`房间删除: ${room}`);
});
Redis 适配器(多服务器) #
javascript
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
Promise.all([
pubClient.connect(),
subClient.connect()
]).then(() => {
io.adapter(createAdapter(pubClient, subClient));
io.on('connection', (socket) => {
socket.join('room-1');
io.to('room-1').emit('hello');
});
});
其他适配器 #
text
┌─────────────────────────────────────────────────────────────┐
│ 适配器选择 │
├─────────────────────────────────────────────────────────────┤
│ │
│ @socket.io/redis-adapter │
│ ───────────────────────────────────────────────────────── │
│ 基于 Redis 的适配器 │
│ 适合:多服务器、高可用 │
│ │
│ @socket.io/mongo-adapter │
│ ───────────────────────────────────────────────────────── │
│ 基于 MongoDB 的适配器 │
│ 适合:已有 MongoDB 基础设施 │
│ │
│ @socket.io/postgres-adapter │
│ ───────────────────────────────────────────────────────── │
│ 基于 PostgreSQL 的适配器 │
│ 适合:已有 PostgreSQL 基础设施 │
│ │
│ @socket.io/cluster-adapter │
│ ───────────────────────────────────────────────────────── │
│ 基于 Node.js 集群的适配器 │
│ 适合:单机多进程 │
│ │
└─────────────────────────────────────────────────────────────┘
完整示例:协作编辑 #
javascript
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: '*' } });
const documents = io.of('/documents');
documents.on('connection', (socket) => {
console.log('用户连接:', socket.id);
socket.on('join document', async (documentId) => {
socket.join(`doc-${documentId}`);
socket.documentId = documentId;
const sockets = await documents.in(`doc-${documentId}`).fetchSockets();
const users = sockets.map(s => ({
id: s.id,
name: s.username,
cursor: s.cursor
}));
socket.emit('document users', users);
socket.to(`doc-${documentId}`).emit('user joined', {
id: socket.id,
name: socket.username
});
});
socket.on('edit', (changes) => {
socket.to(`doc-${socket.documentId}`).emit('edit', {
userId: socket.id,
changes
});
});
socket.on('cursor move', (position) => {
socket.cursor = position;
socket.to(`doc-${socket.documentId}`).emit('cursor move', {
userId: socket.id,
position
});
});
socket.on('selection', (range) => {
socket.to(`doc-${socket.documentId}`).emit('selection', {
userId: socket.id,
range
});
});
socket.on('leave document', () => {
if (socket.documentId) {
socket.to(`doc-${socket.documentId}`).emit('user left', {
id: socket.id
});
socket.leave(`doc-${socket.documentId}`);
socket.documentId = null;
}
});
socket.on('disconnect', () => {
if (socket.documentId) {
socket.to(`doc-${socket.documentId}`).emit('user left', {
id: socket.id
});
}
});
});
httpServer.listen(3001, () => {
console.log('协作编辑服务运行在 http://localhost:3001');
});
下一步 #
最后更新:2026-03-29