Перейти к содержимому
Поиск
Разработка

Socket.IO: события в реальном времени — 8 сценариев и Node.js

Разбираем события, комнаты, подтверждения и разрывы связи на двух клиентах. Интерактивная модель на странице и настоящий Node.js-проект с запуском Windows/Linux и закрытой публикацией на VPS.

Александр Некрасов
Socket.IO: интерфейс в реальном времени — события между сервером и двумя клиентами

Пользователь открыл карточку заявки, а коллега уже взял её в работу. Отчёт собирается на сервере, но человек не видит прогресс. Новое сообщение приходит только после обновления страницы. Socket.IO помогает связать такие изменения с интерфейсом: сервер отправляет события подключённым клиентам, а клиенты могут отправлять команды и получать подтверждения.

Разберём это на маленьком рабочем пространстве с двумя участниками, комнатами, чатом, уведомлениями и учебным отчётом. На странице доступна интерактивная модель; из архива запускается настоящий Node.js + Socket.IO 4.8.4 с тем же интерфейсом и общими правилами. Можно проверить разрыв связи, задержку подтверждения и повтор команды, а затем разместить закрытую демонстрацию на Linux VPS с доменом и HTTPS.

Что такое Socket.IO и где он полезен

Socket.IO — библиотека событийного обмена между клиентом и сервером. Она помогает организовать соединение, отправлять именованные события, группировать подключения в комнаты и восстанавливать связь после временного обрыва. Вместо постоянного ручного обновления страницы интерфейс получает изменения по мере их появления.

Практические применения разнообразны. В CRM можно сообщить отделу о новой заявке. В панели администратора — обновить статус импорта. В службе поддержки — показать присутствие коллег и новые сообщения. В системе мониторинга — прислать свежие показатели. В совместном рабочем процессе — сообщить, что карточку изменил другой пользователь, и предложить перечитать состояние.

Но событие не должно становиться единственным местом хранения истины. Если браузер был закрыт, человеку всё равно нужен актуальный список заявок после входа. Поэтому в реальном приложении основное состояние хранится на сервере, а события помогают быстро доставить изменения. После подключения клиент получает снимок или недостающие записи по известной позиции.

Socket.IO работает поверх транспортных механизмов, включая WebSocket и HTTP long-polling. Он имеет собственный протокол, поэтому обычный WebSocket-клиент не может без дополнительной реализации говорить с Socket.IO-сервером. Если нужен минимальный стандартный WebSocket-канал, выбирают соответствующий API; если нужны комнаты, acknowledgements и удобное управление соединением, Socket.IO даёт готовую основу.

Источник: введение и устройство Socket.IO.

Когда достаточно HTTP, SSE или периодического опроса

Для формы, обычного поиска и редкого обновления списка HTTP часто остаётся самым простым решением. Если данные меняются раз в несколько минут, постоянное соединение может не дать заметной пользы. Сначала определите, какую задержку действительно замечает человек, сколько пользователей подключено и какие изменения необходимо показывать немедленно.

Если сервер только передаёт новости или прогресс клиенту, Server-Sent Events тоже может быть хорошим вариантом. Команды при этом отправляются обычным HTTP. Socket.IO особенно удобен, когда обмен двусторонний, есть несколько типов событий, комнаты, подтверждения и логика переподключения. Это выбор под сценарий, а не обязательный компонент любого современного интерфейса.

Периодический опрос не всегда плох: он прост, легко наблюдается через обычные HTTP-инструменты и бывает достаточен. Однако частый опрос множества вкладок создаёт лишние запросы, а редкий показывает устаревшее состояние. Событийный канал позволяет обновлять экран при изменениях, сохраняя HTTP API для загрузки документов, файлов и первоначальных данных.

Мини-мастерская: два клиента и восемь сценариев

В мастерской есть Анна и Борис. Каждый может подключаться, выбирать комнату, отправлять сообщение, уведомлять коллег и запускать короткий отчёт. Сверху выбирается сценарий: он объясняет цель, последовательность действий и ожидаемый результат. Ниже находятся журнал событий и исходники прикладной логики, адаптера и сервера.

