WebSockets в JavaScript

27 августа 2026
Автор

Антон Ларичев

Что такое WebSockets

WebSocket — это протокол, который обеспечивает полнодуплексный канал связи поверх единственного TCP-соединения. В отличие от классической модели HTTP, где клиент всегда инициирует запрос, WebSocket позволяет серверу самостоятельно отправлять данные клиенту в любой момент.

Это делает WebSockets незаменимыми для:

  • чатов и мессенджеров,
  • онлайн-игр,
  • систем уведомлений в реальном времени,
  • совместного редактирования документов,
  • финансовых тикеров и биржевых данных.

До появления WebSockets разработчики использовали обходные пути: polling (периодические запросы к серверу) и long polling (сервер держит соединение открытым до появления новых данных). Оба подхода неэффективны: они создают лишнюю нагрузку и имеют высокую задержку.

Курс по теме

Курс JavaScript с нуля

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

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

Соединение начинается с HTTP-запроса — так называемого handshake (рукопожатия). Клиент отправляет заголовок Upgrade: websocket, сервер отвечает статусом 101 Switching Protocols, и после этого оба перехоят на протокол WebSocket.

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

После установки соединения данные передаются в виде фреймов — бинарных или текстовых. Соединение остаётся открытым до тех пор, пока одна из сторон не закроет его явно.

Создание WebSocket-соединения

В браузере WebSocket доступен через встроенный глобальный класс WebSocket.

const socket = new WebSocket('ws://localhost:3000');

Для защищённых соединений используется схема wss:// (аналог HTTPS):

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

Объект WebSocket сразу начинает устанавливать соединение. Отслеживать его состояние можно через свойство readyState:

Значение Константа Описание
0 CONNECTING Соединение устанавливается
1 OPEN Соединение открыто
2 CLOSING Соединение закрывается
3 CLOSED Соединение закрыто

Основные события

WebSocket работает через события. Для каждого состояния есть свой обработчик.

open

Срабатывает, когда соединение успешно установлено:

const socket = new WebSocket('ws://localhost:3000');

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

message

Срабатывает при получении сообщения от сервера:

socket.addEventListener('message', (event) => {
  console.log('Получено:', event.data);
});

Данные в event.data могут быть строкой, Blob или ArrayBuffer — в зависимости от типа отправленного сообщения.

error

Срабатывает при ошибке соединения:

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

close

Срабатывает при закрытии соединения — как со стороны клиента, так и со стороны сервера:

socket.addEventListener('close', (event) => {
  console.log('Соединение закрыто');
  console.log('Код:', event.code);
  console.log('Причина:', event.reason);
  console.log('Корректное закрытие:', event.wasClean);
});

Событие close содержит код закрытия по стандарту RFC 6455. Код 1000 означает нормальное завершение, коды 10011015 описывают различные ошибки.

Отправка данных

Метод send() позволяет отправлять данные на сервер. Он принимает строку, Blob, ArrayBuffer или ArrayBufferView.

// Отправка строки
socket.send('Hello, server!');

// Отправка JSON
const message = { type: 'chat', text: 'Привет всем!', user: 'Иван' };
socket.send(JSON.stringify(message));

// Отправка бинарных данных
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setInt32(0, 42);
socket.send(buffer);

Вызывать send() можно только когда readyState === WebSocket.OPEN. Если вызвать раньше — браузер выбросит исключение.

function safeSend(socket, data) {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(typeof data === 'string' ? data : JSON.stringify(data));
  } else {
    console.warn('Соединение не открыто, readyState:', socket.readyState);
  }
}

Закрытие соединения

Закрыть соединение можно методом close(). Первый аргумент — код закрытия (по умолчанию 1000), второй — строковая причина:

// Нормальное закрытие
socket.close();

// Закрытие с кодом и причиной
socket.close(1001, 'Пользователь вышел со страницы');

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

Рассмотрим полноценный пример клиентской части чата:

class ChatClient {
  constructor(url) {
    this.url = url;
    this.socket = null;
    this.reconnectDelay = 1000;
    this.maxReconnectDelay = 30000;
  }

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

    this.socket.addEventListener('open', () => {
      console.log('Подключён к чату');
      this.reconnectDelay = 1000;
      this.onConnect();
    });

    this.socket.addEventListener('message', (event) => {
      try {
        const data = JSON.parse(event.data);
        this.handleMessage(data);
      } catch {
        console.error('Не удалось разобрать сообщение:', event.data);
      }
    });

    this.socket.addEventListener('close', (event) => {
      if (!event.wasClean) {
        this.scheduleReconnect();
      }
    });

