2. WebSocket: протокол
WebSocket — стандартный протокол (RFC 6455), обеспечивающий постоянное двустороннее соединение между клиентом и сервером поверх TCP.
Как устанавливается соединение
Заголовок раздела «Как устанавливается соединение»HTTP Upgrade Handshake
Заголовок раздела «HTTP Upgrade Handshake»WebSocket начинается с обычного HTTP-запроса и затем “апгрейдится”:
1. Клиент отправляет HTTP запрос:───────────────────────────────GET /chat HTTP/1.1Host: example.comUpgrade: websocket ← запрос на апгрейдConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Sec-WebSocket-Version: 13
2. Сервер отвечает (101 Switching Protocols):─────────────────────────────────────────────HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
3. Соединение установлено — теперь это не HTTP, а WebSocket!Ключ безопасности
Заголовок раздела «Ключ безопасности»Sec-WebSocket-Key — случайный base64 ключ от клиента. Сервер вычисляет ответ:
const crypto = require('crypto');
function computeAccept(key) { const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; return crypto .createHash('sha1') .update(key + GUID) .digest('base64');}
// Примерconst key = 'dGhlIHNhbXBsZSBub25jZQ==';console.log(computeAccept(key)); // s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Фреймы (Frames)
Заголовок раздела «Фреймы (Frames)»После установки соединения данные передаются в виде фреймов:
WebSocket Frame:┌─────────┬──────────┬──────────────┬────────────────┐│ Flags │ Opcode │ Payload len │ Payload data ││ (1byte)│ (4bits) │ (7/16/64bit)│ (переменная) │└─────────┴──────────┴──────────────┴────────────────┘
Opcodes: 0x0 = continuation frame 0x1 = text frame ← текст (UTF-8) 0x2 = binary frame ← бинарные данные 0x8 = close 0x9 = ping 0xA = pongHeartbeat: Ping/Pong
Заголовок раздела «Heartbeat: Ping/Pong»Чтобы держать соединение живым и проверять, что клиент ещё там:
const WebSocket = require('ws');const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => { ws.isAlive = true;
ws.on('pong', () => { // Клиент ответил на ping ws.isAlive = true; });});
// Пингуем всех клиентов каждые 30 секундconst interval = setInterval(() => { wss.clients.forEach((ws) => { if (!ws.isAlive) { console.log('Клиент не отвечает — отключаем'); return ws.terminate(); } ws.isAlive = false; ws.ping(); // Отправляем ping });}, 30000);
wss.on('close', () => clearInterval(interval));Закрытие соединения
Заголовок раздела «Закрытие соединения»Закрытие происходит с кодом статуса:
// Клиент закрывает нормальноws.close(1000, 'Пользователь вышел');
// Сервер закрывает с причинойws.close(1001, 'Сервер перезагружается');
// Коды закрытия (RFC 6455):const CLOSE_CODES = { 1000: 'Normal closure', // нормальное закрытие 1001: 'Going away', // вкладка закрыта 1002: 'Protocol error', // ошибка протокола 1003: 'Unsupported data', // неподдерживаемый тип данных 1006: 'Abnormal closure', // соединение потеряно 1007: 'Invalid frame payload', // невалидные данные 1008: 'Policy violation', // нарушение политики 1009: 'Message too big', // слишком большое сообщение 1011: 'Internal error', // внутренняя ошибка сервера 1012: 'Service restart', // перезапуск сервиса};Маскировка данных
Заголовок раздела «Маскировка данных»Клиент обязан маскировать все данные (XOR с 4-байтным ключом). Сервер — нет:
Клиент → Сервер: ВСЕГДА маскироватьСервер → Клиент: НИКОГДА не маскироватьЭто защита от кеш-отравления прокси-серверов.
Максимальный размер сообщения
Заголовок раздела «Максимальный размер сообщения»По умолчанию ограничений нет, но рекомендуется ограничивать на сервере:
const wss = new WebSocket.Server({ port: 8080, maxPayload: 1024 * 1024, // 1 MB максимум});Расширения протокола
Заголовок раздела «Расширения протокола»WebSocket поддерживает расширения (Extensions):
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bitspermessage-deflate — сжатие данных (уменьшает трафик на 60-80% для JSON):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080, perMessageDeflate: { zlibDeflateOptions: { chunkSize: 1024, memLevel: 7, level: 3, }, threshold: 1024, // Сжимать только если больше 1KB },});SubProtocols
Заголовок раздела «SubProtocols»Можно договориться о вложенном протоколе:
Клиент: Sec-WebSocket-Protocol: chat, superchatСервер: Sec-WebSocket-Protocol: chat// Клиент указывает предпочитаемые протоколыconst ws = new WebSocket('ws://example.com/chat', ['json', 'binary']);
// Сервер выбирает одинconst wss = new WebSocket.Server({ port: 8080, handleProtocols: (protocols, req) => { if (protocols.has('json')) return 'json'; if (protocols.has('binary')) return 'binary'; return false; // Отказать },});ws vs socket.io vs native
Заголовок раздела «ws vs socket.io vs native»| Аспект | ws (native) | socket.io | Browser WebSocket |
|---|---|---|---|
| Авто-переподключение | ❌ | ✅ | ❌ |
| Fallback на polling | ❌ | ✅ | ❌ |
| Комнаты | ❌ | ✅ | ❌ |
| Namespace | ❌ | ✅ | ❌ |
| Размер | ~50KB | ~200KB | 0KB (браузер) |
| Контроль | Полный | Ограничен | Браузерный |
Задания
Заголовок раздела «Задания»- Реализуй функцию
computeAccept(key)для WebSocket handshake - Объясни, почему клиент маскирует данные, а сервер — нет
- Что произойдёт, если сервер не ответит на ping в течение 30 секунд?
- Когда использовать бинарный фрейм вместо текстового?
WebSocket — это надстройка над TCP с простым протоколом: handshake через HTTP, затем фреймы с данными в обе стороны. В следующем уроке — работа с браузерным WebSocket API.