Введение в 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 функциональности — сделаем чат, в котором сообщения будут приходить одновременно всем его участникам.
Мы создадим базовую версию, в которой будет настроена только отправка сообщений всем подключенным клиентам. В дальнейшем вы сможете вернуться к этому файлу и добавить дополнительную информацию, такую как время отправки, автора и другие тонкости.
Перед непосредственным написанием кода нужно:
- Создать директорию, в которой будет храниться код чата — через терминал или интерфейс вашей операционной системы
- Создать в этой директории два файла — 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/ — сайт, на котором можно посмотреть реализацию веб-сокетов. Обратите внимание на вкладку с запросами и ответами в браузере.
Артем Коноплев
Автор статьи