    this.socket.addEventListener('error', () => {
      // ошибка всегда предшествует close, поэтому переподключение — в close
    });
  }

  send(text, username) {
    if (this.socket?.readyState === WebSocket.OPEN) {
      this.socket.send(JSON.stringify({
        type: 'message',
        text,
        username,
        timestamp: Date.now(),
      }));
    }
  }

  handleMessage(data) {
    switch (data.type) {
      case 'message':
        this.onMessage(data);
        break;
      case 'users':
        this.onUsersUpdate(data.users);
        break;
      default:
        console.warn('Неизвестный тип сообщения:', data.type);
    }
  }

  scheduleReconnect() {
    console.log(`Переподключение через ${this.reconnectDelay}мс...`);
    setTimeout(() => this.connect(), this.reconnectDelay);
    this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
  }

  disconnect() {
    this.socket?.close(1000, 'Пользователь вышел');
  }

  // Хуки для переопределения
  onConnect() {}
  onMessage(data) {}
  onUsersUpdate(users) {}
}

Использование:

const chat = new ChatClient('wss://chat.example.com/ws');

chat.onConnect = () => {
  document.getElementById('status').textContent = 'Подключён';
};

chat.onMessage = ({ username, text, timestamp }) => {
  const time = new Date(timestamp).toLocaleTimeString();
  appendMessage(`[${time}] ${username}: ${text}`);
};

chat.onUsersUpdate = (users) => {
  updateUserList(users);
};

chat.connect();

document.getElementById('send-btn').addEventListener('click', () => {
  const input = document.getElementById('message-input');
  chat.send(input.value, 'Иван');
  input.value = '';
});

Автоматическое переподключение

Sockets нестабильны — сеть может прерваться, сервер перезапуститься. Хорошая практика — реализовать экспоненциальный backoff при переподключении, как показано выше в методе scheduleReconnect(). Задержка удваивается после каждой неудачной попытки, но ограничена максимумом (например, 30 секунд).

Также стоит отслеживать, хочет ли пользователь реально отключиться или соединение разорвалось само:

class ReconnectingWebSocket {
  constructor(url) {
    this.url = url;
    this.intentionallyClosed = false;
    this.connect();
  }

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

    this.ws.addEventListener('close', () => {
      if (!this.intentionallyClosed) {
        setTimeout(() => this.connect(), 3000);
      }
    });
  }

  close() {
    this.intentionallyClosed = true;
    this.ws.close();
  }

  send(data) {
    this.ws.send(data);
  }
}

Работа с бинарными данными

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

Настроить тип получаемых бинарных данных можно через свойство binaryType:

const socket = new WebSocket('ws://localhost:3000');
socket.binaryType = 'arraybuffer'; // по умолчанию 'blob'

socket.addEventListener('message', (event) => {
  if (event.data instanceof ArrayBuffer) {
    const view = new DataView(event.data);
    const value = view.getInt32(0);
    console.log('Получено число:', value);
  } else {
    console.log('Получена строка:', event.data);
  }
});

Простой сервер на Node.js

Для быстрого тестирования клиентского кода удобно поднять сервер на Node.js с библиотекой ws:

npm install ws
import { WebSocketServer } from 'ws';

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

const clients = new Set();

wss.on('connection', (ws) => {
  clients.add(ws);
  console.log('Новый клиент подключился. Всего:', clients.size);

  ws.on('message', (rawData) => {
    let data;
    try {
      data = JSON.parse(rawData.toString());
    } catch {
      return;
    }

    // Рассылаем всем подключённым клиентам
    const payload = JSON.stringify(data);
    for (const client of clients) {
      if (client.readyState === 1) { // OPEN
        client.send(payload);
      }
    }
  });

  ws.on('close', () => {
    clients.delete(ws);
    console.log('Клиент отключился. Осталось:', clients.size);
  });
});

console.log('WebSocket-сервер запущен на ws://localhost:3000');

Heartbeat: проверка живости соединения

Некоторые прокси-серверы и балансировщики нагрузки разрывают «молчащие» соединения. Чтобы этого избежать, используют механизм ping/pong:

function startHeartbeat(socket, interval = 30000) {
  const timer = setInterval(() => {
    if (socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify({ type: 'ping' }));
    } else {
      clearInterval(timer);
    }
  }, interval);

  socket.addEventListener('close', () => clearInterval(timer));

  return timer;
}

const socket = new WebSocket('ws://localhost:3000');
socket.addEventListener('open', () => startHeartbeat(socket));

socket.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  if (data.type === 'pong') return; // игнорируем служебные сообщения
  // обрабатываем остальные сообщения
});

WebSockets vs Server-Sent Events vs Polling

Метод Направление Поддержка Когда использовать
Polling Клиент → Сервер Везде Простые случаи, нет реального времени
Server-Sent Events Сервер → Клиент Современные браузеры Только push от сервера (уведомления, новости)
WebSocket Двусторонний Современные браузеры Чат, игры, совместная работа