На странице статьи работает учебная модель внутри браузера. Она наглядно показывает состояние и правила, но не создаёт сетевое соединение с Socket.IO-сервером сайта. В архиве находится настоящий Node.js-сервер: при его запуске тот же интерфейс открывает два независимых Socket.IO-соединения. Это различие явно указано в самой мастерской, чтобы посетитель понимал, что именно он проверяет.

Модель и сервер используют общий Hub: он хранит комнаты, сообщения, версии и результаты команд. Поэтому правила проверки текста, распознавания повтора и назначения заявки совпадают. Сетевые особенности проверяются уже в настоящем режиме: задержки, транспорт, переподключение и работа через reverse proxy не могут быть доказаны локальной моделью.

Внутри встроенной мастерской можно прокручивать страницу. На телефоне удобнее открыть её на весь экран.

Как проходит событие: интерфейс, сервер и новое состояние

Представим, что Анна отправляет сообщение. Интерфейс собирает payload с текстом и operationId, затем ждёт acknowledgement. Сервер проверяет, подключён ли участник к комнате, допустим ли текст и не встречалась ли операция раньше. После изменения состояния он отправляет обновлённый снимок участникам нужной комнаты и отдельно подтверждает команду отправителю.

Подтверждение команды и рассылка состояния решают разные задачи. Ack сообщает отправителю, что сервер ответил на его конкретный запрос. Событие state обновляет представление всех получателей. В больших системах вместо полного снимка часто рассылают компактные изменения, но тогда нужно продумать версионирование, пропуски и порядок применения этих изменений.

В учебном проекте полный снимок выбран специально: легко увидеть, какие сообщения и присутствующие пользователи относятся к комнате. У каждого состояния есть revision. Это полезный ориентир при диагностике, но число версии само по себе не делает обновления надёжными: клиент должен правильно обрабатывать устаревшие ответы, а сервер — сохранять нужную историю, если она требуется продукту.

// Клиент Socket.IO. Операция относится к текущей комнате.
const operation = {
  operationId: crypto.randomUUID(),
  text: 'Заявка принята в работу',
};
socket.timeout(900).emit('message:send', operation, (error, result) => {
  if (error) {
    console.log('Результат пока неизвестен');
    return;
  }
  console.log(result.ok ? 'Подтверждено' : result.message);
});

1. Уведомления: событие получает нужный отдел

Уведомление полезно, когда изменение требует внимания человека: появилась новая заявка, документ готов к согласованию или коллега передал обращение. Важно отправить сигнал тем, кому он нужен. Если каждое событие показывается всем, интерфейс превращается в шум и пользователи перестают замечать важные сообщения.

Оставьте Анну и Бориса в комнате «Продажи» и отправьте уведомление от Анны. Его увидят оба клиента. Затем переведите Бориса в «Поддержку» и повторите действие. Новое уведомление останется в продажах. Комнаты позволяют выразить такую маршрутизацию без ручного перебора всех подключений в прикладном коде.

В реальном сервисе имя комнаты может быть связано с отделом, заказом или конкретным пользователем. Но доступ к комнате определяет сервер. Нельзя принимать произвольный projectId от браузера и сразу подписывать человека на чужие события. Сначала проверяют пользователя и его права, затем вызывают join. Предсказуемое имя комнаты не является ни паролем, ни механизмом авторизации.

В нашей демке доступны только две учебные комнаты и два вымышленных имени. Уведомления хранятся ограниченным списком в памяти. Для рабочего центра уведомлений нужны постоянное хранилище, отметки прочтения и возможность загрузить пропущенное после входа. Мгновенный сигнал — лишь одна часть такой системы.

// Сервер: после проверки прав пользователя.
socket.join('sales');
io.to('sales').emit('notice:new', {
  id: 'notice-1042',
  text: 'Появилась новая заявка',
});

Источник: комнаты Socket.IO.

2. Чат: сообщения принадлежат комнате

Чат нужен не только мессенджерам. Обсуждение заказа, комментарии оперативной группы или поддержка внутри личного кабинета используют похожую схему: пользователь отправляет текст, сервер проверяет его и сообщает участникам соответствующего разговора. В мастерской можно сразу сравнить результат у двух клиентов.

