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');
});

下一步 #

现在你已经掌握了 Socket.IO 的房间和命名空间,接下来学习 中间件,了解如何实现认证、授权和日志记录!

最后更新:2026-03-29