WebSocket на сервере: базовая настройка на примере чата
Разбираем, чем WebSocket отличается от обычных HTTP-запросов, поднимаем минимальный сервер на Node.js и настраиваем Nginx так, чтобы соединение действительно устанавливалось, а не обрывалось молча.
Зачем нужен WebSocket, когда есть обычный HTTP
Обычный HTTP-запрос устроен по принципу «запрос — ответ»: браузер отправляет запрос, сервер обрабатывает его и возвращает ответ, после чего соединение (в старых версиях протокола) закрывается или, при использовании keep-alive, остаётся открытым для следующего аналогичного цикла — но инициатором каждого нового обмена данными всегда выступает клиент, сервер не может сам отправить браузеру что-либо без предварительного запроса.
Для многих задач такая модель избыточна или недостаточна — например, для получения новых сообщений в чате без её ограничений пришлось бы либо постоянно опрашивать сервер запросами «есть ли новое сообщение?» (polling), тратя ресурсы на подавляющее большинство пустых ответов «нет», либо мириться с задержкой между реальным появлением сообщения и его получением клиентом.
WebSocket решает эту проблему принципиально другим устройством соединения: после первоначального «рукопожатия» через обычный HTTP-запрос соединение повышается (upgrade) до постоянного двустороннего канала, который остаётся открытым сколь угодно долго, и через него обе стороны — и сервер, и клиент — могут отправлять сообщения друг другу в любой момент, без необходимости заново инициировать запрос на каждое сообщение.
WebSocket не заменяет HTTP полностью, а решает конкретный класс задач, где нужна низкая задержка и инициатива со стороны сервера, — обычные страницы сайта, формы и большая часть API по-прежнему прекрасно работают на классическом HTTP.
Практический принцип выбора протокола для конкретной задачи
Типичные сценарии применения — чаты и мессенджеры, live-уведомления, совместное редактирование документов в реальном времени, живые обновления котировок или показателей на дашборде, многопользовательские онлайн-игры. Эта статья разбирает базовую настройку такого сервера на примере простого чата: минимальный сервер на Node.js, конфигурацию Nginx для корректного проксирования и вопросы масштабирования на несколько серверов приложения.
Минимальный WebSocket-сервер на Node.js
Для практического примера удобнее всего пакет ws — минималистичная и широко используемая библиотека для Node.js, реализующая протокол WebSocket без лишней обвязки, идеально подходящая для понимания базового механизма без отвлечения на возможности более крупных фреймворков.
mkdir chat-server && cd chat-server
npm init -y
npm install ws
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 3001 });
const clients = new Set();
wss.on('connection', (ws) => {
clients.add(ws);
console.log('Новое подключение, всего клиентов:', clients.size);
ws.on('message', (data) => {
const message = data.toString();
for (const client of clients) {
if (client.readyState === client.OPEN) {
client.send(message);
}
}
});
ws.on('close', () => {
clients.delete(ws);
});
});
Этот код умещается в один файл и уже реализует рабочий групповой чат: сервер держит множество открытых соединений в переменной clients и при получении сообщения от одного клиента рассылает его всем остальным. Проверка client.readyState === client.OPEN важна, поскольку соединение могло уже начать закрываться на момент рассылки, и попытка отправить данные в закрывающийся сокет приведёт к ошибке.
Для production такой минимальный пример стоит дополнить обработкой ошибок, ограничением размера сообщений и, для реального чата, привязкой сообщений к конкретным комнатам или пользователям, а не к слепой рассылке всем подряд — но принцип постоянного открытого соединения остаётся тем же самым.
Запуск процесса приложения на сервере
Node.js-процесс, в отличие от PHP-FPM, не управляется автоматически веб-сервером — его нужно запустить и держать работающим отдельно, желательно под управлением process-менеджера, который автоматически перезапустит процесс при аварийном завершении и позволит просматривать его лог.
npm install -g pm2
pm2 start server.js --name chat-server
pm2 save
pm2 startup
Аналогичного результата можно добиться и через systemd-юнит с директивой Restart=on-failure — выбор между pm2 и systemd скорее вопрос личных предпочтений и привычной инфраструктуры, чем принципиальной разницы в надёжности для несложного проекта на одном сервере.
Проксирование WebSocket через Nginx: заголовки Upgrade и Connection
Приложение на Node.js в примере выше слушает внутренний порт 3001, недоступный напрямую снаружи, — как и с обычным HTTP-приложением, перед ним ставится Nginx, обрабатывающий внешние подключения на 80 и 443 портах и передающий их на внутренний процесс. Но для WebSocket одного стандартного proxy_pass недостаточно.
Ключевая деталь — специальные заголовки Upgrade и Connection, сигнализирующие о том, что клиент хочет повысить обычное HTTP-соединение до постоянного WebSocket-канала. Без явной передачи этих заголовков в проксируемый запрос Nginx по умолчанию их не пробрасывает, и приложение видит обычный HTTP-запрос вместо запроса на upgrade — в результате соединение либо не устанавливается вовсе, либо тихо откатывается назад к обычному HTTP без явной ошибки, что особенно сбивает с толку при первой настройке.
location /ws/ {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
proxy_http_version 1.1 обязателен, поскольку механизм upgrade соединения — часть протокола HTTP/1.1, а версия по умолчанию в директиве proxy_pass — HTTP/1.0, не поддерживающая эту возможность в принципе. Значение заголовка Connection берётся именно из переменной $http_upgrade, а не задаётся статической строкой "upgrade" напрямую, чтобы Nginx корректно обрабатывал и обычные, не WebSocket-запросы к тому же location, не ломая их.
Долгоживущее соединение позади прокси: proxy_read_timeout
Стандартный таймаут ожидания ответа от бэкенда в Nginx (обычно 60 секунд) рассчитан на обычные HTTP-запросы, которые получают ответ быстро. WebSocket-соединение по своей природе устроено иначе — оно может оставаться открытым часами без активной передачи данных, ожидая, когда появится новое сообщение для отправки.
Без явного увеличения этого таймаута Nginx будет расценивать долгую паузу в передаче данных как признак зависшего соединения и принудительно закроет его — с точки зрения пользователя это будет выглядеть как случайные разрывы чата спустя ровно минуту бездействия, что типичная и легко узнаваемая по этому признаку проблема при первой настройке WebSocket за прокси.
location /ws/ {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
Дополнительная практическая мера — реализовать в самом приложении механизм ping/pong (встроенный в протокол WebSocket) с периодом заметно короче установленного таймаута Nginx: сервер или клиент периодически отправляют служебное ping-сообщение, поддерживая соединение активным и одновременно позволяя быстро обнаружить действительно оборванное соединение, а не ждать истечения полного таймаута для этого.
HTTPS и защищённый WebSocket: wss://
Если основной сайт работает по HTTPS (а на актуальном проекте это должно быть правилом, а не исключением), WebSocket-соединение к нему тоже должно устанавливаться через защищённую версию протокола — wss:// вместо обычного ws://, иначе браузер заблокирует смешанный контент, не позволяя незащищённому WebSocket-соединению работать со страницы, загруженной по HTTPS.
Отдельный сертификат для WebSocket не требуется — TLS терминируется на том же самом уровне 443 порта Nginx, что и для обычного HTTPS-трафика того же домена, с тем же сертификатом. Разница лишь в клиентском коде: адрес соединения указывается с протоколом wss:// вместо ws://, а на стороне Nginx конфигурация location для WebSocket добавляется в тот же server-блок, что уже обслуживает основной HTTPS-сайт.
const ws = new WebSocket('wss://example.ru/ws/');
Масштабирование: что происходит с несколькими серверами приложения
Минимальный сервер из примера выше прекрасно работает, пока все клиенты подключены к одному-единственному процессу Node.js на одном сервере, — переменная clients в памяти этого процесса видит их всех и может разослать сообщение каждому.
Проблема начинается, когда для отказоустойчивости или под растущую нагрузку приложение запускается в нескольких экземплярах, за балансировщиком нагрузки, распределяющим клиентов между разными серверами (или разными процессами на одном сервере). Список clients в памяти каждого процесса в этой ситуации видит только тех, кто подключился именно к нему, — сообщение от пользователя на первом сервере физически не долетит до пользователя, подключённого ко второму.
Первое решение — sticky-сессии на уровне балансировщика: правило, гарантирующее, что один и тот же клиент при повторных подключениях всегда попадает на один и тот же сервер приложения. Это работает, но не решает задачу, если два участника одного чата изначально оказались привязаны к разным серверам — сообщение всё равно не попадёт от одного к другому напрямую.
Более общее решение — общий слой публикации сообщений между всеми экземплярами приложения, для чего часто используют Redis в режиме pub/sub, подробно разобранный в отдельной статье базы знаний: каждый сервер приложения при получении сообщения от своего клиента публикует его в общий канал Redis, а все серверы, включая отправивший, подписаны на этот канал и рассылают полученное сообщение своим собственным подключённым клиентам, независимо от того, на каком именно сервере физически находится получатель.
Redis pub/sub в этой роли решает конкретную задачу — синхронизацию сообщений между независимыми процессами приложения, а не хранение истории сообщений; для истории чата по-прежнему нужна обычная база данных.
Практическое разделение ответственности при масштабировании WebSocket
Практический пример архитектуры с несколькими серверами
Рассмотрим упрощённую схему для чата, растущего за пределы одного сервера: балансировщик нагрузки перед несколькими одинаковыми экземплярами Node.js-приложения, каждый из которых поддерживает собственный набор WebSocket-соединений и общий Redis-сервер для координации между ними.
При получении сообщения от своего клиента сервер публикует его в канал Redis командой вроде PUBLISH chat-room-1 "текст сообщения", а каждый сервер приложения, включая отправивший, подписан на этот канал через SUBSCRIBE и при получении публикации рассылает сообщение своим локально подключённым клиентам той же комнаты.
Такая схема разделяет две разные задачи — доставку сообщения между процессами приложения (Redis pub/sub) и доставку сообщения от процесса приложения до конкретного браузера (WebSocket-соединение) — что упрощает масштабирование каждой из них по отдельности, не переусложняя архитектуру там, где реальный масштаб проекта пока этого не требует.
Для небольшого и среднего проекта, где WebSocket-сервер стабильно помещается в один процесс на одном сервере, добавление Redis и координации между серверами — преждевременное усложнение; переходить к этой схеме имеет смысл тогда, когда реальная нагрузка или требования к отказоустойчивости этого действительно требуют, а не заранее «на всякий случай».
Типичные ошибки при настройке WebSocket на сервере
Отсутствие заголовков Upgrade и Connection в конфигурации Nginx — самая частая причина, по которой WebSocket-соединение просто не устанавливается, при том что сам Node.js-процесс работает исправно и напрямую (без Nginx) принимает соединения без проблем.
Оставленный таймаут по умолчанию для location с WebSocket — приводит к случайным обрывам соединения ровно после периода бездействия, равного значению таймаута, что легко перепутать с нестабильностью самого приложения или сети, если не знать об этой конкретной причине.
Использование wss:// на сайте, где ещё не настроен HTTPS для основного домена, — браузер заблокирует такое смешанное соединение как небезопасный контент со страницы, загруженной по обычному HTTP, что стоит проверить в первую очередь при получении ошибок подключения именно на production, при том что на локальной машине по HTTP всё работает.
Хранение состояния подключённых клиентов только в памяти одного процесса без плана масштабирования — работает ровно до момента запуска второго экземпляра приложения, после чего часть сообщений начинает необъяснимо не доходить до части пользователей, если заранее не предусмотреть механизм синхронизации между процессами.
Отсутствие переподключения на стороне клиента при разрыве соединения — сервер может перезапуститься по любой причине (деплой, авария), и без явной логики автоматического повторного подключения с задержкой пользователь останется без обновлений молча, не получив никакого уведомления о разрыве.
Чек-лист настройки WebSocket-сервера
Шаг 1. Реализуйте минимальный WebSocket-сервер на подходящей платформе (Node.js с ws — рабочий и хорошо документированный выбор для старта) и запустите его под управлением process-менеджера с автоперезапуском.
Шаг 2. Настройте отдельный location в конфигурации Nginx с обязательными заголовками Upgrade и Connection и proxy_http_version 1.1.
Шаг 3. Увеличьте proxy_read_timeout и proxy_send_timeout для этого location до значения, покрывающего реальную длительность бездействия соединений вашего приложения.
Шаг 4. Реализуйте ping/pong на уровне приложения для поддержания соединения активным и быстрого обнаружения реально оборванных подключений.
Шаг 5. Убедитесь, что клиентский код использует wss:// на HTTPS-сайте и реализует автоматическое переподключение с задержкой при разрыве соединения.
Шаг 6. При росте нагрузки за пределы одного процесса добавьте sticky-сессии на балансировщике и общий слой публикации сообщений вроде Redis pub/sub, а не откладывайте эту задачу до момента, когда проблема с недоставленными сообщениями уже проявится у реальных пользователей.
Итог: постоянное соединение требует немного другого мышления
WebSocket решает конкретный класс задач, где важна низкая задержка и возможность серверу самому инициировать отправку данных, — но требует другого подхода к инфраструктуре по сравнению с классическим HTTP: постоянно запущенный процесс приложения вместо обработки по требованию, явная настройка прокси для передачи заголовков upgrade и увеличенные таймауты для долгоживущих соединений.
Большая часть практических проблем при первой настройке WebSocket сводится к одной и той же причине — конфигурации Nginx, не пробрасывающей нужные заголовки или обрывающей соединение по таймауту раньше времени, а не к самому коду приложения. Если настройка WebSocket-сервера для конкретного проекта — чата, live-уведомлений или совместного редактирования — вызывает сложности, оставьте заявку внизу страницы, поможем настроить связку под ваш стек.
Вопрос масштабирования на несколько серверов стоит откладывать до момента реальной необходимости — для большинства проектов, начинающих с чата или live-уведомлений, одного процесса на одном сервере достаточно надолго, а преждевременное усложнение архитектуры общим слоем координации редко окупается на старте проекта.
Почему стоит выбрать нас
Быстрый запуск
Подберём конфигурацию и развернём проект в день обращения.
Надёжность
NVMe-диски, защита от DDoS и резервные копии по расписанию.
Поддержка 24/7
Дежурные инженеры на связи в любое время суток.
Настройка под ключ
Установим CMS, настроим веб-сервер, SSL и почту за вас.
Перенос без простоя
Аккуратно перенесём сайт и базу данных на новый сервер.
Прозрачные цены
Актуальные тарифы и честный подбор без навязывания.
Вопросы и ответы
Чем WebSocket принципиально отличается от обычного AJAX-запроса?
Обычный HTTP-запрос — это разовый цикл «запрос — ответ», после которого соединение закрывается; WebSocket устанавливает одно постоянное соединение, остающееся открытым, через которое обе стороны — и сервер, и браузер — могут отправлять сообщения друг другу в любой момент, без необходимости заново инициировать запрос на каждое сообщение.
Почему WebSocket-соединение не устанавливается через Nginx, хотя сам сервер работает?
Чаще всего причина в отсутствующих заголовках Upgrade и Connection в конфигурации Nginx для соответствующего location — без них Nginx обрабатывает запрос как обычный HTTP и не передаёт бэкенду сигнал о необходимости повышения протокола до WebSocket, из-за чего соединение остаётся обычным HTTP и быстро закрывается.
Нужно ли увеличивать proxy_read_timeout для WebSocket?
Да, обязательно — значение по умолчанию (обычно 60 секунд) рассчитано на быстрые обычные HTTP-запросы, тогда как WebSocket-соединение по своей природе может оставаться открытым часами без активной передачи данных; без увеличения таймаута Nginx будет закрывать соединение как «зависшее», даже если оно совершенно исправно.
Можно ли использовать WebSocket без Node.js, например на PHP?
Технически да, существуют PHP-библиотеки для WebSocket (например, Ratchet), но классический PHP-FPM, работающий как обработчик запросов по циклу «запрос — ответ», плохо подходит для постоянных долгоживущих соединений; на практике для WebSocket-сервера чаще выбирают среды, изначально спроектированные под работу с постоянными соединениями, — Node.js, Go или аналогичные.
Как масштабировать WebSocket-сервер на несколько серверов приложения?
Ключевая сложность в том, что WebSocket-соединение привязано к конкретному процессу сервера, к которому подключился клиент, — при нескольких серверах приложения нужен либо механизм sticky-сессий на балансировщике, чтобы клиент всегда попадал на тот же сервер, либо общий слой публикации сообщений (например, через Redis pub/sub), рассылающий сообщение на все серверы одновременно.
Что произойдёт с открытыми WebSocket-соединениями при перезапуске сервера приложения?
Все активные соединения будут разорваны, поскольку они привязаны к конкретному процессу, — клиентский код должен предусматривать автоматическое переподключение с задержкой при разрыве соединения, иначе пользователь просто перестанет получать новые сообщения без какого-либо видимого уведомления об этом.
Нужен ли отдельный SSL-сертификат для WebSocket (wss://)?
Нет, отдельный сертификат не нужен — защищённый WebSocket (wss://) использует ровно тот же TLS-сертификат, что и обычный HTTPS того же домена; Nginx терминирует TLS на уровне 443 порта одинаково для обоих типов трафика, различие только в протоколе после установления соединения.
Как понять, что WebSocket-соединение установилось успешно, а не откатилось до обычного HTTP?
В сетевой вкладке инструментов разработчика браузера успешное WebSocket-соединение отображается отдельной строкой с кодом ответа 101 Switching Protocols, а не обычным 200 — если вместо этого видна ошибка или обрыв соединения сразу после установления, проблема почти всегда в конфигурации прокси-сервера перед приложением.
Как оплачивать услуги?
Оплата помесячная, для облачных серверов доступна почасовая тарификация. Принимаются карты, банковские переводы и электронные кошельки.
Есть ли круглосуточная поддержка?
Да, дежурные инженеры на связи 24/7.
Выдаёте ли закрывающие документы?
Да, предоставим необходимые документы для оплаты.
Как оставить заявку?
Заполните форму на странице или нажмите «Заказать» — мы свяжемся и поможем с подбором.