Отправьте сообщение от Анны и ответьте от Бориса. Переключите одного участника в другую комнату: там другая история. Вернувшись обратно, он получит текущий снимок. История в примере ограничена последними двадцатью сообщениями на комнату, чтобы интерфейс оставался небольшим и было видно, какие данные демонстрируются.

Пользовательский текст выводится через textContent. Если отправить строку с HTML-тегами, она остаётся текстом. Это простая и важная граница: серверное событие не даёт права выполнять разметку или JavaScript, полученные от другого участника. Если продукт разрешает форматированные сообщения, для них требуется отдельный безопасный формат и проверка.

Для полноценного чата также понадобятся идентификаторы пользователей, история в БД, постраничная загрузка, права на вложения и продуманные статусы доставки. Этот пример показывает канал и согласование состояния; он не обещает готовую систему переписки с хранением после перезапуска.

3. Ack: кнопка отправки и результат операции

После нажатия кнопки интерфейс не знает, что произошло на сервере. Данные могли не пройти проверку, соединение могло оборваться, а операция могла успешно выполниться. Acknowledgement позволяет привязать ответ к конкретной отправке и показать понятный результат: принято, отклонено или ответ пока не получен.

Отправьте пустое сообщение. Сервер вернёт ошибку TEXT, состояние не изменится. Затем отправьте нормальный текст: появится подтверждение с operationId и revision. Сравните журнал и экран. Это показывает, почему успешное соединение само по себе не означает успешное выполнение команды.

В интерфейсе полезно различать ошибку проверки и сетевую неопределённость. При ошибке текста пользователь исправляет поле. При timeout нельзя уверенно написать «не отправлено»: сервер мог сохранить запись, но ответ не успел прийти. Здесь нужна возможность запросить состояние или повторить ту же идемпотентную операцию, а не создавать новую запись вслепую.

Ack является договорённостью вашего приложения. Сервер должен отправлять его после той стадии, которую считает завершением. Если ответить «успешно» до записи в БД, подтверждение не будет означать сохранение данных. Для фоновой задачи можно подтвердить постановку в очередь, отдельно сообщив её идентификатор и последующий прогресс.

Источник: подтверждения и timeout.

4. Присутствие: соединение не равно активности человека

Список присутствующих помогает понять, кто подключён к разговору или панели. Отключите Бориса и посмотрите на список у Анны. Затем подключите его снова: состояние комнаты обновится. В нашем Hub несколько соединений с одним учебным именем показываются одним именем, поэтому дополнительная вкладка не превращает одного человека в нескольких сотрудников.

В рабочем приложении нужно отдельно определить смысл статуса. Открытая вкладка не означает, что человек смотрит на экран. Разрыв соединения не всегда означает выход: телефон мог переключить сеть, браузер уйти в фон, а компьютер — заснуть. Для индикатора «онлайн» обычно задают допустимую задержку и не обещают абсолютную точность.

Если присутствие влияет на важное действие, одной метки недостаточно. Назначение заявки должно проверяться сервером независимо от того, как давно обновился список коллег. Для оценки занятости нужны собственные бизнес-правила, а не только число Socket.IO-подключений.

5. «Печатает…»: событие, которое можно потерять

Начните вводить сообщение от Анны. Борис увидит индикатор набора. Перестаньте печатать — примерно через 1,1 секунды подпись исчезнет. Такие сигналы короткоживущие: если сеть задержала событие на минуту, доставлять его уже бессмысленно.

Для этого используется volatile-событие. Оно допускает потерю, когда канал не готов, и не должно накапливаться как важная операция. Индикатор также имеет время жизни на стороне интерфейса. Иначе пропущенный сигнал «перестал печатать» мог бы навсегда оставить неверную надпись.

Подобная логика подходит для положения курсора, промежуточных координат и некоторых живых показателей, когда следующая свежая порция заменяет предыдущую. Однако так нельзя отправлять заказ, изменение прав или финансовую операцию. Для них потеря события меняет смысл процесса, поэтому нужны другие гарантии и хранение результата.