Server-Sent Events (SSE) проще в реализации, если нужно только получать данные от сервера. WebSocket — правильный выбор, когда клиент тоже активно отправляет данные.

Типичные ошибки

Отправка до открытия соединения. Вызов send() до события open вызывает ошибку. Всегда дожидайтесь открытия или буферизируйте сообщения.

Отсутствие обработки переподключения. Сети ненадёжны. Без автопереподключения пользователи будут видеть разорванное соединение без возможности восстановления.

Игнорирование типа данных. Если сервер отправляет JSON, не забудьте JSON.parse(event.data). Если бинарные данные — настройте binaryType.

Утечка обработчиков событий. Если WebSocket создаётся динамически, убедитесь, что при его уничтожении вы вызываете close() и убираете обработчики.

// Правильное уничтожение
function destroySocket(socket) {
  socket.close(1000, 'Компонент удалён');
  // removeEventListener не нужен, если использовали addEventListener
  // — они автоматически удалятся вместе с объектом
}

Итог

WebSockets — мощный инструмент для приложений реального времени. Ключевые моменты:

  • Используйте wss:// в продакшне (как HTTPS для HTTP).
  • Всегда реализуйте логику переподключения с экспоненциальной задержкой.
  • Передавайте структурированные данные через JSON или бинарный протокол.
  • Добавьте heartbeat, если соединение может простаивать долгое время.
  • Разделяйте типы сообщений через поле type в JSON-объекте.

С этими базовыми принципами вы сможете построить надёжную real-time систему на чистом JavaScript.

Чтобы глубже разобраться с асинхронностью и современными API браузера — изучите курс по JavaScript на PurpleSchool: https://purpleschool.ru/course/javascript?utm_source=knowledgebase&utm_medium=text&utm_campaign=websockets-javascript

JavaScript Web WorkersСтрелочка вправо

Постройте личный план изучения Javascript до уровня Middle — бесплатно!

Javascript — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Javascript

Как работает метод trim() - JavaScriptКак работает метод toUpperCase() - JavaScriptКак работает метод toLowerCase() - JavaScriptКак работает метод substring() - JavaScriptКак работает метод startsWith() - JavaScriptКак работает метод split() - JavaScriptКак работает метод slice() - JavaScriptКак работает метод search() - JavaScriptКак работает метод replaceAll() - JavaScriptКак работает метод replace() - JavaScriptКак работает метод repeat() - JavaScriptКак работает метод padStart() - JavaScriptКак работает метод padEnd() - JavaScriptКак работает метод matchAll() - JavaScriptКак работает метод match() - JavaScriptКак работает метод localeCompare() - JavaScriptКак работает свойство length - JavaScriptКак работает метод lastIndexOf() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод fromCodePoint() - JavaScriptКак работает метод fromCharCode() - JavaScriptКак работает метод endsWith() - JavaScriptКак работает метод concat() - JavaScriptКак работает метод codePointAt() - JavaScriptКак работает метод charCodeAt() - JavaScriptКак работает метод charAt() - JavaScript
Итератор в JavaScript
try...catch в JavaScriptError в JavaScript
Событие wheel в JavaScriptСобытие touch в JavaScriptСобытие unload в JavaScriptСобытие scroll в JavaScriptСобытие submit в JavaScriptСобытие reset в JavaScriptМетод .preventDefault() в JavaScriptСобытие mouseover в JavaScriptСобытие mouseout в JavaScriptСобытие load в JavaScriptСобытие keyup в JavaScriptСобытие keydown в JavaScriptСобытие invalid в JavaScriptСобытие input в JavaScriptСобытийная модель Event в JavaScriptОбъект события Event в JavaScriptСобытие DOMContentLoaded в JavaScriptСобытие dblclick в JavaScriptСобытие click в JavaScriptСобытие change в JavaScriptJavaScript BroadcastChannel — межвкладочное взаимодействиеСобытие beforeunload в JavaScript
Как работает метод some() - JavaScriptКак работает метод reverse() - JavaScriptКак работает метод reduce() - JavaScriptКак работает метод map() - JavaScriptКак работает метод isArray() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод from() - JavaScriptКак работает метод forEach() - JavaScriptКак работает метод flatMap() - JavaScriptКак работает метод flat() - JavaScriptКак работает метод findIndex() - JavaScriptКак работает метод find() - JavaScriptКак работает метод filter() - JavaScriptКак работает метод every() - JavaScriptМассивы в JavaScriptArray.at, findLast, findLastIndex — новые методы массивов
Открыть базу знаний

Лучшие курсы по теме

изображение курса

Основы JavaScript

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий