Введение в WebSocket: Теория и Примеры для Начинающих

Введение в WebSocket: Теория и Примеры для Начинающих

Эта технология используется для чатов, игр, финансовых приложений и других систем, требующих мгновенного обмена данными. Все, что нужно знать новичкам о веб-сокетах и как их настраивать — вы найдете в этой статье.

На связи Артем Коноплев — фронтенд-разработчик, преподаватель Эльбруса 2021-2022 годов, ваш бессменный помощник и проводник в мир веб-разработки. Итак, начнем ↓

Теория WebSocket

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

История и развитие

Протокол WebSocket был разработан в 2008 году и стандартизирован IETF как RFC 6455 в 2011 году. Он был создан для решения проблем, связанных с двусторонней связью в веб-приложениях, где традиционные методы, такие как HTTP, были неэффективны для приложений в реальном времени.

Особенности протокола

  • Двусторонняя связь — позволяет клиенту и серверу обмениваться данными в обоих направлениях, без повторного открытия соединения
  • Низкая задержка — поскольку соединение остаётся открытым, задержка при передаче данных значительно ниже по сравнению с HTTP-запросами
  • Эффективность — веб-сокеты используют меньше ресурсов, по сравнению с открытием новых HTTP-соединений для каждого запроса.

Преимущества WebSocket

  • Идеально подходит для приложений, требующих обновления данных в реальном времени (например, чаты, игровые приложения, финансовые биржи).
  • Уменьшает нагрузку на сервер и сеть за счёт постоянного соединения.
  • Современные браузеры поддерживают WebSocket из коробки, что облегчает его внедрение.

Порядок работы WebSocket

1. Установление соединения

Начинается с HTTP-запроса на установку соединения (handshake). Если сервер поддерживает WebSocket, он отвечает специальным заголовком, подтверждающим установку соединения. Этот заголовок включает в себя Upgrade-заголовок, который сообщает серверу, что клиент хочет переключиться на протокол WebSocket.

Пример запроса и ответа на установление WebSocket соединения

Для установления WebSocket соединения используется начальный HTTP-запрос, называемый handshake (рукопожатие). Этот запрос и ответ позволяют клиенту и серверу переключиться на протокол WebSocket. Ниже приведены примеры запроса от клиента и ответа от сервера.

  • Пример запроса от клиента: клиент отправляет HTTP-запрос с методом GET и заголовками, которые указывают на намерение установить WebSocket соединение.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com
Объяснение заголовков:
  • GET /chat HTTP/1.1 — запрос к ресурсу /chat на сервере
  • Host: example.com — доменное имя сервера
  • Upgrade: websocket — инструкция серверу для переключения на протокол WebSocket
  • Connection: Upgrade — указывает, что соединение должно быть обновлено
  • Sec-WebSocket-Key — случайная строка, сгенерированная клиентом, которая используется сервером для создания ответа
  • Sec-WebSocket-Version — версия протокола WebSocket, которую поддерживает клиент
  • Origin — указывает источник запроса, чтобы сервер мог проверить его.
  • Пример ответа от сервера: сервер отвечает, подтверждая, что он готов переключиться на протокол WebSocket.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Объяснение заголовков:
  • HTTP/1.1 101 Switching Protocols — код ответа 101 указывает на успешное обновление протокола
  • Upgrade: websocket — подтверждение, что протокол обновляется до WebSocket
  • Connection: Upgrade — подтверждение обновления соединения
  • Sec-WebSocket-Accept — хэш, созданный сервером на основе ключа Sec-WebSocket-Key, присланного клиентом. Этот хэш подтверждает, что сервер поддерживает WebSocket.

2. Обмен данными

После установления соединения клиент и сервер могут обмениваться данными в обоих направлениях в любое время. Сообщения передаются в формате фреймов. Основные типы фреймов включают:

  • Текстовые фреймы содержат текстовые данные, закодированные в UTF-8
  • Бинарные фреймы содержат бинарные данные
  • Пинг и понг фреймы используются для проверки активности соединения
  • Фреймы закрытия инициируют закрытие соединения.

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

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

Примеры использования WebSocket

Чтобы веб-сокет соединение работало,  нужно установить его как на клиенте, так и на сервере ↓

Установка соединения на клиенте (JavaScript)

// Создаем новое соединение WebSocket
let socket = new WebSocket("ws://example.com/socket");

// Открываем соединение
socket.onopen = function(event) {
  console.log("Соединение установлено.");
  // Отправляем сообщения серверу при установке соединения
  socket.send("Привет, сервер!"); 
};

// Получаем сообщения
socket.onmessage = function(event) {
  // Выводим полученное сообщение в консоль
  console.log("Сообщение от сервера: ", event.data); 
};

// Обрабатываем ошибки
socket.onerror = function(error) {
  // Выводим ошибки в консоль
  console.error("Ошибка: ", error.message); 
};

// Закрываем соединение
socket.onclose = function(event) {
  if (event.wasClean) {
    // Выводим сообщение о чистом закрытии
    console.log(`Соединение закрыто чисто, код=${event.code} причина=${event.reason}`); 
  } else {
    // Выводим сообщение о прерванном соединении
    console.error('Соединение прервано'); 
  }
};