socket.volatile.emit('typing', {});
socket.on('typing', ({ actor }) => {
  // Покажите подпись и сбросьте таймер её исчезновения.
  console.log(`${actor} печатает…`);
});

6. Прогресс: сервер сообщает о ходе работы

Нажмите «Собрать отчёт». Оба клиента в комнате увидят изменение прогресса и смогут скачать CSV после завершения. Это демонстрирует полезную связь между серверной операцией и интерфейсом: человеку не приходится гадать, работает ли кнопка, и нажимать её повторно.

В этом проекте отчёт — ограниченный двухсекундный таймер. Он не обрабатывает большой файл и не работает в отдельном worker. CSV формируется в браузере из итогового учебного состояния. Такое устройство честно показывает передачу прогресса, но не выдаёт её за надёжную фоновую обработку.

Для импорта, генерации PDF или рассылки лучше использовать очередь, например BullMQ. Worker сохраняет статус задачи и периодически сообщает о прогрессе, а Socket.IO доставляет обновление клиенту. Если браузер отключится, работа продолжится независимо от него; после входа пользователь запросит актуальный статус по jobId.

Частоту обновлений выбирают по смыслу. Отправлять процент на каждом обработанном байте обычно избыточно. Полезнее показывать этап, количество обработанных объектов и ограниченно обновляемый процент. Ошибка и отмена также должны быть отдельными понятными состояниями, а не зависшей полоской на девяноста девяти процентах.

7. Разрыв связи: восстановить канал и перечитать состояние

Отключите Бориса, отправьте сообщение от Анны и подключите Бориса снова. Он увидит актуальный снимок комнаты. Ручная кнопка отключения закрывает клиентское соединение намеренно; её не нужно путать с неожиданной потерей транспорта.

Для проверки временного сбоя есть «Обрыв канала». В настоящем режиме закрывается транспорт, после чего клиент пытается переподключиться. На сервере включён connection state recovery с окном тридцать секунд. Если восстановление удалось, Socket.IO может вернуть состояние соединения и пропущенные пакеты. Но успех не гарантирован: окно могло пройти, процесс мог перезапуститься, а выбранный адаптер — не поддерживать механизм.

Поэтому интерфейс в любом случае получает свежий снимок после подключения. Эта дополнительная сверка делает поведение понятным даже при неудачном recovery. Для больших объёмов вместо полного снимка можно передавать последнюю известную позицию и догружать недостающие изменения из постоянного журнала.

Не прячьте отсутствие связи. Пользователь должен понимать, отправилась ли команда и насколько актуальны данные. В нашей мастерской отключённый клиент не принимает новые команды в очередь автоматически: сначала нужно подключиться. В другом продукте допустима очередь локальных действий, но тогда требуются явные правила конфликтов и повторов.

const io = new Server(httpServer, {
  connectionStateRecovery: {
    maxDisconnectionDuration: 30_000,
    skipMiddlewares: false,
  },
});

Источник: connection state recovery и его ограничения.

8. Timeout и повтор: не создать дубль

Самый показательный сценарий — задержанное подтверждение. Введите текст и нажмите «Отправить с задержкой ack». Сервер применит команду сразу, но намеренно задержит ответ. Клиент раньше получит timeout и сообщит, что результат пока неизвестен. При этом сообщение уже может быть видно в истории.

Теперь повторите тот же ID. Hub найдёт сохранённый результат и вернёт его с признаком duplicate, не добавляя второе сообщение. Ключ включает комнату, участника и operationId; вместе с ним проверяется отпечаток команды. Если попытаться использовать тот же ID с другим содержимым, сервер отклонит запрос, чтобы повтор не превратился в другую операцию.

В демке этот кеш хранит сто операций в памяти. После вытеснения записи или рестарта защита для старого ID исчезает. Для заказов, платежей и других важных действий идемпотентность должна храниться вместе с результатом в постоянной БД, обычно с уникальным ограничением и транзакцией. Сетевой retry без такой защиты способен создать дубль.

Здесь же есть назначение заявки с expectedVersion. Сервер сравнивает ожидаемую версию с текущей: устаревший клиент получает конфликт и новый снимок. Это другой механизм — оптимистическая конкуренция. Он предотвращает незаметное перетирание чужого изменения, тогда как operationId предотвращает повторное выполнение одной команды.

Порядок событий и гарантии доставки

Socket.IO сохраняет порядок событий, которые действительно дошли до получателя. Но порядок не означает обязательную доставку. По умолчанию действует at-most-once: при обрыве в неподходящий момент событие может потеряться. Приложение само решает, нужны ли повторы, история и подтверждения.

Клиентские retries с ackTimeout помогают повторять отправку до подтверждения, однако не создают автоматическую долговечную очередь. Обновление вкладки может потерять ожидающие события. На серверной стороне пропущенные важные изменения нужно хранить и выдавать по идентификатору или позиции, если продукт обещает восстановление после длительного отсутствия.

Поэтому формулировка «Socket.IO гарантирует, что сообщение точно сохранится один раз» неверна без дополнительной архитектуры. Надёжность складывается из договора событий, постоянного хранилища, идемпотентности, правил повторов и восстановления. Мастерская специально показывает неопределённый timeout, чтобы это различие было видно в интерфейсе.

Источник: официальные гарантии доставки.

Договор событий: что отправлять и как проверять

Имена событий стоит выбирать так, чтобы была понятна роль сообщения. Команда message:send просит выполнить действие, а событие state описывает уже полученное состояние. Для каждого типа зафиксируйте обязательные поля, допустимые значения, ошибки и смысл успешного ответа. Иначе клиент и сервер постепенно начнут по-разному понимать один и тот же payload.

Объекты можно передавать напрямую: Socket.IO выполняет необходимую сериализацию, поэтому дополнительный JSON.stringify обычно только усложняет обработчик. Проверять содержимое всё равно нужно. Типы TypeScript помогают разработчику, но произвольный сетевой клиент способен прислать другой объект. В PHP, Node.js и любом другом окружении граница сети остаётся границей доверия.

Не отправляйте весь объект пользователя только ради имени и не включайте секреты в диагностический журнал. Достаточно полей, нужных получателю. Для больших файлов лучше отдельная загрузка по HTTP, а событием передать идентификатор и статус. Такой договор уменьшает трафик, упрощает тесты и позволяет аккуратно развивать протокол без внезапного изменения всех экранов.

Авторизация, лимиты и несколько серверов

Подключение проверяется отдельно от каждой бизнес-команды. Middleware может установить личность пользователя, но его права на конкретный проект способны измениться позже. Обработчик события должен проверить доступ к нужной записи, допустимость перехода состояния и структуру payload. Имя события не является защитой от прямого вызова.

У учебного сервера есть ограничение размера пакета, частоты событий, числа соединений и списка комнат. Они удерживают демонстрацию в понятных рамках. Но выбор имени «Анна» не подтверждает личность, а Origin/Host guard не заменяет пароль. Поэтому рецепт VPS закрывает весь сайт Basic Auth; для рабочего продукта понадобится полноценная система пользователей и прав.

При нескольких Node-процессах нужна доставка событий между ними и общее бизнес-хранилище. Для HTTP long-polling обычно требуется привязка запросов одной сессии к одному процессу. Простой Redis adapter передаёт события через Pub/Sub, но не превращает память Hub в общую базу и не поддерживает connection state recovery. Redis Streams adapter имеет другой набор возможностей — совместимость проверяют по документации выбранного адаптера.

Redis и служебные каналы должны оставаться внутри доверенной инфраструктуры. Если приложение масштабируется, продумывают также поведение при рестарте узла, устаревшие presence-записи и восстановление состояния клиента. Одна добавленная зависимость adapter не решает эти вопросы автоматически.

Источники: middleware, несколько серверов, Redis adapter.

Windows: запуск и проверка

Установите Node.js 24 LTS из официального MSI: https://nodejs.org/en/download/archive/v24. После установки откройте новый PowerShell. Распакуйте ZIP и перейдите в папку с package.json:

Set-Location C:\projects\socketio-project
node --version
npm.cmd ci
npm.cmd run build
npm.cmd test
npm.cmd start

Откройте http://127.0.0.1:8104/. В другом окне PowerShell:

Invoke-RestMethod http://127.0.0.1:8104/health
Invoke-RestMethod http://127.0.0.1:8104/config.json

Ожидаются ok:true, mode:socket.io, версия 4.8.4 и mode:real во втором ответе. Выберите чат, отправьте сообщение от Анны и увидите его у Бориса. Затем откройте вторую вкладку этого адреса. Ctrl+C останавливает сервер. Для другого порта задайте обе переменные до запуска:

$env:PORT='8105'
$env:PUBLIC_ORIGIN='http://127.0.0.1:8105'
npm.cmd start

Используйте точный адрес из PUBLIC_ORIGIN. localhost и 127.0.0.1 — разные Host; смешивание приведёт к403. Не открывайте HTML через file://. Тестовый транспорт использует 18104; он должен быть свободен во время npm test.

Linux: локальная работа

Для Ubuntu/Debian приложен установщик официального Node.js 24.22.2 с проверкой SHA256, x64/arm64. Исправный Node 24 в /opt/node24 используется повторно, посторонний каталог не перезаписывается.

cd ~/projects/socketio-project
sudo bash deploy/install-node24.sh
export PATH="/opt/node24/bin:$PATH"
node --version
npm ci
npm run build
npm test
npm start

Во втором терминале:

curl --fail http://127.0.0.1:8104/health
curl --fail http://127.0.0.1:8104/config.json

Откройте http://127.0.0.1:8104/. Для другого порта: PORT=8105 PUBLIC_ORIGIN=http://127.0.0.1:8105 npm start. Нормальное завершение Ctrl+C закрывает Socket.IO и очищает таймеры; состояния в БД у демки нет.

Docker: альтернативный локальный запуск

Нужен Docker Engine с Compose plugin на Linux либо Docker Desktop в режиме Linux containers на Windows. Если native-сервер уже запущен на 8104, остановите его. Compose публикует порт только на 127.0.0.1, внутри контейнера Node слушает 0.0.0.0.

PowerShell из каталога проекта:

docker compose config
docker compose up -d --build
docker compose ps
Invoke-RestMethod http://127.0.0.1:8104/health
docker compose logs --tail 50 app
docker compose down

Bash:

docker compose config
docker compose up -d --build
docker compose ps
curl --fail http://127.0.0.1:8104/health
docker compose logs --tail 50 app
docker compose down

Контейнер работает от node, с read-only rootfs, временным /tmp, init и restart-policy. Healthcheck отправляет правильный Host из PUBLIC_ORIGIN. Память контейнера ограничена 256 MiB для этой небольшой демки; это не оценка ресурсов любого Socket.IO-проекта. Данных в volume нет: остановка/пересоздание теряет учебное состояние.

Ubuntu 24.04 VPS: установка проекта

Нужны SSH с sudo, домен, A/AAAA-записи на сервер и доступность TCP 80/443. Node-порт 8104 снаружи не открывайте. Публичная точка входа — только Caddy. Если на сервере уже есть сайты, сохраните их конфигурацию и добавьте новый блок домена вместо полной замены.

sudo apt-get update
sudo apt-get install -y curl unzip ca-certificates
mkdir -p "$HOME/socketio-build"
cd "$HOME/socketio-build"
curl -fL -o socketio-project.zip \
  https://ae-nekrasov.ru/wp-content/plugins/aen-socketio-workshop/socketio-project.zip
unzip socketio-project.zip
cd socketio-project
sudo bash deploy/install-node24.sh
sudo useradd --system --user-group --create-home --home-dir /var/lib/socketio-demo \
  --shell /usr/sbin/nologin socketiodemo
RELEASE="$(date -u +%Y%m%dT%H%M%SZ)"
RELEASE_DIR="/opt/socketio-demo/releases/$RELEASE"
sudo install -d -o socketiodemo -g socketiodemo "$RELEASE_DIR"
sudo cp -a . "$RELEASE_DIR/"
sudo chown -R socketiodemo:socketiodemo "$RELEASE_DIR"
sudo -H -u socketiodemo env PATH=/opt/node24/bin:/usr/bin:/bin \
  npm --prefix "$RELEASE_DIR" ci
sudo -H -u socketiodemo env PATH=/opt/node24/bin:/usr/bin:/bin \
  npm --prefix "$RELEASE_DIR" run build
sudo -H -u socketiodemo env PATH=/opt/node24/bin:/usr/bin:/bin \
  npm --prefix "$RELEASE_DIR" test
sudo chown -R root:root "$RELEASE_DIR"
sudo ln -sfn "$RELEASE_DIR" /opt/socketio-demo/current

Команда useradd выполняется один раз. При повторной установке проверьте существующего служебного пользователя, не создавайте дубль. npm работает от отдельного пользователя, не от root; после сборки файлы передаются root и служба только читает их. Тесты не открывают настоящий браузер, но создают реальные Socket.IO-клиенты Node.js.

Переменные и автозапуск systemd

sudo install -o root -g root -m 600 deploy/socketio-demo.env.example /etc/socketio-demo.env
sudo nano /etc/socketio-demo.env

Установите PUBLIC_ORIGIN=https://realtime.example.com, заменив домен своим. Не добавляйте путь, query string или fragment. Оставьте HOST=127.0.0.1 и PORT=8104. Файл600 читает systemd до запуска процесса от socketiodemo; сам пользователь не обязан иметь доступ к файлу окружения.

sudo install -m 644 deploy/socketio-demo.service /etc/systemd/system/socketio-demo.service
sudo systemctl daemon-reload
sudo systemctl enable --now socketio-demo
sudo systemctl status socketio-demo --no-pager
curl --fail -H 'Host: realtime.example.com' http://127.0.0.1:8104/health
sudo journalctl -u socketio-demo -n 50 --no-pager

Здесь Host должен совпасть с доменом из env. Если указали нестандартный порт в origin, включите его и в Host. После настройки публичного origin обычный curl к 127.0.0.1 без этого заголовка получает 403 — это работа guard, а не признак падения Node. Служба перезапускается после аварии и запускается после перезагрузки ОС.

HTTPS и обязательный пароль перед демкой

Установщик использует официальный stable apt-репозиторий Caddy. На сервере с существующим Caddy сначала изучите его конфигурацию. Хеш пароля генерируется интерактивно, открытый пароль в конфигурацию не записывается.

sudo bash deploy/install-caddy.sh
sudo caddy hash-password
sudo cp -a /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-socketio
sudo install -m 644 deploy/Caddyfile.example /etc/caddy/Caddyfile
sudo nano /etc/caddy/Caddyfile

Замените домен и REPLACE_WITH_GENERATED_PASSWORD_HASH. Если здесь уже были сайты, объедините блоки, не удаляя их. Basic Auth должна охватывать страницу, /health и /socket.io/, как в приложенном файле. Caddy сохраняет исходный Host при proxy к HTTP upstream и поддерживает WebSocket upgrade. Не срезайте префикс /socket.io/ через handle_path.

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl enable --now caddy
sudo systemctl reload caddy
curl -I https://realtime.example.com/health
curl --fail --user demo https://realtime.example.com/health
sudo journalctl -u caddy -n 50 --no-pager

Без пароля ожидается 401, с правильным паролем 200 и JSON health. curl с --user demo попросит пароль. Откройте домен в браузере, войдите и проверьте сообщения в двух вкладках, комнаты, задержанный ack и переподключение. Сертификат появится только при правильных DNS/портах; успешная локальная проверка конфигурации не означает, что ACME уже прошёл.

На Windows проверка VPS доступна из PowerShell:

curl.exe -I https://realtime.example.com/health
curl.exe --fail --user demo https://realtime.example.com/health

Если используется Nginx, нужны HTTP/1.1, Upgrade/Connection и сохранение Host; proxy_read_timeout должен быть больше pingInterval+pingTimeout. Конфигурацию Caddy из этого проекта не копируют в Nginx. Подробности: https://socket.io/docs/v4/reverse-proxy/.

