Socket.io: чат на Node.js и что он добавляет к WebSocket
✓ Все примеры выполнены на Socket.io 4.8.3, Node.js v22.23.1 — июль 2026
Содержание
Socket.io — самый частый ответ на «сделать чат на Node». Разберём, что он даёт поверх чистого WebSocket и где расставлены грабли.
Весь код выполнен на Socket.io 4.8.3, сервер и клиент — в одном процессе.
Это не WebSocket
Первое, что нужно понять: Socket.io — не реализация WebSocket, а собственный протокол поверх него.
Следствие сразу практическое: клиент Socket.io не подключится к серверу ws, а браузерный new WebSocket() не подключится к Socket.io. Они говорят на разных языках, хотя транспорт снизу тот же.
клиент подключён, id: -vf97L…
транспорт: websocket
Оговорка к популярному мифу: часто пишут, что Socket.io «сначала подключается через polling, потом апгрейдится до WebSocket». В нашем замере из Node клиент сразу пошёл по WebSocket — апгрейда не было. Polling остаётся запасным вариантом для окружений, где WebSocket не проходит (старый прокси, корпоративный файрвол), и в браузере поведение может отличаться. Именно этот откат — одна из причин брать Socket.io.
Сервер и клиент
import { createServer } from 'node:http';
import { Server } from 'socket.io';
const httpServer = createServer();
const io = new Server(httpServer);
io.on('connection', (socket) => {
console.log('подключился', socket.id);
socket.on('chat', (msg) => {
io.emit('chat', msg); // всем
});
});
httpServer.listen(3000);
Socket.io цепляется к обычному HTTP-серверу — тому же, что отдаёт страницы. Отдельный порт не нужен.
Модель — события: emit шлёт, on слушает, на обеих сторонах одинаково.
emit и broadcast: разница, которую путают
socket.on('chat', (msg) => io.emit('chat', msg)); // всем
socket.on('private', (msg) => socket.broadcast.emit('chat', msg)); // всем, кроме автора
io.emit -> A получил: [ 'привет из клиента A' ]
socket.broadcast -> A получил: [] <- пусто
B получил: [ 'это broadcast от A' ]
io.emit — от лица сервера всем подключённым, включая отправителя. Для чата это обычно и нужно: автор видит своё сообщение в общем потоке.
socket.broadcast.emit — всем, кроме этого сокета. Нужен, когда автор уже отрисовал сообщение у себя локально и дубль ему не нужен, или для событий вроде «пользователь печатает».
io.emit('chat', msg)
От лица сервера — всем
- Отправитель получит тоже
- Обычный выбор для чата
- Автор видит своё сообщение в потоке
socket.broadcast.emit(...)
Всем, кроме этого сокета
- Отправитель не получит
- «Пользователь печатает…»
- Когда автор уже отрисовал у себя
io.to('room').emit(...)
Только участникам комнаты
- Остальные не увидят ничего
- Приватные чаты, каналы
- Комната создаётся сама при
join
Комнаты
socket.on('join', (room) => {
socket.join(room);
io.to(room).emit('chat', `в комнате ${room} новый участник`);
});
socket.on('room-msg', ({ room, text }) => {
io.to(room).emit('chat', text);
});
B (в комнате vip) : [ 'в комнате vip новый участник', 'сообщение только для комнаты vip' ]
A (не в комнате) : [] <- пусто, комната изолирует
Комната — просто строка. Создавать её не нужно: она появляется при первом join и исчезает, когда последний участник вышел. Каждый сокет автоматически состоит в комнате со своим socket.id — поэтому io.to(someSocketId).emit(...) работает как личное сообщение.
⚠️ Комнаты живут в памяти одного процесса. Запустили второй инстанс за балансировщиком — и он про эти комнаты ничего не знает. Нужен @socket.io/redis-adapter, который синхронизирует их через Redis pub/sub. Это первое, обо что спотыкаются при масштабировании.
Подтверждения: RPC поверх событий
// сервер
socket.on('need-answer', (payload, ack) => {
ack('ответ сервера на ' + payload);
});
// клиент
const answer = await a.emitWithAck('need-answer', 'вопрос');
emitWithAck вернул: ответ сервера на вопрос
Третий аргумент обработчика — функция подтверждения. Вызвали её — и emitWithAck на клиенте разрешил промис. Получается запрос-ответ поверх событийной модели: удобно, когда нужно узнать, что сервер принял сообщение.
Главная ловушка: socket.id меняется
console.log('connected до обрыва :', a.connected);
a.io.engine.close(); // рвём соединение
// ...ждём автопереподключение
connected до обрыва : true
connected сразу после: false
переподключился сам : true | новый id: MfXyyn…
Клиент вернулся сам — и с другим id. Автопереподключение это то, ради чего Socket.io и берут: писать его руками поверх чистого WebSocket муторно.
Но отсюда правило, которое нарушают постоянно: не привязывайте состояние к socket.id. Хранили «пользователь Аня = сокет abc123» — после обрыва сети Аня вернулась как xyz789, и ваша таблица врёт.
Привязывайте к своему идентификатору пользователя, а socket.id считайте временным. Проверка это подтверждает: сервер за прогон увидел три подключения, хотя клиентов было два — третье и есть переподключившийся A.
подключился -vf97L…
подключился Bj_qbF…
подключился MfXyyn… <- это снова A после обрыва
всего сокетов сейчас: 2
Пространства имён: не то же самое, что комнаты
Их постоянно путают, а разница принципиальная.
const admin = io.of('/admin'); // пространство имён
admin.on('connection', (socket) => {
socket.join('reports'); // комната внутри пространства
});
Пространство имён (/admin) — это отдельная точка входа со своими обработчиками и своим middleware. Клиент подключается к нему явно: ioClient(url + '/admin'). Это способ разделить логику: у админки свои события, у чата свои, и они не пересекаются.
Комната — это группа внутри пространства имён. Клиент в неё не подключается, его туда добавляет сервер через join. Это способ разделить аудиторию.
| Пространство имён | Комната | |
|---|---|---|
| Кто решает | клиент (подключается сам) | сервер (socket.join) |
| Зачем | разделить логику и middleware | разделить аудиторию |
| Своё соединение | нет, транспорт общий | нет |
| Аутентификация | своя на каждое | общая |
Важная деталь: разные пространства имён не создают новых TCP-соединений — они мультиплексируются поверх одного. Так что бояться «лишних подключений» не нужно.
Аутентификацию удобно вешать именно на пространство имён:
admin.use((socket, next) => {
const token = socket.handshake.auth.token;
isAdmin(token) ? next() : next(new Error('нет доступа'));
});
Socket.io или чистый WebSocket
| Нужно | Брать |
|---|---|
| Двусторонний канал, переподключение пишете сами | ws |
| Комнаты, автопереподключение, подтверждения | Socket.io |
| Только сервер → клиент | SSE, не сокеты вообще |
| Несколько инстансов | Socket.io + redis-adapter |
Цена Socket.io — свой протокол (несовместимость с чистым WebSocket) и лишний вес на клиенте. Плата за то, что переподключение, комнаты и откат на polling уже написаны.
Что было на этой странице в 2017 году
Первая версия вышла 30 июля 2017 года и называлась «Создание чата с нуля. Часть 1» — с Express в связке.
Что изменилось по существу:
emitWithAckне было. Подтверждения существовали, но только колбэком:socket.emit('event', data, (ответ) => {...}). Промис-версия появилась в 4-й ветке, и с ней подтверждение стало обычнымawait.- Express был обязателен по привычке. Сегодня видно, что Socket.io цепляется к любому HTTP-серверу — встроенного
node:httpдостаточно, если вы не раздаёте статику. - Про несколько инстансов не думали. В 2017-м чат обычно жил на одной машине, и вопрос «комнаты в памяти процесса» не всплывал. Сейчас это первое, обо что спотыкаются.
Что не изменилось совсем: модель emit/on и разница между io.emit и broadcast. Код чата из той статьи концептуально работает и сегодня — поменялись версии, а не идея.
Смежные темы: как устроен WebSocket — что именно Socket.io прячет; Redis — нужен для комнат на нескольких инстансах; HTTP-сервер — к чему цепляется Socket.io. Полный список — в справочнике по Node.js.
Частые вопросы
Socket.io — это WebSocket?
new WebSocket() к серверу Socket.io тоже не подключится.Чем `io.emit` отличается от `socket.broadcast.emit`?
io.emit шлёт всем, включая отправителя. socket.broadcast.emit — всем, кроме него. Проверено: при broadcast автор сообщения не получил ничего.Можно ли хранить состояние по `socket.id`?
socket.id считайте временным.Нужен ли Socket.io, если есть встроенный WebSocket?
ws. Socket.io берут ради комнат, автопереподключения, подтверждений и отката на polling.Работает ли Socket.io на нескольких серверах?
@socket.io/redis-adapter или аналог.