Пример сервера на Node.js

Для создания сервера WebSocket на Node.js используется библиотека ws.

const WebSocket = require('ws');

// Создаем новый сервер WebSocket на порту 8080
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
  console.log('Новое соединение установлено.');

  // Обрабатываесм полученное сообщение от клиента
  ws.on('message', function incoming(message) {
    console.log('Получено сообщение: %s', message);
    // Отправляем эхо-сообщение обратно клиенту 
    ws.send(`Эхо: ${message}`); 
  });

  // Обрабатываем закрытие соединения
  ws.on('close', function() {
    console.log('Соединение закрыто.');
  });
});

console.log('Сервер WebSocket запущен на порту 8080.');

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

Теперь попробуем использовать эту технологию для реализации real-time функциональности — сделаем чат, в котором сообщения будут приходить одновременно всем его участникам.

Мы создадим базовую версию, в которой будет настроена только отправка сообщений всем подключенным клиентам. В дальнейшем вы сможете вернуться к этому файлу и добавить дополнительную информацию, такую как время отправки, автора и другие тонкости.

Перед непосредственным написанием кода нужно:

  1. Создать директорию, в которой будет храниться код чата — через терминал или интерфейс вашей операционной системы
  2. Создать в этой директории два файла — index.html или server.js (конкретные имена не играют роли, но далее в примере будут использоваться именно такие названия).

Клиентская часть (HTML + JavaScript)

Напишем код, который будет исполняться в браузере. Для максимальной компактности и прозрачности будем писать JS-код прям в файле index.html, в теге <script>:

<!DOCTYPE html>
<html>
<head>
  <title>WebSocket Chat</title>
</head>
<body>
  <h1>WebSocket Chat</h1>
  <!-- Контейнер для отображения сообщений -->
  <div id="messages"></div> 
  <input id="input" type="text" placeholder="Введите сообщение...">
  <!-- Кнопка для отправки сообщения -->
  <button onclick="sendMessage()">Отправить</button> 

  <script>
    // Создаем новое соединение WebSocket
    const socket = new WebSocket("ws://localhost:8080");

    // Открываем соединение
    socket.onopen = function() {
      console.log("Соединение установлено.");
    };

    // Обрабатываем полученное сообщение
    socket.onmessage = function(event) {
      // Находим список сообщений
      let messages = document.getElementById("messages");
      // Создаем контейнер нового сообщения
      let message = document.createElement("div"); 
      // Извлекаем текст из сообщения и добавляем его в контейнер сообщения
      message.textContent = event.data; 
      // Добавляем контейнер сообщения в список сообщений
      messages.appendChild(message); 
    };

    // Функция отправки сообщения, вызывается при нажатии на кнопку (см. верстку выше)
    function sendMessage() {
      // Находим в документе текстовое поле ввода с сообщением
      let input = document.getElementById('input'); 
      // Отправляем введенное сообщение
      socket.send(input.value); 
      // Очищаем поле ввода
      input.value = ''; 
    }
  </script>
</body>
</html>

Этот файл теперь можно открыть в двух разных браузерах (или разных окнах одного браузера). Однако сейчас для работы чата не хватает сервера — напишем его.

Серверная часть (Node.js)

Так как для работы сокетов в node.js нужна внешняя библиотека, нам нужно её установить. Открываем терминал в той же самой директории, которую создали ранее, и пишем:

npm init -y

Эта команда создаст нам файл package.json с подходящими настройками. Далее устанавливаем нужную нам библиотеку:

npm i ws

А затем в файле server.js пишем следующий код:

const WebSocket = require('ws');

// Создаем новый сервер WebSocket на порту 8080
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('Новое соединение установлено.');

  // Обрабатываем полученное сообщение от клиента
  ws.on('message', (message) => {
    /* Обязательно превращаем message в строку, 
    поскольку по умолчанию message – это т.н. blob, 
    более низкоуровневая сущность, оптимизированная для передачи по сети */
	const messageString = message.toString(); 
    console.log(`Получено сообщение: ${messageString}`);

    // Рассылаем сообщения всем подключенным клиентам
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(messageString);
      }
    });
  });

  // Обрабатываем закрытия соединения
  ws.on('close', () => {
    console.log('Соединение закрыто.');
  });
});

console.log('Сервер WebSocket запущен на порту 8080.');

После этого пишем в терминале:

npm start

Эта команда запустит наш сервер на 8080 порте. После этого возвращаемся в окна браузера с файлом index.html, которые мы открыли ранее, обновляем страницу. Теперь можно написать сообщение в текстовое поле, нажать на кнопку и получить его во всех открытых окнах.

Пример установления WebSocket соединения с использованием библиотеки socket.io

Socket.IO — это библиотека, которая упрощает работу с WebSocket и предоставляет дополнительные функции, такие как автоматическое восстановление соединений и поддержку других транспортных протоколов.

Установка зависимостей

Для работы с Socket.IO необходимо установить соответствующие пакеты:

npm install socket.io
npm install socket.io-client

Клиентская часть (HTML + JavaScript)