Проверка настоящего события

После npm test выполните в UI практическую проверку: Анна и Борис в sales, сообщение видно обоим; Борис в support — новое уведомление sales ему не приходит. Верните Бориса и отключите его, отправьте сообщение от Анны, подключите Бориса: снимок показывает актуальное состояние. Наконец, отправьте команду с задержкойack и повторите тот же ID: сообщение должно остаться одним.

HTTP 200 на /health проверяет только доступность процесса. Он не доказывает прохождение WebSocket через все прокси и не доказывает корректность прикладных событий. Реальные тесты Node-клиентов покрывают и polling, и WebSocket локально; на конкретном VPS нужны отдельные проверки через его домен.

Обновление, откат и резервные копии

Не меняйте зависимости прямо в работающем каталоге. Подготовьте новую распакованную версию в отдельной папке, затем создайте новый release. Следующие команды выполняются из её каталога с package.json:

PREVIOUS="$(readlink -f /opt/socketio-demo/current)"
RELEASE="$(date -u +%Y%m%dT%H%M%SZ)"
RELEASE_DIR="/opt/socketio-demo/releases/$RELEASE"
sudo install -d -o socketiodemo -g socketiodemo "$RELEASE_DIR"
sudo cp -a . "$RELEASE_DIR/"
sudo chown -R socketiodemo:socketiodemo "$RELEASE_DIR"
sudo -H -u socketiodemo env PATH=/opt/node24/bin:/usr/bin:/bin \
  npm --prefix "$RELEASE_DIR" ci
sudo -H -u socketiodemo env PATH=/opt/node24/bin:/usr/bin:/bin \
  npm --prefix "$RELEASE_DIR" run build
sudo -H -u socketiodemo env PATH=/opt/node24/bin:/usr/bin:/bin \
  npm --prefix "$RELEASE_DIR" test
sudo chown -R root:root "$RELEASE_DIR"
sudo systemctl stop socketio-demo
sudo ln -sfn "$RELEASE_DIR" /opt/socketio-demo/next
sudo mv -Tf /opt/socketio-demo/next /opt/socketio-demo/current
sudo systemctl start socketio-demo
curl --fail -H 'Host: realtime.example.com' http://127.0.0.1:8104/health
printf 'Previous release: %s\n' "$PREVIOUS"

Сохраните напечатанный путь. При остановке клиенты отключаются, незавершённый демо-отчёт прекращается, память очищается. После запуска UI должен получить новый снимок. Zero-downtime обновление этот рецепт не обещает.

Для отката подставьте реальный сохранённый каталог:

PREVIOUS=/opt/socketio-demo/releases/20261107T120000Z
sudo test -d "$PREVIOUS"
sudo systemctl stop socketio-demo
sudo ln -sfn "$PREVIOUS" /opt/socketio-demo/rollback
sudo mv -Tf /opt/socketio-demo/rollback /opt/socketio-demo/current
sudo systemctl start socketio-demo
sudo journalctl -u socketio-demo -n 50 --no-pager

Резервируйте исходники, lockfile, env, unit и Caddyfile в закрытый архив; env может позже содержать секреты. Данные комнат в оперативной памяти не являются резервной копией: экспорт JSON-журнала в браузере — учебное свидетельство, не журнал восстановления сервера. Для рабочих сообщений нужны постоянное хранилище, схема резервирования и проверяемое восстановление БД.

Как развивать пример в своём приложении

Выберите событие, которое помогает пользователю: новое сообщение, изменение статуса или готовый отчёт. Опишите payload, права отправителя, получателей и смысл подтверждения. Затем решите, как клиент восстановит состояние после пропуска. Только после этого подключайте визуальные эффекты и уведомления.

Удобно оставить HTTP API для первоначальных данных и крупных ресурсов, а Socket.IO использовать для событий и коротких команд. Храните важные изменения в БД, выполняйте долгие работы в очереди, выводите текст безопасно и явно показывайте отсутствие связи. Такой раздел обязанностей помогает получить интерфейс, которому можно доверять, даже когда сеть ведёт себя нестабильно.

Документация и полезные материалы