WebSocket

Зачем нужно

WebSocket — протокол полнодуплексной связи поверх TCP. В отличие от HTTP (запрос → ответ), WebSocket позволяет серверу отправлять данные клиенту в любой момент без запроса. Это делает WebSocket идеальным для real-time приложений.

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

  • Чаты и мессенджеры
  • Онлайн-игры
  • Торговые платформы (котировки в реальном времени)
  • Совместное редактирование (Google Docs)
  • Уведомления в реальном времени
  • Live-мониторинг и дашборды

HTTP vs WebSocket

HTTP (полудуплекс):
Клиент → Запрос  → Сервер
Клиент ← Ответ   ← Сервер
Клиент → Запрос  → Сервер
Клиент ← Ответ   ← Сервер
(Каждый раз новый запрос)

WebSocket (полный дуплекс):
Клиент → HTTP Upgrade → Сервер    (handshake)
Клиент ↔ Данные ↔ Сервер          (постоянное соединение)
Клиент ← Сообщение ← Сервер       (сервер инициирует)
Клиент → Сообщение → Сервер       (клиент инициирует)

Жизненный цикл соединения

1. Handshake (HTTP Upgrade):
   GET /chat HTTP/1.1
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

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

2. Соединение открыто → обмен сообщениями

3. Закрытие (любая сторона может инициировать)

WebSocket API (клиент)

// === Создание соединения ===
const ws = new WebSocket('ws://localhost:3000');
// Для HTTPS: wss://example.com (шифрованное)

// === События ===

// Соединение установлено
ws.addEventListener('open', (event) => {
  console.log('Подключено!');
  ws.send('Привет, сервер!');
});

// Получено сообщение
ws.addEventListener('message', (event) => {
  console.log('Получено:', event.data);

  // Если данные в JSON
  const data = JSON.parse(event.data);
  console.log(data);
});

// Соединение закрыто
ws.addEventListener('close', (event) => {
  console.log('Отключено:', event.code, event.reason);
  // event.code — код закрытия (1000 = нормальное)
  // event.wasClean — true если закрытие чистое
});

// Ошибка
ws.addEventListener('error', (event) => {
  console.error('Ошибка WebSocket:', event);
});

// === Отправка данных ===
ws.send('Текстовое сообщение');
ws.send(JSON.stringify({ type: 'chat', text: 'Привет!' }));
ws.send(new Blob(['бинарные данные']));

// === Закрытие ===
ws.close(1000, 'Нормальное закрытие');

// === Свойства ===
ws.readyState; // 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
ws.bufferedAmount; // Байт в очереди на отправку

WebSocket сервер (Node.js)

npm install ws
const { WebSocketServer } = require('ws');

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

// Хранилище подключённых клиентов
const clients = new Set();

wss.on('connection', (ws, request) => {
  console.log('Новое подключение:', request.socket.remoteAddress);
  clients.add(ws);

  // Отправить приветствие
  ws.send(JSON.stringify({
    type: 'welcome',
    message: 'Добро пожаловать!',
    online: clients.size,
  }));

  // Получение сообщения от клиента
  ws.on('message', (data) => {
    const message = JSON.parse(data.toString());
    console.log('Получено:', message);

    // Рассылка всем (broadcast)
    for (const client of clients) {
      if (client !== ws && client.readyState === 1) {
        client.send(JSON.stringify({
          type: 'chat',
          from: message.from,
          text: message.text(),
          timestamp: Date.now(),
        }));
      }
    }
  });

  // Закрытие соединения
  ws.on('close', (code, reason) => {
    clients.delete(ws);
    console.log(`Отключился: ${code}`);
  });

  ws.on('error', (err) => {
    console.error('Ошибка:', err);
    clients.delete(ws);
  });
});

console.log('WebSocket сервер на ws://localhost:3000');

Практический пример: чат

// === Клиент: chat.js ===
class ChatClient {
  constructor(url, username) {
    this.username = username;
    this.ws = null;
    this.reconnectAttempts = 0;
    this.maxReconnect = 5;
    this.connect(url);
  }

  connect(url) {
    this.ws = new WebSocket(url);

    this.ws.onopen = () => {
      console.log('Подключено к чату');
      this.reconnectAttempts = 0;
      this.ws.send(JSON.stringify({
        type: 'join',
        username: this.username,
      }));
    };

    this.ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      this.handleMessage(msg);
    };

    this.ws.onclose = () => {
      if (this.reconnectAttempts < this.maxReconnect) {
        this.reconnectAttempts++;
        const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000);
        console.log(`Переподключение через ${delay}ms...`);
        setTimeout( => this.connect(url), delay);
      }
    };
  }

  sendMessage(text) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({
        type: 'message',
        text,
        username: this.username,
      }));
    }
  }

  handleMessage(msg) {
    switch (msg.type) {
      case 'message':
        console.log(`${msg.username}: ${msg.text()}`);
        break;
      case 'join':
        console.log(`${msg.username} вошёл в чат`);
        break;
      case 'leave':
        console.log(`${msg.username} вышел из чата`);
        break;
    }
  }

  disconnect {
    this.maxReconnect = 0; // Отключить реконнект
    this.ws.close(1000, 'Пользователь вышел');
  }
}

const chat = new ChatClient('ws://localhost:3000', 'Антон');
chat.sendMessage('Всем привет!');

Heartbeat (keep-alive)

// Сервер: отправляет ping каждые 30 секунд
const HEARTBEAT_INTERVAL = 30000;

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

  ws.on('pong', () => {
    ws.isAlive = true; // Клиент ответил
  });
});

const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) {
      return ws.terminate; // Не ответил → мёртвое соединение
    }
    ws.isAlive = false;
    ws.ping; // Отправить ping
  });
}, HEARTBEAT_INTERVAL);

wss.on('close', () => clearInterval(interval));

Частые ошибки

  1. Нет реконнекта — соединение обрывается, клиент не переподключается
  2. Нет heartbeat — мёртвые соединения висят, утечка ресурсов
  3. JSON.parse без try/catch — бинарные или некорректные данные → crash
  4. Нет проверки readyState — отправляют в закрытое соединение → ошибка
  5. Одно соединение на всех — нет разделения по комнатам/каналам
  6. Забывают wss:// — ws:// работает только без HTTPS, для production нужен wss://

Практика

  1. Создать WebSocket-сервер на ws (Node.js)
  2. Подключиться из браузера, отправлять/получать сообщения
  3. Реализовать broadcast — отправку всем подключённым клиентам
  4. Добавить автореконнект с exponential backoff
  5. Собрать простой чат: ввод → отправка → отображение у всех

Связанные темы

Ресурсы


🎓 Источник: WebSocket сервер на Node.js

  • 📅 2018-10-31 · YouTube
  • Тезисы:
    • URL ws:// / wss:// (secure); путь URL можно использовать как room.
    • WebSocket-сервер пропатчивает http.Server — добавляет обработку HTTP Upgrade.
    • requestaccept для принятия соединения (с autoAcceptConnections: false).
    • Сообщения с типом: utf8Data или binaryData.
    • Broadcast рассылкой по списку всех подключённых клиентов.
  • Цитата: «WebSocket пропатчивает HTTP-сервер, чтобы он понимал заголовки HTTP Upgrade.»