<!DOCTYPE html>
<html>
<head>
  <title>Socket.IO Chat</title>
  <script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
</head>
<body>
  <h1>Socket.IO Chat</h1>
  <!-- Контейнер для отображения сообщений -->
  <div id="messages"></div> 
  <input id="input" type="text" placeholder="Введите сообщение...">
  <!-- Кнопка для отправки сообщения -->
  <button onclick="sendMessage()">Отправить</button> 

  <script>
    // Создаем новое соединение Socket.IO
    const socket = io('http://localhost:8080');

    // Обрабатываем полученное сообщение
    socket.on('message', (message) => {
      const messages = document.getElementById('messages');
      const messageElem = document.createElement('div');
      messageElem.textContent = message;
      // Добавляем сообщение в контейнер
      messages.appendChild(messageElem); 
    });

    // Функция отправки сообщения
    function sendMessage() {
      const input = document.getElementById('input');
      // Отправляем введенное сообщение
      socket.send(input.value); 
      // Очищаем поле ввода
      input.value = ''; 
    }
  </script>
</body>
</html>

Серверная часть (Node.js)

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

// Создаем новое Express-приложение и HTTP-сервер
const app = express();
const server = http.createServer(app);
const io = socketIo(server);

// Обрабатываем новое подключение
io.on('connection', (socket) => {
  console.log('Новое соединение установлено.');

  // Обрабатываем полученное сообщение от клиента
  socket.on('message', (message) => {
    console.log('Получено сообщение: %s', message);
    // Отправляем эхо-сообщение обратно клиенту
    socket.send(`Эхо: ${message}`);
  });

  // Обрабатываем отключение клиента
  socket.on('disconnect', () => {
    console.log('Соединение закрыто.');
  });
});

// Запуск сервера на порту 8080
server.listen(8080, () => {
  console.log('Сервер запущен на порту 8080.');
});

Альтернативные решения

Помимо WebSocket, существует несколько других технологий и протоколов, которые позволяют достичь аналогичной функциональности ↓

Long Polling

Long Polling — это метод, при котором клиент отправляет запрос к серверу и держит соединение открытым до тех пор, пока сервер не отправит ответ. После получения ответа клиент немедленно отправляет новый запрос, что создаёт иллюзию постоянного соединения.

Приемущества:
  • Совместим с любыми веб-серверами
  • Поддерживается всеми браузерами.
Недостатки:
  • Более высокая задержка по сравнению с WebSocket
  • Увеличенная нагрузка на сервер из-за большого количества открытых соединений.

Server-Sent Events (SSE)

SSE позволяет серверу отправлять обновления данных клиенту по HTTP-соединению. Клиент инициирует одноразовый HTTP-запрос, и сервер продолжает отправлять данные по этому соединению, пока оно не будет закрыто.

Приемущества:
  • Простая реализация на стороне сервера
  • Поддерживает автоматическое восстановление соединения.
Недостатки:
  • Односторонняя связь: данные могут идти только от сервера к клиенту
  • Ограничена только текстовыми данными.

HTTP/2

HTTP/2 поддерживает многопоточность, что позволяет клиенту и серверу обмениваться несколькими потоками данных по одному TCP-соединению. Это позволяет улучшить производительность по сравнению с HTTP/1.1.

Приемущества:
  • Совместимость с существующими веб-технологиями
  • Улучшенная производительность по сравнению с HTTP/1.1.
Недостатки:
  • Не обеспечивает такую же низкую задержку и двустороннюю связь, как WebSocket
  • Сложнее настроить и внедрить.

MQTT

MQTT (Message Queuing Telemetry Transport) — это легковесный протокол для обмена сообщениями, оптимизированный для работы в условиях ограниченных ресурсов и нестабильных сетей. Он часто используется в IoT (Интернет вещей).

Приемущества:
  • Оптимизирован для низкой пропускной способности и высокой надежности
  • Поддерживает сообщения с низкой задержкой.
Недостатки:
  • Требует специализированного брокера сообщений
  • Меньшая поддержка в веб-браузерах по сравнению с WebSocket.

WebTransport

WebTransport — это современный протокол, предназначенный для обеспечения эффективной и надёжной передачи данных между клиентом и сервером. WebTransport использует протокол QUIC и предоставляет аналогичную функциональность WebSocket, но с улучшенной производительностью и безопасностью.

Приемущества:
  • Использование QUIC позволяет WebTransport обеспечивать быструю передачу данных с еще более низкой задержкой, чем на веб-сокетах
  • Встроенная поддержка надёжной и ненадёжной доставки данных
  • Протокол QUIC обеспечивает улучшенные механизмы безопасности по сравнению с TCP.
Недостатки:
  • В настоящее время поддерживается не всеми браузерами и серверами
  • Более сложная реализация по сравнению с WebSocket.

Бонус

https://ws-playground.netlify.app/ — сайт, на котором можно посмотреть реализацию веб-сокетов. Обратите внимание на вкладку с запросами и ответами в браузере.

Артем Коноплев

Автор статьи

Софья Пирогова

Софья Пирогова

Главный редактор / Автор статей