5. Socket.io: комнаты и Namespaces
Комнаты и Namespaces — ключевые механизмы Socket.io для организации соединений.
Rooms (Комнаты)
Заголовок раздела «Rooms (Комнаты)»Комнаты позволяют группировать сокеты и отправлять события группам:
// Войти в комнатуsocket.join('room-1');socket.join(['room-1', 'room-2']); // Несколько комнат
// Выйти из комнатыsocket.leave('room-1');
// Отправить в комнатуio.to('room-1').emit('event', data); // всем в комнатеsocket.to('room-1').emit('event', data); // всем кроме себяio.to('room-1').to('room-2').emit('event', data); // в несколько комнат
// Узнать в каких комнатах находится сокетconsole.log(socket.rooms); // Set { socket.id, 'room-1', 'room-2' }Пример: чат по комнатам
Заголовок раздела «Пример: чат по комнатам»io.on('connection', (socket) => { // Войти в комнату чата socket.on('join-room', ({ roomId, username }) => { socket.join(roomId); socket.data.username = username; socket.data.roomId = roomId;
// Уведомить остальных в комнате socket.to(roomId).emit('user-joined', { username, message: `${username} вошёл в чат`, });
// Отправить историю новому пользователю const history = getMessageHistory(roomId); socket.emit('message-history', history); });
// Сообщение в комнату socket.on('send-message', ({ text }) => { const { username, roomId } = socket.data;
const message = { id: generateId(), username, text, timestamp: new Date().toISOString(), };
// Сохранить и отправить всем в комнате saveMessage(roomId, message); io.to(roomId).emit('new-message', message); });
// Покинуть комнату socket.on('leave-room', ({ roomId }) => { socket.leave(roomId); socket.to(roomId).emit('user-left', { username: socket.data.username, }); });
// Авто-выход при отключении socket.on('disconnect', () => { const { username, roomId } = socket.data; if (roomId) { socket.to(roomId).emit('user-left', { username }); } });});Список сокетов в комнате
Заголовок раздела «Список сокетов в комнате»// Получить сокеты в комнатеasync function getSocketsInRoom(roomId) { const sockets = await io.in(roomId).fetchSockets(); return sockets.map(s => ({ id: s.id, username: s.data.username, }));}
// Количество пользователей в комнатеconst room = io.sockets.adapter.rooms.get('room-1');const count = room ? room.size : 0;Namespaces
Заголовок раздела «Namespaces»Namespace — логическое разделение одного Socket.io сервера:
Один сервер, разные пространства:/ — главный namespace (по умолчанию)/admin — административная панель/chat — чат/game — игровая логика/notifications — уведомления// Сервер — создаём namespacesconst chatNs = io.of('/chat');const adminNs = io.of('/admin');
// Middleware только для /adminadminNs.use((socket, next) => { if (socket.handshake.auth.role === 'admin') { next(); } else { next(new Error('Только для администраторов')); }});
chatNs.on('connection', (socket) => { console.log('Chat клиент:', socket.id);});
adminNs.on('connection', (socket) => { console.log('Admin клиент:', socket.id);
// Отправить статистику socket.emit('stats', { connections: chatNs.sockets.size, uptime: process.uptime(), });});
// Клиент — подключиться к namespaceimport { io } from 'socket.io-client';
const chatSocket = io('http://localhost:3000/chat');const adminSocket = io('http://localhost:3000/admin', { auth: { role: 'admin', token: adminToken },});Dynamic Namespaces
Заголовок раздела «Dynamic Namespaces»Динамически создаваемые пространства (например, для multi-tenant):
// Regex patternio.of(/^\/workspace-[a-z0-9]+$/).on('connection', (socket) => { const namespace = socket.nsp.name; // '/workspace-abc123' const workspaceId = namespace.slice(11); // 'abc123'
console.log(`Подключение к workspace: ${workspaceId}`);
// Middleware для проверки доступа socket.nsp.use(async (socket, next) => { const userId = socket.handshake.auth.userId; const hasAccess = await checkWorkspaceAccess(userId, workspaceId); if (hasAccess) next(); else next(new Error('Нет доступа')); });});Rooms vs Namespaces
Заголовок раздела «Rooms vs Namespaces»| Rooms | Namespaces | |
|---|---|---|
| Создание | Динамически (socket.join) | Явно (io.of('/ns')) |
| Middleware | Нет | Есть |
| Разные соединения | Нет (одно TCP) | Да (разные соединения) |
| Использование | Группировка в рамках фичи | Разные фичи/секции |
Практический пример: система уведомлений
Заголовок раздела «Практический пример: система уведомлений»// Серверconst io = new Server(httpServer);const notifNs = io.of('/notifications');
// Каждый пользователь в своей "комнате"notifNs.on('connection', async (socket) => { const userId = socket.handshake.auth.userId;
// Пользователь подписывается на свои уведомления socket.join(`user:${userId}`);
// Отправить непрочитанные уведомления const unread = await getUnreadNotifications(userId); socket.emit('unread-notifications', unread);
// Отметить как прочитанное socket.on('mark-read', async (notificationId) => { await markAsRead(notificationId, userId); socket.emit('notification-read', notificationId); });});
// В другом месте кода — отправить уведомление пользователюasync function sendNotification(userId, notification) { await db.notifications.create({ data: { ...notification, userId } }); notifNs.to(`user:${userId}`).emit('new-notification', notification);}
// Клиентconst notifSocket = io('/notifications', { auth: { userId: currentUser.id, token: authToken },});
notifSocket.on('new-notification', (notification) => { showNotificationBanner(notification); updateNotificationCount();});
notifSocket.on('unread-notifications', (notifications) => { renderNotificationList(notifications);});Emit на все namespace/rooms
Заголовок раздела «Emit на все namespace/rooms»// Всем во всех namespace (исключая sender)io.emit('server-restart', { in: 60 });
// Конкретный socket по IDio.to(socketId).emit('private-message', data);
// Исключить сокетыio.except(socket.id).emit('broadcast', data);
// Всем в комнате, кроме конкретных сокетовio.to('room-1').except([socket1.id, socket2.id]).emit('event', data);Persistent rooms с Redis
Заголовок раздела «Persistent rooms с Redis»Для горизонтального масштабирования нужен адаптер:
const { createAdapter } = require('@socket.io/redis-adapter');const { createClient } = require('redis');
const pubClient = createClient({ url: 'redis://localhost:6379' });const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
// Теперь io.to('room').emit() работает на всех серверах!Задания
Заголовок раздела «Задания»- Создай систему чата с несколькими комнатами. Покажи список онлайн-пользователей
- Реализуй namespace
/adminс проверкой роли через middleware - Добавь “typing indicator” (кто сейчас печатает) в комнату чата
- Как отправить сообщение конкретному пользователю, зная только его userId?
Rooms — для группировки внутри одной логики (чат-комнаты, игровые сессии). Namespaces — для разных фич (чат / админка / уведомления). В следующем уроке — Server-Sent Events как более простая альтернатива для однонаправленных потоков.