WebSocket: как работает реальное время в вебе

WebSockets: протокол двусторонней связи в реальном времени между браузером и сервером - изометрическая иллюстрация с потоком данных

Привет! Когда вы общаетесь в мессенджере, торгуете на бирже или смотрите, как меняется счетчик просмотров на стриме, за всем этим стоит одна технология - мгновенный обмен данными между браузером и сервером. HTTP-протокол для этого не подходит: каждый запрос требует нового соединения, а сервер не может сам “протолкнуть” данные клиенту. Тут и появляются WebSockets.

В этой статье разберем, как работает протокол WebSocket, чем он отличается от HTTP и как реализовать real-time обмен данными на Node.js. Если вы пропустили нашу прошлую статью про PostgreSQL или MySQL: какую СУБД выбрать?, рекомендую заглянуть.

Что такое WebSocket

WebSocket - это протокол полнодуплексной связи поверх TCP, описанный в RFC 6455. Он был стандартизирован в декабре 2011 года и с тех пор стал основным инструментом для real-time взаимодействия в браузере. Протокол позволяет серверу отправлять данные клиенту без запроса со стороны клиента, и наоборот - через одно постоянное TCP-соединение.

Главная проблема, которую решает WebSocket: в обычном HTTP сервер не может инициировать отправку данных. Чтобы получить обновления, клиенту приходится постоянно опрашивать сервер (polling), открывать длинные соединения (long polling) или использовать Server-Sent Events. Каждый из этих подходов добавляет задержку и увеличивает нагрузку.

WebSocket дает один канал, через который обе стороны могут отправлять сообщения в любой момент. Никаких повторных запросов, никаких лишних заголовков.

Где это используется

  • Чаты и мессенджеры
  • Онлайн-игры с множеством игроков
  • Стриминг котировок на биржах
  • Совместное редактирование документов
  • Уведомления в реальном времени
  • Live-обновления дашбордов и аналитики

HTTP vs WebSocket: в чем разница

Обычный HTTP работает по модели “запрос-ответ”. Клиент отправляет запрос, сервер отвечает, соединение закрывается. Если клиенту нужны обновления, ему нужно отправлять новый запрос.

WebSocket начинается с обычного HTTP-запроса. Клиент обращается к серверу с просьбой “переключить” протокол (Upgrade). Если сервер согласен, соединение превращается в постоянный двусторонний канал.

websockets-vs-http.webp

Вот ключевые отличия:

  • Соединение: HTTP создает новое соединение для каждого запроса (или переиспользует через keep-alive с ограничениями). WebSocket держит одно соединение открытым все время.
  • Направление: HTTP - только клиент к серверу. WebSocket - оба направления одновременно.
  • Overhead: каждый HTTP-запрос несет полный набор заголовков (сотни байт и больше). После handshake WebSocket отправляет только минимальный фрейм (2-10 байт заголовка).
  • Формат данных: HTTP работает с текстом. WebSocket поддерживает текст (UTF-8) и бинарные данные.

Когда WebSocket не нужен

Если вам достаточно односторонней отправки данных от сервера к клиенту (уведомления, логи), подойдут Server-Sent Events (SSE). Они проще в реализации и не требуют специального протокола. А если данные обновляются раз в минуту-пять, обычного polling с setInterval хватит за глаза.

Как работает протокол

Процесс установки WebSocket-соединения состоит из двух фаз: handshake (рукопожатие) и обмен данными через фреймы.

Handshake

how-to-work-websockets-handshake.webp

Все начинается с обычного HTTP GET-запроса, в котором клиент просит сервер “апгрейднуть” соединение:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Ключевые заголовки:

  • Upgrade: websocket - говорит серверу, что клиент хочет переключиться на WebSocket
  • Connection: Upgrade - подтверждает намерение сменить протокол
  • Sec-WebSocket-Key - случайный nonce в base64, нужен серверу для подтверждения handshake
  • Sec-WebSocket-Version: 13 - версия протокола (13 - текущая и единственная актуальная)

Сервер отвечает статусом 101 Switching Protocols:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Заголовок Sec-WebSocket-Accept вычисляется так: берется значение Sec-WebSocket-Key, к нему добавляется GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, от результата берется SHA-1 хеш и кодируется в base64. Это гарантирует, что сервер действительно понял запрос и поддерживает WebSocket, а не просто вернул HTML-страницу.

