WebSocket API в браузере

WebSocket API в браузере

Теория WebSocket'ов понятна. Но как это выглядит в коде? Разберём WebSocket API — встроенный в браузер интерфейс для открытия соединения, отправки и приёма сообщений, обработки ошибок и закрытия.

Открытие соединения

WebSocket-соединение создаётся одной строкой:

const socket = new WebSocket('wss://example.com/chat');

Вызов конструктора НЕ ждёт завершения handshake — он возвращает объект сразу. Соединение устанавливается асинхронно. Состояние соединения доступно в socket.readyState:

readyStateЗначениеОписание
WebSocket.CONNECTING0Соединение устанавливается
WebSocket.OPEN1Соединение открыто, можно передавать данные
WebSocket.CLOSING2Соединение закрывается
WebSocket.CLOSED3Соединение закрыто

События WebSocket

WebSocket работает на событиях (event-driven). Четыре главных события:

const socket = new WebSocket('wss://example.com/chat');

socket.onopen = (event) => {
  console.log('Соединение открыто! Можно отправлять сообщения.');
  socket.send(JSON.stringify({ type: 'join', username: 'Иван' }));
};

socket.onmessage = (event) => {
  // event.data содержит данные (строка или Blob)
  const message = JSON.parse(event.data);
  console.log('Получено сообщение:', message);
  renderMessage(message);
};

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

socket.onclose = (event) => {
  console.log(`Соединение закрыто. Код: ${event.code}, Причина: ${event.reason}`);
  if (event.code !== 1000) {
    // Нештатное закрытие — возможно, переподключиться
    setTimeout(() => reconnect(), 3000);
  }
};

Порядок событий гарантирован: сначала onopen, потом onmessage (сколько угодно раз), потом onclose. onerror может произойти в любой момент до onclose.

Отправка сообщений

Метод socket.send(data) принимает строку, ArrayBuffer или Blob:

// Текст (формат text, opcode=1)
socket.send('Привет!');

// JSON — самый популярный формат
socket.send(JSON.stringify({
  type: 'message',
  room: 'general',
  text: 'Всем привет!'
}));

// Бинарные данные (opcode=2)
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setInt32(0, 42);
socket.send(buffer);

send() не блокирует выполнение. Данные ставятся в очередь и отправляются асинхронно. Если соединение ещё не открыто (readyState !== OPEN) — send() бросит исключение.

Формат сообщений: как договариваются клиент и сервер

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

// Клиент → Сервер: присоединиться к комнате
socket.send(JSON.stringify({ type: 'join', room: 'general' }));

// Сервер → Клиент: новое сообщение
socket.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'new_message') {
    showMessage(msg.user, msg.text);
  } else if (msg.type === 'user_joined') {
    showJoinNotification(msg.user);
  }
};

Поле type в JSON — распространённый паттерн для различения видов сообщений. По сути, ты строишь свой мини-протокол поверх WebSocket'а.

Переподключение и обработка разрывов

WebSocket-соединение может разорваться в любой момент: пропала сеть, сервер перезагрузился, прокси разорвал idle-соединение. Реальный код всегда включает логику переподключения:

let socket;
let reconnectAttempts = 0;

function connect() {
  socket = new WebSocket('wss://example.com/chat');

  socket.onopen = () => {
    console.log('Подключено');
    reconnectAttempts = 0; // Сброс счётчика
  };

  socket.onclose = (event) => {
    if (event.code === 1000) return; // Нормальное закрытие — не переподключаемся

    // Экспоненциальная задержка: 1с, 2с, 4с, 8с... максимум 30с
    const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
    reconnectAttempts++;
    console.log(`Переподключение через ${delay / 1000}с (попытка ${reconnectAttempts})`);
    setTimeout(connect, delay);
  };
}

Экспоненциальная задержка (exponential backoff) — стандартный подход: с каждой неудачей ждём дольше. Без этого сервер может быть завален лавиной переподключений после восстановления (thundering herd).

Готовность и буферизация

WebSocket имеет свойство bufferedAmount — количество байт, ожидающих отправки (в очереди, но ещё не переданных по сети):

// Не отправлять слишком много, пока очередь не очистится
if (socket.bufferedAmount < 1024 * 1024) {
  socket.send(largeData);
} else {
  console.log('Буфер заполнен, ждём drain');
}

Для большинства приложений это не нужно, но при передаче больших файлов через WebSocket контроль буфера предотвращает переполнение памяти.

Проверь себя

  1. Почему send() может бросить исключение?
  2. Что такое exponential backoff и зачем он нужен при переподключении?
  3. В каком формате обычно передают данные через WebSocket?
<details> <summary>Ответы</summary>
  1. Если соединение ещё не готово (readyState !== OPEN) или уже закрыто. Нужно дождаться события onopen.
  2. Exponential backoff — увеличение задержки переподключения вдвое с каждой неудачей (1с → 2с → 4с → ...). Предотвращает лавину запросов к восстанавливающемуся серверу.
  3. JSON — самый популярный формат. Поле type различает виды сообщений. Можно также передавать бинарные данные (ArrayBuffer, Blob).
</details>

Что унести с урока

  • new WebSocket(url) открывает соединение асинхронно. События: onopen, onmessage, onerror, onclose.
  • send() принимает строки и бинарные данные. JSON — стандарт для структурированных сообщений.
  • WebSocket не переподключается сам — нужна ручная логика с exponential backoff.
  • readyState показывает текущее состояние: CONNECTING → OPEN → CLOSING → CLOSED.

В завершающем уроке модуля сравним WebSocket vs SSE vs Long Polling — когда что использовать и какие компромиссы.

Попробуйте интерактивную версию

Практические задачи, квизы и AI-наставник — бесплатный старт без карты

Перейти к практике