Как работают WebSockets: подробное руководство по соединениям в реальном времени
На заре развития веба браузер представлял собой простой инструмент для просмотра документов. Вы запрашивали страницу, сервер ее отрисовывал, и соединение закрывалось. Этот цикл «запрос-ответ» лежит в основе протокола HTTP (Hypertext Transfer Protocol).
Однако по мере того, как веб-приложения превращались в интерактивные сервисы — такие как чаты в реальном времени, финансовые котировки, совместное редактирование и многопользовательские игры, — традиционная модель HTTP начала демонстрировать свои ограничения.
Чтобы получать обновления в реальном времени, разработчики поначалу полагались на обходные пути:
- Короткие опросы (Short Polling): Браузер регулярно отправляет HTTP-запросы на сервер каждые несколько секунд, чтобы проверить наличие новых данных. Это создает огромную нагрузку на заголовки и тратит ресурсы сервера.
- Длинные опросы (Long Polling / Comet): Браузер отправляет запрос, а сервер держит соединение открытым до тех пор, пока не появятся новые данные. После отправки данных соединение закрывается, и браузер сразу же открывает новый запрос. Этим процессом сложно управлять, и он по-прежнему требует больших затрат на установку соединений.
Технология WebSockets решила эти проблемы, предложив стандартизированный протокол для постоянного двустороннего полнодуплексного обмена данными по одному TCP-соединению.
Что такое WebSocket?
Протокол WebSockets (описанный в RFC 6455) работает параллельно с HTTP. В то время как HTTP — это протокол без сохранения состояния, в котором только клиент может инициировать запросы, соединение WebSocket остается открытым неограниченное время. Это позволяет и клиенту, и серверу обмениваться данными в любой момент с минимальной задержкой.
Вот фундаментальное правило WebSockets:
После установки соединения любая из сторон может отправлять сообщения в любое время без отправки нового запроса на подключение.
Пошаговое руководство: Жизненный цикл соединения
Соединение WebSocket проходит через три отдельные фазы: рукопожатие (Handshake), передачу данных и закрытие.
1. HTTP-рукопожатие (обновление протокола)
Поскольку сетевые экраны и маршрутизаторы настроены на пропуск стандартного веб-трафика через порты 80 (HTTP) и 443 (HTTPS), подключение WebSockets начинается как обычный запрос HTTP/1.1. Это называется Upgrade Handshake.
Запрос клиента
Клиент отправляет HTTP-запрос GET со специальными заголовками, запрашивающими смену протокола:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
Upgrade: websocketиConnection: Upgrade: сообщают серверу, что клиент хочет сменить протокол.Sec-WebSocket-Key: случайное 16-байтное значение, закодированное в Base64. Оно используется для подтверждения того, что сервер получил запрос и понимает протокол WebSocket.Sec-WebSocket-Version: указывает версию протокола WebSocket (обычно 13).Origin: используется сервером для проверки безопасности и защиты от межсайтовых подключений с неавторизованных ресурсов.
Ответ сервера
Если сервер поддерживает WebSockets, он проверяет запрос и отвечает кодом состояния HTTP 101 Switching Protocols:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- Как сервер вычисляет
Sec-WebSocket-Accept:- Сервер берет заголовок
Sec-WebSocket-Key(dGhlIHNhbXBsZSBub25jZQ==). - Объединяет его со стандартным уникальным идентификатором GUID:
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11". - Вычисляет хеш SHA-1 объединенной строки.
- Кодирует полученный результат в Base64.
- Если клиент подтверждает совпадение этого значения со своими ожиданиями, рукопожатие считается успешным, HTTP-соединение переключается на чистый TCP-сокет, и обе стороны переходят на работу по протоколу WebSocket.
- Сервер берет заголовок
2. Структурирование и передача данных
В отличие от HTTP, который отправляет текстовые заголовки, а затем тело запроса, WebSockets передают данные в виде структурированных двоичных пакетов, называемых фреймами (frames).
Фрейм WebSocket имеет небольшой заголовок (от 2 до 14 байт), за которым следуют полезные данные (payload). Этот заголовок содержит:
- FIN-бит (1 бит): указывает, является ли этот фрейм последним в сообщении.
- Opcode (4 бита): определяет тип фрейма:
0x1: текстовый фрейм (в кодировке UTF-8)0x2: бинарный фрейм0x8: запрос на закрытие соединения0x9: пинг (Ping)0xA: понг (Pong)
- Mask-бит (1 бит): указывает, замаскированы ли полезные данные.
- Payload Length: размер данных.
- Masking Key (4 байта): важнейшее требование безопасности: все фреймы, отправляемые от клиента к серверу, должны быть замаскированы (обфусцированы методом XOR) с использованием случайного 4-байтового ключа. Это предотвращает чтение трафика прокси-серверами и защищает от атак отравления кэша. Фреймы от сервера к клиенту маскироваться не должны.
Проверка активности (Heartbeats)
Чтобы предотвратить закрытие простаивающих соединений маршрутизаторами и балансировщиками нагрузки, любая из сторон может отправить фрейм Ping. Получатель обязан немедленно ответить фреймом Pong, содержащим те же полезные данные.
3. Закрытие соединения
Для правильного закрытия соединения:
- Один из участников отправляет фрейм
Close, содержащий код состояния (например,1000для штатного закрытия,1006для сбоя) и необязательное текстовое описание причины. - Второй участник отвечает своим фреймом
Close. - Базовый TCP-сокет закрывается.
Пример кода: Реализация WebSockets на Node.js
Чтобы увидеть WebSockets в действии, напишем простое приложение на Node.js. Мы создадим локальный сервер, возвращающий любые полученные сообщения, и клиентский скрипт для подключения.
WebSocket-сервер (server.js)
const { WebSocketServer } = require('ws');
const http = require('http');
// 1. Создаем стандартный HTTP-сервер
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('HTTP Server running. Use WebSocket to connect.\n');
});
// 2. Подключаем WebSocket-сервер к HTTP-серверу
const wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => {
const clientIp = req.socket.remoteAddress;
console.log(`[Сервер] Подключился новый клиент с адреса ${clientIp}`);
// Отправляем приветственное сообщение клиенту
ws.send(JSON.stringify({ type: 'welcome', message: 'Вы подключились к серверу Ghaznix!' }));
// Слушаем входящие сообщения от клиента
ws.on('message', (message) => {
console.log(`[Сервер] Получено: ${message}`);
try {
const data = JSON.parse(message);
ws.send(JSON.stringify({
type: 'echo',
message: `Эхо сервера: ${data.text.toUpperCase()}`,
timestamp: new Date().toISOString()
}));
} catch (e) {
ws.send(JSON.stringify({ type: 'error', message: 'Неверный формат JSON' }));
}
});
// Обрабатываем отключение клиента
ws.on('close', (code, reason) => {
console.log(`[Сервер] Клиент отключился (Код: ${code}, Причина: ${reason.toString() || 'Нет'})`);
});
ws.on('error', (error) => {
console.error(`[Сервер] Ошибка сокета: ${error.message}`);
});
});
server.listen(8080, () => {
console.log('WebSocket-сервер запущен на ws://localhost:8080');
});
Браузерный клиент (JavaScript на стороне клиента)
Вы можете запустить этот код прямо в консоли вашего браузера:
// 1. Устанавливаем соединение с сервером
const socket = new WebSocket('ws://localhost:8080');
// 2. Обработчик успешного открытия соединения
socket.addEventListener('open', (event) => {
console.log('[Клиент] Соединение установлено.');
const payload = JSON.stringify({ text: 'привет, сервер!' });
socket.send(payload);
console.log(`[Клиент] Отправлено: ${payload}`);
});
// 3. Слушаем входящие сообщения от сервера
socket.addEventListener('message', (event) => {
const response = JSON.parse(event.data);
console.log('[Клиент] Получено сообщение от сервера:', response);
});
// 4. Слушаем закрытие соединения
socket.addEventListener('close', (event) => {
console.log(`[Клиент] Соединение закрыто (Код: ${event.code})`);
});
// 5. Обработка ошибок
socket.addEventListener('error', (error) => {
console.error('[Клиент] Ошибка WebSocket:', error);
});
Сравнение HTTP и WebSockets
| Характеристика | HTTP/1.1 | WebSockets |
|---|---|---|
| Тип связи | Однонаправленный (только от клиента) | Двунаправленный (клиент или сервер) |
| Модель подключения | Запрос-ответ (краткосрочное) | Постоянное (длительное) |
| Накладные расходы | Высокие (заголовки при каждом запросе) | Очень низкие (минимальный объем фрейма) |
| Сохранение состояния | Нет | Да (контекст подключения сохраняется) |
| Протокол | http:// или https:// |
ws:// или wss:// |
| Сфера применения | Загрузка документов, REST API | Чаты, панели мониторинга, онлайн-игры |
Вопросы безопасности при использовании WebSockets
Поскольку после рукопожатия WebSockets обходят стандартную маршрутизацию HTTP, они создают специфические риски безопасности:
- Используйте защищенный протокол WebSocket Secure (
wss://): Всегда запускайте WebSockets поверх TLS/SSL (порт 443). Шифрование фреймов защищает полезную нагрузку от прослушивания и перехвата. - Проверка заголовка Origin: Для WebSockets не действует правило ограничения домена (Same-Origin Policy). Обязательно проверяйте заголовок
Originна стороне сервера во время рукопожатия. - Авторизация при рукопожатии: Аутентифицируйте пользователей до установки соединения. Обычно это делается путем передачи токена (например, JWT) в параметрах запроса или проверки сессионных файлов cookie.
- Фильтрация ввода: Относитесь к любому входящему сообщению через WebSockets как к потенциально опасному. Очищайте и проверяйте входящие данные для предотвращения XSS-атак.
Резюме
Протокол WebSockets произвел революцию в разработке интерактивных приложений, устранив избыточные затраты на регулярные HTTP-опросы. Благодаря сохранению постоянного TCP-соединения он обеспечивает мгновенную доставку сообщений в обоих направлениях, что позволяет создавать динамические дашборды, многопользовательские игры и чаты. Понимание процессов обновления HTTP, архитектуры фреймов и мер безопасности поможет вам проектировать быстрые и надежные сервисы реального времени.