После этого handshake завершен - HTTP больше не участвует, и начинается обмен фреймами.

Формат фреймов

Данные передаются в виде фреймов - небольших пакетов с заголовком и полезной нагрузкой. Каждый фрейм содержит:

  • FIN (1 бит) - является ли этот фрейм последним в сообщении
  • Opcode (4 бита) - тип данных: 0x1 для текста, 0x2 для бинарных данных, 0x8 для закрытия, 0x9 для ping, 0xA для pong
  • MASK (1 бит) - замаскированы ли данные (клиент обязан маскировать свои сообщения)
  • Payload length (7 бит или больше) - длина полезной нагрузки
  • Masking key (32 бита) - ключ для демаскировки (только от клиента к серверу)
  • Payload data - сами данные

Маскирование данных от клиента к серверу - это мера безопасности, которая предотвращает атаки подмены данных на прокси-серверах. Сервер отправляет данные без маски.

Ping/Pong: проверка жизни соединения

how-to-work-websockets-ping-pong.webp

В любой момент после handshake любая сторона может отправить ping-фрейм (opcode 0x9). Получатель обязан ответить pong-фреймом (opcode 0xA). Это heartbeat-механизм: если pong не приходит, соединение считается разорванным.

Реализация на Node.js: библиотека ws

Самый популярный и легковесный вариант для Node.js - библиотека ws. Она имеет более 22 тысяч звезд на GitHub, стабильна и быстра. Для простых задач это лучший выбор.

Установка и простой сервер

npm install ws

Создаем echo-сервер, который отправляет обратно все полученные сообщения:

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('error', console.error);

  ws.on('message', function message(data) {
    console.log('received: %s', data);
    ws.send('Echo: ' + data);
  });

  ws.send('Connected to WebSocket server');
});

console.log('WebSocket server started on ws://localhost:8080');

Разберем, что тут происходит. Мы создаем WebSocket-сервер на порту 8080. При подключении нового клиента срабатывает событие connection, и мы получаем объект ws - это отдельное соединение с клиентом. Через ws.on('message', ...) слушаем входящие сообщения, а через ws.send() отправляем данные обратно.

Broadcast: отправка всем клиентам

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

import WebSocket, { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('error', console.error);

  ws.on('message', function message(data, isBinary) {
    wss.clients.forEach(function each(client) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data, { binary: isBinary });
      }
    });
  });
});

Здесь wss.clients - это Set всех подключенных клиентов. Мы перебираем их и отправляем сообщение каждому, кроме отправителя. Проверка readyState === WebSocket.OPEN гарантирует, что мы не пытаемся отправить данные в уже закрытое соединение.

Heartbeat: обнаружение разрывов

Соединение может оборваться “тихо” - без явного закрытия. Например, когда пользователь закрывает ноутбук или пропадает Wi-Fi. Чтобы обнаружить такие ситуации, используем ping/pong:

import { WebSocketServer } from 'ws';

function heartbeat() {
  this.isAlive = true;
}

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.isAlive = true;

  ws.on('error', console.error);

  ws.on('pong', heartbeat);
});

