Живой чат, экран заказов на кухне, курсы валют или онлайн-игра должны показывать данные сразу, без перезагрузки страницы. Для этого часто выбирают WebSocket. Технология создаёт между браузером и сервером двусторонний канал, который открывается один раз и долго остаётся открытым. Проблема в том, что WebSocket-приложение, отлично работавшее на компьютере разработчика, после загрузки на обычный хостинг нередко вообще не подключается. Разберём, почему так происходит и где и как такое приложение правильно запускать.
Чем WebSocket отличается от обычного HTTP-запроса
Обычный сайт работает по принципу «запрос — ответ»: браузер запрашивает страницу, сервер выполняет PHP-скрипт, возвращает ответ, и соединение закрывается. WebSocket тоже начинается с HTTP-запроса, но в нём есть заголовок Upgrade: websocket. Если сервер согласен, он отвечает 101 Switching Protocols, после чего то же TCP-соединение остаётся открытым. Теперь и сервер, и браузер могут в любой момент отправлять друг другу сообщения.
Значит, для WebSocket на сервере нужен постоянно работающий процесс, который держит соединения. Обычно это отдельная программа на Node.js, Python или Go: она слушает свой порт и хранит в памяти тысячи открытых соединений. Именно это требование и сталкивается с устройством обычного хостинга.
Почему WebSocket не работает на обычном хостинге
На обычном (виртуальном) хостинге один сервер разделён между множеством клиентов. PHP-скрипты запускаются при поступлении запроса и должны завершиться за несколько секунд, время выполнения ограничено. Открыть собственный порт и принимать на него внешние подключения клиенту, как правило, нельзя, а постоянно работающие фоновые процессы могут останавливаться из-за ограничений ресурсов. Настройки веб-сервера также не в руках клиента, поэтому добавить правило проксирования, передающее заголовок Upgrade внутреннему приложению, не получится.
По этой же причине и PHP-библиотеки для WebSocket (например, Ratchet) нестабильны на обычном хостинге: им тоже нужны постоянный процесс и открытый порт. Если проект обязан остаться на обычном хостинге, работу «почти в реальном времени» придётся строить иначе.
Если нужно остаться на обычном хостинге
Во многих случаях для «живого» поведения WebSocket на самом деле не нужен. Например, для изменений в расписании на школьном сайте или статуса заказа в интернет-магазине достаточно, чтобы браузер раз в 5–10 секунд отправлял небольшой JSON-запрос. Такой периодический опрос (polling) работает на обычном хостинге без проблем, потому что каждый запрос короткий и выполняется как обычный PHP-скрипт.
Есть ещё long polling и Server-Sent Events, но они надолго занимают PHP-процесс для каждого пользователя. Если одновременно подключатся десятки человек, лимит процессов на хостинге быстро исчерпается, и сайт перестанет отвечать на другие запросы. Поэтому на обычном хостинге самый безопасный вариант — короткий периодический опрос, а полноценный WebSocket запускается на отдельном сервере.
Пример: экран заказов для кухни ресторана
Ресторан в Ташкенте хочет, чтобы официанты вводили заказ на планшете, а он сразу появлялся на экране на кухне. Даже задержка в пару секунд неудобна поварам, поэтому здесь WebSocket уместен. Основной сайт и меню могут оставаться на обычном хостинге, а WebSocket-сервер работает на небольшом VPS. Ниже минимальный сервер на Node.js с библиотекой ws: он рассылает каждый новый заказ всем подключённым экранам и убирает оборвавшиеся соединения.
// server.js (npm install ws)
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ host: '127.0.0.1', port: 8080 });
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
ws.on('message', (data) => {
// разослать новый заказ всем подключённым экранам
for (const client of wss.clients) {
if (client.readyState === 1) client.send(data.toString());
}
});
});
// проверка «жив ли клиент» раз в 30 секунд
setInterval(() => {
for (const ws of wss.clients) {
if (!ws.isAlive) { ws.terminate(); continue; }
ws.isAlive = false;
ws.ping();
}
}, 30000);
Обратите внимание: сервер слушает только адрес 127.0.0.1, то есть напрямую снаружи к нему не подключиться. Внешние соединения принимает Nginx, шифрует их и передаёт внутрь. Так и безопаснее, и сертификатом можно управлять в одном месте.
Настройка wss:// через Nginx
Если сайт открывается по HTTPS, браузер заблокирует незашифрованное соединение ws:// как смешанный контент, поэтому нужен wss://. Удобнее всего держать сертификат Let's Encrypt на Nginx, а запросы к адресу /ws/ проксировать на внутренний сервер Node.js. Ключевой момент — передать заголовки Upgrade и Connection и включить HTTP/1.1, иначе Nginx воспримет соединение как обычный запрос.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
server_name misol.uz;
# ssl_certificate ... (сертификат Let's Encrypt)
location /ws/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
}
Важно и значение proxy_read_timeout. По умолчанию Nginx закрывает соединение, если 60 секунд не было данных. Пинг раз в 30 секунд в сервере выше создаёт трафик раньше этого срока, а увеличенный тайм-аут даёт дополнительный запас. В браузере соединение открывается так:
const sock = new WebSocket('wss://misol.uz/ws/');
sock.onmessage = (e) => showOrder(JSON.parse(e.data));
sock.onclose = () => setTimeout(() => location.reload(), 3000);
Проверка соединения и постоянная работа сервера
Проверить настройку можно ещё до браузера, с помощью curl. Если первая строка ответа — HTTP/1.1 101 Switching Protocols, то и Nginx, и Node.js работают правильно. Если пришёл 400 или 502, сначала убедитесь, что процесс Node.js запущен, а затем проверьте заголовки в конфигурации Nginx.
curl -i -N \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
https://misol.uz/ws/
Не оставляйте сервер Node.js запущенным вручную в терминале: при закрытии SSH-сессии он может остановиться. Оформите его как службу systemd, тогда он поднимется автоматически и после перезагрузки сервера, и после падения программы.
# /etc/systemd/system/ws-kitchen.service
[Service]
WorkingDirectory=/opt/ws-kitchen
ExecStart=/usr/bin/node server.js
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now ws-kitchen
sudo nginx -t && sudo systemctl reload nginx
Несколько процессов и масштабирование
Одного процесса Node.js хватает для приложения со средней нагрузкой. Если позже вы запустите несколько процессов или серверов, клиент, подключённый к одному процессу, не увидит сообщение, пришедшее в другой. Тогда процессам нужно обмениваться сообщениями через общую очередь, и чаще всего эту роль выполняет механизм pub/sub в Redis. В небольшом проекте с этим не стоит спешить: сначала добейтесь стабильной работы одного процесса.
Итак, WebSocket требует постоянного процесса, открытого порта и доступа к настройкам веб-сервера — всё это есть на VPS. Заказав VPS в кабинете sayt.uz, можно оставить основной сайт на хостинге и вынести на отдельный сервер только часть, работающую в реальном времени.