Real-Time Backend
Rooms и Channels
Discord обслуживает 800K участников в одном guild. Как сервер знает, кому отправить сообщение в #general, а кому - в #gaming? Без абстракции rooms каждый emit пришлось бы адресовать миллионам соединений вручную.
- Discord guilds: каждый текстовый канал - отдельная room. Участник получает push только из каналов где он присутствует. 19 млн активных серверов работают на этой абстракции.
- Slack channels: лимит 50K участников на канал продиктован математикой broadcast - при 50K сокетах один emit генерирует 5MB трафика. Slack хранит channel membership в PostgreSQL, а активные соединения - в Redis.
- Twitch raid: рейд перемещает 50K зрителей между channels атомарно. На уровне WebSocket это 50K параллельных leave+join. Redis Cluster делает это через Lua-скрипт за одну транзакцию.
- Google Docs: каждый документ - это room. Операционные трансформации broadcast'ятся только участникам room. Когда последний редактор закрывает вкладку, room удаляется из памяти (но документ сохранён в БД).
Rooms Concept
**Room** - это именованная группа соединений, объединённых общим контекстом. Сервер хранит маппинг `roomId -> Set<socketId>` и использует его для адресной доставки сообщений. Клиент сам по себе не знает, кто ещё в комнате - это знает только сервер.
Discord реализует эту модель в масштабе: сервер (guild) с 800K+ участниками - это иерархия комнат. Каждый текстовый канал - отдельный room. Участник получает сообщения только из тех каналов, в которых присутствует его соединение. Голосовые каналы - отдельный тип room с дополнительными метаданными (bitrate, region).
Room - это серверная абстракция. На транспортном уровне нет никаких "комнат" - есть только отдельные WebSocket-соединения. Весь роутинг делает сервер, итерируя по `Set<socketId>` и отправляя каждому индивидуально.
- Room (namespaced) — Динамически создаётся/удаляется. Участники приходят и уходят. Пример: игровая сессия, чат-комната, live-сессия редактора.
- Channel (persistent topic) — Существует независимо от подписчиков. Slack channel с 50K+ участников существует, даже когда все offline. Пример: pub/sub топик, Discord #general.
Что происходит с room в Socket.io, когда последний участник покидает её?
Rooms Join Leave
Join и leave - это мутации серверного Set. `socket.join('room-id')` добавляет `socket.id` в `adapter.rooms.get('room-id')`. `socket.leave('room-id')` удаляет. При disconnect сервер автоматически вызывает leave для всех комнат этого сокета.
Twitch raid - яркий пример атомарного join: 50K зрителей одновременно покидают один channel и входят в другой. На уровне WebSocket это 50K параллельных join-операций. Redis adapter делает это через Lua-скрипт для атомарности: один SMOVE вместо N отдельных операций.
- `socket.join(room)` - добавляет сокет в room (async в кластерном режиме)
- `socket.leave(room)` - удаляет сокет из room
- `socket.rooms` - Set всех rooms, в которых сейчас сокет
- `io.socketsLeave(room)` - принудительно выгнать всех из room (admin-операция)
- `io.in(room).fetchSockets()` - получить список сокетов в room
В Socket.io: `socket.to('room').emit(...)` vs `io.to('room').emit(...)` - в чём разница?
Rooms Broadcast
Broadcast - это отправка одного сообщения всем участникам room. На одном сервере это цикл по Set<socketId> с вызовом `socket.send()` для каждого. В кластере с несколькими серверами сообщение нужно доставить на каждый узел - для этого используется pub/sub через Redis или другой broker.
Slack ограничивает channels до 50K участников. Это не случайное число - при broadcast в channel с 50K участниками на одном сервере получается 50K системных вызовов write(). При 100 байт на сообщение это 5MB исходящего трафика за один emit. При частоте 10 событий/сек канал генерирует 50MB/s - на грани насыщения 1Gbps линка.
| Паттерн | Метод | Когда использовать |
|---|---|---|
| Всем в room | `io.to(room).emit()` | State sync, объявления |
| Всем кроме себя | `socket.to(room).emit()` | Chat, действия игроков |
| Нескольким rooms | `.to(r1).to(r2).emit()` | Cross-room уведомления |
| Исключить room | `.except(room).emit()` | Mute, ban механики |
| Без гарантии | `.volatile.to(room).emit()` | Позиции, курсоры 60fps |
Игра отправляет позиции игроков 60 раз в секунду. Какой broadcast-паттерн правильный?
Rooms Impl
Реализация rooms без фреймворка - это `Map<string, Set<string>>` на сервере. Socket.io добавляет поверх адаптерный слой: in-memory адаптер для одного сервера, Redis/Postgres адаптер для кластера. Адаптер синхронизирует состояние rooms между инстансами через pub/sub.
В кластере нужен двусторонний индекс: `roomId -> [serverId, socketId][]`. Redis Cluster хранит это как два Hash: `room:{id}:members` и `socket:{id}:rooms`. При join/leave обновляются оба атомарно через Lua-скрипт. Именно так работает `@socket.io/redis-adapter` - он pub/sub'ит broadcast команды между инстансами.
Room - это серверный процесс или отдельный поток, который обрабатывает сообщения участников
Room - это просто Set<socketId> в памяти. Никаких отдельных процессов нет. Это чистая структура данных для адресации.
Путаница возникает из-за термина: в реальной жизни комната - физическое место с границами. В realtime-бэкенде room - это routing label. Весь broadcast - это цикл `for (id of room) socket.send()` в event loop того же процесса.
Зачем в room manager нужен обратный индекс `socketId -> Set<roomId>`?
Ключевые идеи
- Room - это `Map<roomId, Set<socketId>>` на сервере. Не процесс, не поток - просто структура данных для роутинга.
- Join/leave - мутации Set. Disconnect автоматически вызывает leave для всех rooms сокета. Обратный индекс `socketId -> rooms` критичен для O(1) cleanup.
- `socket.to(room)` исключает отправителя; `io.to(room)` включает. Для 60fps updates - volatile emit чтобы не переполнять буфер.
- В кластере rooms синхронизируются через Redis pub/sub. Каждый broadcast публикуется в Redis, все инстансы получают и доставляют своим локальным сокетам.
Связанные темы
Rooms строятся поверх WebSocket и масштабируются через кластерные примитивы.
- WebSocket Protocol — Транспортный слой, поверх которого работают rooms. Room - это серверная абстракция, WebSocket не знает о комнатах.
- Pub/Sub Pattern — Механизм синхронизации rooms между инстансами кластера. Redis pub/sub транслирует broadcast-команды между серверами.
- Horizontal Scaling — Без Redis adapter rooms существуют только на одном инстансе. Для горизонтального масштабирования нужен shared state.
Вопросы для размышления
- Чат-приложение хранит историю сообщений. Где правильнее держать список участников channel - в Redis (вместе с активными сокетами) или только в PostgreSQL?
- Пользователь открыл одну вкладку браузера и залогинился с двух устройств. Нужно ли создавать две разные personal room или объединить их под userId?
- Игровой сервер отправляет позиции всех игроков 60 раз в секунду. При 100 игроках в room это 100 x 100 = 10K сообщений/сек. Как оптимизировать без отказа от broadcast?