const interval = setInterval(function ping() {
  wss.clients.forEach(function each(ws) {
    if (ws.isAlive === false) return ws.terminate();

    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', function close() {
  clearInterval(interval);
});

Каждые 30 секунд сервер отправляет ping всем клиентам. Если клиент не ответил pong, помечаем его как неактивного. На следующей проверке, если pong так и не пришел, соединение принудительно закрывается через terminate().

Клиентская часть в браузере

Браузерный API WebSocket предельно прост. Подключаемся, слушаем события, отправляем данные:

<!DOCTYPE html>
<html>
<head>
  <title>WebSocket Chat</title>
</head>
<body>
  <ul id="messages"></ul>
  <form id="form">
    <input id="input" autocomplete="off" />
    <button>Отправить</button>
  </form>

  <script>
    const socket = new WebSocket('ws://localhost:8080');

    socket.addEventListener('open', function() {
      console.log('Connected to server');
    });

    socket.addEventListener('message', function(event) {
      const item = document.createElement('li');
      item.textContent = event.data;
      document.getElementById('messages').appendChild(item);
    });

    socket.addEventListener('close', function() {
      console.log('Disconnected from server');
    });

    document.getElementById('form').addEventListener('submit', function(e) {
      e.preventDefault();

      const input = document.getElementById('input');

      if (input.value) {
        socket.send(input.value);
        input.value = '';
      }
    });
  </script>
</body>
</html>

Браузерная реализация поддерживается всеми современными браузерами с 2015 года. Схемы подключения: ws:// для обычного соединения и wss:// для зашифрованного (аналог http/https). В продакшене всегда используйте wss://.

Socket.IO: продвинутая альтернатива

how-to-work-socket-io.webp

Библиотека Socket.IO работает поверх WebSocket, но добавляет несколько важных возможностей:

  • Автоматическое переподключение при разрыве связи
  • Fallback на long-polling, если WebSocket недоступен
  • Rooms - группировка клиентов для targeted-рассылки
  • Namespaces - разделение логических каналов в одном соединении
  • Подтверждение доставки сообщений (acknowledgements)

Пример сервера с rooms

const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server);

io.on('connection', (socket) => {
  console.log('User connected:', socket.id);

  socket.on('join-room', (room) => {
    socket.join(room);
    socket.to(room).emit('user-joined', socket.id);
  });

  socket.on('chat message', (room, msg) => {
    io.to(room).emit('chat message', msg);
  });

  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

server.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Rooms позволяют легко организовать групповой чат, где сообщения отправляются только участникам конкретной комнаты. Метод socket.join(room) добавляет клиента в комнату, io.to(room).emit() отправляет сообщение всем участникам.

Клиент Socket.IO

<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io();

  socket.on('connect', () => {
    socket.emit('join-room', 'general');
  });

  socket.on('chat message', (msg) => {
    console.log('Message:', msg);
  });

  socket.on('user-joined', (userId) => {
    console.log('User joined:', userId);
  });
</script>

ws или Socket.IO?

Выбор зависит от задачи:

  • ws - если нужен чистый WebSocket без лишних абстракций, важна производительность и минимальный overhead. Подходит для внутренних сервисов, игр, стриминга.
  • Socket.IO - если нужны rooms, auto-reconnect, broadcasting и fallback-механизмы. Удобен для чатов, коллаборативных инструментов, приложений с нестабильным соединением.

Практические советы

Аутентификация при подключении

WebSocket не поддерживает кастомные заголовки из браузера. Токены авторизации обычно передают через query-параметр или в рамках протокола (подпротокол):

// Клиент
const socket = new WebSocket('ws://localhost:8080?token=your-jwt-token');

// Сервер (ws)
wss.on('connection', function connection(ws, req) {
  const token = new URL(req.url, 'ws://localhost').searchParams.get('token');
  // Проверяем токен...
});

Масштабирование

Один WebSocket-сервер не справится с тысячами одновременных подключений. Для горизонтального масштабирования используют sticky sessions (привязка клиента к конкретному серверу через load balancer) и Redis adapter для синхронизации сообщений между инстансами. Об этом мы подробно поговорим в будущей статье про WebSockets масштабирование.

Безопасность

  • Всегда используйте wss:// в продакшене
  • Проверяйте Origin заголовок при handshake, чтобы избежать CSRF-атак
  • Ограничивайте размер сообщений, чтобы защититься от DoS
  • Валидируйте данные на сервере - никогда не доверяйте клиенту

Заключение

WebSocket - это не замена HTTP, а дополнение к нему. HTTP отлично справляется с запросами документов и API, а WebSocket берет на себя задачи, где нужна мгновенная двусторонняя связь. Протокол прост в понимании, а библиотеки вроде ws и Socket.IO делают реализацию еще проще.

Если вашему приложению нужны real-time обновления - чат, уведомления, live-данные - WebSocket будет правильным выбором. А если данные обновляются редко и направление только сервер-клиент, посмотрите в сторону SSE или обычного polling. Далее оставляю диаграмму по которой проще будет определить какую технологию выбрать для приложения.

summary-network-selection-diagram.webp

📩 Нужна помощь с разработкой? Напишите нам - мы поможем реализовать ваш проект.

Предыдущая статья
PostgreSQL vs MySQLPostgreSQL или MySQL: какую СУБД выбрать?
Нет комментариев
Ваш комментарий
Ваш адрес email не будет опубликован.
Обязательные поля помечены *
0
24 апр. 2026