Перейти к содержимому

5. Socket.io: комнаты и Namespaces

Комнаты и Namespaces — ключевые механизмы Socket.io для организации соединений.

Комнаты позволяют группировать сокеты и отправлять события группам:

// Войти в комнату
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;

Namespace — логическое разделение одного Socket.io сервера:

Один сервер, разные пространства:
/ — главный namespace (по умолчанию)
/admin — административная панель
/chat — чат
/game — игровая логика
/notifications — уведомления
// Сервер — создаём namespaces
const chatNs = io.of('/chat');
const adminNs = io.of('/admin');
// Middleware только для /admin
adminNs.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(),
});
});
// Клиент — подключиться к namespace
import { io } from 'socket.io-client';
const chatSocket = io('http://localhost:3000/chat');
const adminSocket = io('http://localhost:3000/admin', {
auth: { role: 'admin', token: adminToken },
});

Динамически создаваемые пространства (например, для multi-tenant):

// Regex pattern
io.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('Нет доступа'));
});
});
RoomsNamespaces
СозданиеДинамически (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);
});
// Всем во всех namespace (исключая sender)
io.emit('server-restart', { in: 60 });
// Конкретный socket по ID
io.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);

Для горизонтального масштабирования нужен адаптер:

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() работает на всех серверах!
  1. Создай систему чата с несколькими комнатами. Покажи список онлайн-пользователей
  2. Реализуй namespace /admin с проверкой роли через middleware
  3. Добавь “typing indicator” (кто сейчас печатает) в комнату чата
  4. Как отправить сообщение конкретному пользователю, зная только его userId?

Rooms — для группировки внутри одной логики (чат-комнаты, игровые сессии). Namespaces — для разных фич (чат / админка / уведомления). В следующем уроке — Server-Sent Events как более простая альтернатива для однонаправленных потоков.