WebSocket
Веб-странице нужно получать показания с платы без перезапросов, а плате — команды без опроса сервера. WebSocket начинается как обычный HTTP-запрос, а после ответа 101 превращается в постоянный двусторонний канал поверх того же TCP-соединения.
Обновлено Редакция GAW
Содержание
WebSocket — протокол двустороннего обмена сообщениями поверх TCP, определённый в RFC 6455. Его единственная связь с HTTP — рукопожатие: клиент отправляет HTTP-запрос с заголовком Upgrade, сервер отвечает статусом 101, и дальше по тому же соединению в обе стороны независимо идут кадры WebSocket (разд. 1.2, 1.7). Авторы описывают протокол как «настолько близкий к сырому TCP, насколько это безопасно в вебе»: он добавляет модель origin для браузеров, выбор службы по URI на одном порту, кадрирование без ограничения длины и закрывающее рукопожатие — и больше ничего (разд. 1.5).
Рукопожатие
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Пример из разд. 1.2 RFC 6455. Ключ — случайные 16 байт в Base64, новые для каждого соединения (разд. 4.1). Сервер доказывает, что понял протокол: склеивает ключ со строкой 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, берёт хеш SHA-1 и кодирует его в Base64 — это и есть значение Sec-WebSocket-Accept (разд. 1.3). Любой статус, кроме 101, означает, что соединение осталось HTTP. Заголовок Sec-WebSocket-Protocol позволяет договориться о подпротоколе — так поверх WebSocket пускают MQTT, когда брокер доступен только через веб-порт. Адреса — ws://host/path с портом 80 по умолчанию и wss:// с портом 443 поверх TLS (разд. 3).
Кадр
| Поле | Размер | Значение |
|---|---|---|
| FIN | 1 бит | 1 — последний кадр сообщения |
| RSV1–RSV3 | по 1 биту | 0, если не согласовано расширение |
| opcode | 4 бита | 0 продолжение, 1 текст, 2 двоичные данные, 8 закрытие, 9 ping, 0xA pong |
| Mask | 1 бит | 1 — за длиной следует ключ маски; у клиента всегда 1 |
| длина | 7 бит | 0–125 — сама длина; 126 — длина в следующих 2 байтах; 127 — в 8 байтах |
| ключ маски | 0 или 4 байта | случайное 32-битное значение |
| данные | до 2^63 − 1 байт | текст в UTF-8 или произвольные байты |
Формат по разд. 5.2. Длина кодируется минимальным числом байт. Сообщение можно резать на кадры: первый несёт opcode 1 или 2, остальные — 0, последний — FIN = 1. Управляющие кадры (8, 9, 0xA) не длиннее 125 байт и не фрагментируются; на Ping получатель обязан ответить Pong, поэтому Ping используют как keepalive поверх TCP keepalive или вместо него (разд. 5.5). Close несёт 2-байтовый код статуса, например 1002 — ошибка протокола.
Маскирование
Клиент обязан маскировать каждый кадр, даже поверх TLS; сервер маскировать не вправе (разд. 5.1). Операция проста: байт i данных складывается по XOR с байтом ключа i mod 4 (разд. 5.3). Это не шифрование — ключ лежит в том же кадре. Цель — не дать скрипту в браузере выбрать байты на проводе так, чтобы кеширующий прокси принял их за HTTP-запрос и сохранил подложный ответ (разд. 10.3). Для микроконтроллера это означает лишь источник случайных чисел и один цикл XOR на отправку.
В прошивке
Компонент ESP WebSocket Client на ESP32 настраивается структурой esp_websocket_client_config_t с полем .uri = "ws://host"; для wss:// сервер проверяется только при переданном cert_pem, без сертификата TLS-соединение устанавливается без проверки. В Zephyr WebSocket — надстройка над уже подключённым TCP-сокетом: websocket_connect() возвращает новый дескриптор, в который можно писать обычным send() (только текст) или websocket_send_msg() с WEBSOCKET_OPCODE_DATA_BINARY; опция CONFIG_MQTT_LIB_WEBSOCKET пускает MQTT через этот канал. Какие части — рукопожатие, разбор кадров, таймеры Ping — писать самому, а какие брать из библиотеки, разобрано в статье о реализации протоколов.
Частые вопросы
Зачем WebSocket начинается с HTTP-запроса?
Чтобы делить порт с веб-сервером и проходить через прокси. Клиент шлёт GET с заголовками Upgrade: websocket, Connection: Upgrade и случайным ключом Sec-WebSocket-Key; сервер отвечает 101 Switching Protocols и Sec-WebSocket-Accept — Base64 от SHA-1 ключа, склеенного с GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11. Любой другой статус означает, что это по-прежнему HTTP. После 101 оба конца обмениваются кадрами по тому же TCP-соединению (RFC 6455, разд. 1.3).
Почему клиент обязан маскировать кадры, а сервер — нет?
Это защита инфраструктуры, а не шифрование: случайная 32-битная маска XOR не даёт вредоносному скрипту в браузере подобрать байты на проводе так, чтобы прокси принял их за HTTP-запрос и отравил свой кеш (разд. 10.3). Поэтому клиент маскирует всё, даже поверх TLS, а сервер при немаскированном кадре закрывает соединение с кодом 1002. Серверу маскировать нечего — его трафик скрипт не контролирует.
Сколько байт добавляет WebSocket к каждому сообщению?
Заголовок кадра — минимум 2 байта: FIN с opcode и бит маски с 7-битной длиной (наш расчёт по разд. 5.2). При длине 126–65 535 байт добавляются 2 байта длины, свыше — 8 байт. Кадр от клиента несёт ещё 4 байта маски. Итого показание датчика в 20 байт уходит от платы к серверу в 26 байтах полезной нагрузки TCP (наш расчёт) — заметно меньше, чем HTTP-запрос с заголовками.
Источники
- I. Fette, A. Melnikov. The WebSocket Protocol. RFC 6455, December 2011 — разд. 1.2: две части — рукопожатие и передача данных; запрос GET с Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==, Sec-WebSocket-Version: 13, ответ HTTP/1.1 101 Switching Protocols с Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=; после рукопожатия каждая сторона шлёт сообщения независимо, сообщение состоит из кадров, типы — текст UTF-8, двоичные и управляющие, определено 6 типов кадров, 10 зарезервировано; разд. 1.3: рукопожатие — HTTP Upgrade, чтобы HTTP и WebSocket делили один порт; Accept = Base64(SHA-1(ключ + GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)), любой статус, кроме 101, означает незавершённое рукопожатие, Sec-WebSocket-Protocol выбирает подпротокол, Origin защищает от чужих сайтов; разд. 1.5: слой над TCP — модель origin, адресация служб на одном порту, кадрирование без ограничения длины, закрывающее рукопожатие; разд. 1.7: независимый протокол поверх TCP, порт 80 обычный и 443 поверх TLS; разд. 3: схемы ws и wss с портами 80 и 443 по умолчанию; разд. 4.1: Sec-WebSocket-Key — случайные 16 байт в Base64, новые для каждого соединения, Origin обязателен для браузера; разд. 5.1: клиент маскирует все кадры, в том числе поверх TLS, сервер закрывает соединение при немаскированном кадре (код 1002), сервер свои кадры не маскирует; разд. 5.2: FIN 1 бит, RSV1–3, opcode 4 бита — 0 продолжение, 1 текст, 2 двоичные, 8 закрытие, 9 ping, 0xA pong; Mask 1 бит; длина 7 бит, при 126 — следующие 2 байта, при 127 — 8 байт, минимальная кодировка обязательна; masking-key 4 байта; разд. 5.3: ключ маски — случайные 32 бита, байт i XOR байт ключа i mod 4, маска не меняет длину; разд. 5.5: управляющие кадры до 125 байт, не фрагментируются; Close с 2-байтовым кодом статуса; на Ping обязателен Pong, Ping служит keepalive; разд. 5.6: текстовое сообщение целиком должно быть корректным UTF-8; разд. 10.3: маскирование защищает прокси от отравления кеша
- Espressif. ESP-Protocols documentation, latest: ESP WebSocket Client (ESP32) — клиент WebSocket поверх TCP и TLS с mbedtls, несколько экземпляров; схемы ws (порт 80 по умолчанию) и wss (443); минимальная конфигурация esp_websocket_client_config_t с полем uri, поддержка пути и запроса в URI, поле port переопределяет порт из URI; для проверки сервера нужен сертификат в cert_pem, без него TLS-соединение не проверяет сервер
- Zephyr Project. Zephyr Project Documentation, latest: Networking APIs — Websocket Client API — сначала создаётся и подключается обычный сокет SOCK_STREAM/IPPROTO_TCP, затем websocket_connect(sock, &config, timeout, user_data) возвращает сокет WebSocket; websocket_send_msg с WEBSOCKET_OPCODE_DATA_BINARY, через обычный send() — только текст; опции CONFIG_WEBSOCKET_CLIENT и CONFIG_MQTT_LIB_WEBSOCKET для MQTT поверх WebSocket
Нашли ошибку? ·
Смотрите также
- TCPНадёжный поток байтов поверх IP: рукопожатие, порты, подтверждения, keep-alive
- TLSШифрованный канал поверх TCP: рукопожатие, сертификаты, буферы и время на МК
- MQTTПротокол издатель — подписчик через брокер: топики, QoS, retain, Last Will
- Base64Запись двоичных данных 64 печатными символами: алфавит, группы по 3 байта, дополнение «=», base64url
- Криптографическая хеш-функция (SHA)Функция, сжимающая сообщение в дайджест, к которому нельзя подобрать другое сообщение
- Keep-alive TCPЗонды молчащего соединения TCP: по умолчанию выключены, интервал не меньше 2 ч; настройка через опции сокета lwIP
- ESP32ESP32, S2, S3, C2, C3, C5, C6, H2, P4: ядра, радио, USB, GPIO — сравнение и выбор
- Реализация протокола обменаКадры по паузе, длине и разделителю, COBS, CRC-16, таймауты, разбор потока