Содержание урока · 9 разделов
- Рукопожатие: как HTTP-запрос становится WS
- WebSocket vs HTTP: таблица шести различий
- HTTP против WebSocket: модель соединения
- Типичная ошибка: переносить привычки HTTP на WS
- Кадры: что летит по проводу после 101
- Состояния соединения и вежливое прощание
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
У HTTP и WebSocket много общего, и это не случайность: второй вырос из первого и стартует HTTP-запросом. Полезная метафора — почта против телефонной линии. HTTP — открытки: каждое сообщение самодостаточно, адрес и обратный адрес написаны на каждом конверте, а получатель после прочтения ничего не обязан помнить. Отсюда stateless — сервер спокойно забывает клиента между запросами, и любой инстанс за балансировщиком может ответить на любой запрос: горизонтальное масштабирование получается почти бесплатным. WebSocket — линия: представились один раз при соединении, а дальше говорят, когда хотят, и память о разговоре принадлежит этому конкретному соединению. Обратная сторона — stateful: клиент прибит к тому серверу, который принял рукопожатие, и перенести разговор на другой инстанс нельзя — отсюда позже в курсе появится мост между инстансами через Redis из курса про Redis на Go. Оба протокола едут по одному и тому же TCP, различие — в дисциплине разговора. Теперь к деталям: как линия устанавливается.
Рукопожатие: как HTTP-запрос становится WS#
Апгрейд начинается с самого обычного GET. Браузер шлёт запрос с четырьмя характерными заголовками: Upgrade: websocket — хочу другой протокол; Connection: Upgrade — это относится к текущему соединению, а не ко всему миру; Sec-WebSocket-Key — шестнадцать случайных байт, закодированных в base64; Sec-WebSocket-Version: 13 — единственная живая версия протокола. Веб-страница добавляет ещё Origin — адрес, откуда пришла страница, он станет основой безопасности в уроках про origins. Сервер, согласный на апгрейд, отвечает статусом 101 Switching Protocols и заголовком Sec-WebSocket-Accept — значением, вычисленным из клиентского ключа и жёстко заданной константы 258EAFA5-E914-47DA-95CA-C5AB0DC85B11. Зачем эта арифметика? Браузер должен убедиться, что отвечает настоящий WebSocket-сервер, а не кэш или глупый прокси, который решил, что 101 — это ошибка, и подставил свой ответ. С этой секунды соединение выведено из-под HTTP: gorilla забирает сокет через Hijack, и по проводам идут уже кадры.
Рукопожатие целиком, без библиотек
GO// что шлёт браузер при new WebSocket("ws://localhost:8080/ws")
GET /ws HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://localhost:3000
// что отвечает согласный сервер
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Ни одного лишнего слова: GET с четырьмя обязательными заголовками, ответ 101 с одним вычисляемым. Ключ в примере — контрольный вектор из RFC 6455, это base64 от шестнадцати байт; Accept сервер считает из ключа и константы-«соли», как именно — в коде ниже. Пока заметьте Origin: браузер прикладывает его всегда, и на нём будет держаться вся проверка источника в уроке про междоменные соединения.
Считаем Sec-WebSocket-Accept руками
GOkey := "dGhlIHNhbXBsZSBub25jZQ=="
h := sha1.New()
h.Write([]byte(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
accept := base64.StdEncoding.EncodeToString(h.Sum(nil))
fmt.Println(accept)Вся магия Accept — SHA-1 от ключа клиента и GUID-константы, переведённый в base64. GUID — не секрет, а опознавательный знак: его задача — чтобы случайный HTTP-сервер не выдал себя за WebSocket. Приложения этот код не пишут никогда — Upgrader делает то же самое внутри; мы его разобрали, потому что понимание рукопожатия окупается при отладке за прокси и корпоративными файрволами. Что напечатает Println — проверьте в интерактиве ниже.
Что напечатает расчёт Accept
Прочитайте код и выберите, что он выведет:
key := "dGhlIHNhbXBsZSBub25jZQ=="
h := sha1.New()
h.Write([]byte(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
fmt.Println(base64.StdEncoding.EncodeToString(h.Sum(nil)))WebSocket vs HTTP: таблица шести различий#
По чему вообще честно сравнивать websocket vs http? По шести пунктам, каждый из которых влияет на архитектуру. Кто инициирует передачу: в HTTP всегда клиент, в WebSocket — обе стороны, и это главное различие, из которого растут остальные. Что стоит над каждым сообщением: HTTP тащит стартовую строку и заголовки — с cookies легко набегают сотни байт на каждый чих, кадр WebSocket несёт 2-14 байт служебных. Насколько быстро доходит событие: в HTTP — до следующего запроса клиента, в WebSocket — сразу, потому что соединение уже открыто. Что помнит сервер: HTTP stateless, WebSocket stateful — соединение живо и контекст при нём. Как относятся инфраструктура и кэши: HTTP — родная среда для CDN, прокси и ретраев, WebSocket сквозь промежуточные узлы проходит не всегда и кэшу не подлежит. Наконец, масштабирование: HTTP-клиенту всё равно, какой инстанс ответит, WS-клиент прибит к своему — и мост между инстансами становится отдельной задачей.
HTTP против WebSocket: модель соединения#
| Критерий | HTTP | WebSocket |
|---|---|---|
| Модель | запрос-ответ, клиент всегда начинает | постоянный дуплекс, говорят обе стороны |
| Служебный расход на сообщение | стартовая строка и заголовки, сотни байт | 2-14 байт служебных в кадре |
| Задержка доставки события | до следующего запроса клиента | миллисекунды: сервер шлёт сразу |
| Состояние | stateless: запрос забыт после ответа | stateful: соединение живёт и помнит контекст |
| Прокси, кэши, CDN | зрелая экосистема: кэширование, ретраи | сквозь прокси проходит не всегда, кэша нет |
| Масштабирование | любой инстанс ответит на любой запрос | клиент прибит к инстансу: нужен мост между ними |
Типичная ошибка: переносить привычки HTTP на WS#
Годами работая с HTTP, команда накапливает рефлексы, и половина из них на WebSocket не просто бесполезна, а вредна. Рефлекс «повторю запрос при ошибке» на WS выглядит как переподключение — но соединение-то одно, и без backoff ретрай превращается в шторм reconnect-ов по всему серверу. Рефлекс «кэшируй ответ» не работает вовсе: кадры не кэшируются по определению, и попытки вложить HTTP-кэш между клиентом и сокетом заканчиваются прокси, который режет апгрейд. Рефлекс «серверы одинаковы» ломается о stateful: после рукопожатия клиент прирос к своему инстансу, и балансировщик больше не волен его переносить — отсюда sticky-сессии или мост между инстансами из плана курса. Наконец, рефлекс «ошибки — это статусы» меняет форму: протокол не приносит вам 500-е, соединение просто рвётся или уходит кодом закрытия, и логика ошибок переезжает в уровень сообщений. Ошибкой здесь не знание WS, а память HTTP: заметить рефлексы — значит избежать класса багов, который не ловится тестами на едином инстансе.
Открытый ws:// означает, что кадры читает каждый узел на пути: офисный роутер, провайдер, прокси. На проде только wss:// — тот же протокол внутри TLS, обычно порт 443. Отдельная ловушка — mixed content: страница, открытая по https, не откроет ws:// ни при каком желании, браузер блокирует. Локальная разработка — единственное место, где ws:// уместен.
Кадры: что летит по проводу после 101#
Кадр WebSocket — короткий заголовок и полезная нагрузка. В начале идут мелкие поля: FIN-бит — признак последнего фрагмента сообщения; opcode — тип содержимого: 0x1 текст, 0x2 бинарные байты, а управляющие типы 0x8, 0x9 и 0xA — close, ping и pong; маска-бит и длина нагрузки, которая кодируется семью битами, а при переполнении — ещё двумя или восемью байтами. Отсюда и вилка 2-14 байт служебных. У клиентских кадров к заголовку добавляются четыре байта маски, которой закрывается нагрузка; серверные кадры ходят открыто. Цель маскирования — историческая защита от отравления кэшей недобросовестными прокси, и библиотека делает её сама. Большое сообщение может приехать фрагментами с FIN-битом на последнем куске — библиотека собирает фрагменты и отдаёт одним ReadMessage, границы сообщений сохраняются всегда. Управляющие кадры не могут быть длиннее 125 байт нагрузки. Итог: поверх потока TCP появляется конвертация в сообщения — именно поэтому ReadMessage возвращает готовое сообщение, а не куски байтов.
Классическое рукопожатие — это HTTP/1.1: заголовки Upgrade и Connection — hop-by-hop механика, которой у HTTP/2 нет. Для HTTP/2 существует RFC 8441 с расширенным CONNECT, но поддержка в браузерах и прокси неровная. Практический вывод: планируйте WebSocket как HTTP/1.1-рукопожатие — так его и живёт почти весь real-time-мир.
Проверка: заголовки рукопожатия
Какой заголовок браузер НЕ обязан отправлять в рукопожатии?
Проверка: маскирование
Сколько байт маски добавляется к каждому кадру от клиента к серверу?
Какой код закрытия придёт
Прочитайте код и выберите, что он выведет:
const socket = new WebSocket("ws://localhost:8080/ws");
socket.onclose = (e) => console.log(e.code);
// сервер недоступен: рукопожатие не состоялосьСостояния соединения и вежливое прощание#
У соединения четыре состояния, которые браузер показывает в readyState: CONNECTING — рукопожатие в полёте; OPEN — кадры ходят; CLOSING — идёт прощание; CLOSED — всё кончено. Прощание по правилам выглядит как рукопожатие наоборот: одна сторона шлёт Close-кадр с кодом и причиной, вторая отвечает тем же — и только тогда сокет закрывается. Коды стандартизованы: 1000 — нормальное закрытие, 1001 — сервер или страница уходят, 1008 — нарушение политики, например не прошёл Origin, 1009 — кадр оказался слишком большим, 1011 — сервер словил внутреннюю ошибку. Отдельно живёт 1006: этот код по сети не передаётся никогда — это локальная метка обрыва, когда close-рукопожатия не было вовсе. Жёсткая правда real-time: при выдёргивании Wi-Fi ни одной стороне close не приедет, и соединение будет числиться открытым, пока кто-то не попробует писать или не проверит соединение ping-кадром — механизм heartbeat и дедлайнов разберём в шестом уроке.
Допишите рукопожатие
Заполните четыре пропуска: метод запроса, имя протокола, токен соединения и версию.
Соберите расчёт Accept по шагам
Расставьте строки в порядке выполнения: от ключа клиента до готового заголовка ответа.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: протокол изнутри
Что с этим знанием делать дальше: в третьем уроке установка gorilla/websocket покажет, что всё описанное рукопожатие умещается в один вызов Upgrade, а в четвёртом мы соберём эхо-сервер целиком. Напоминание про запуск: этот урок — про чтение протокола, код здесь не запускается даже локально, потому что живое рукопожатие требует сервера и клиента одновременно; проверять его мы начнём с пятого урока, где браузер встречается с Go. Если хочется углубить фундамент под Hijack и горутинами, держите под рукой продвинутый курс Go — а карта всех 25 уроков лежит на странице курса. Таблица шести различий из этого урока — хороший чек-лист перед выбором протокола в новом проекте.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Рукопожатие — обычный HTTP GET с Upgrade, Connection, Sec-WebSocket-Key и версией 13; ответ — 101 с Sec-WebSocket-Accept.
- Accept — base64 от SHA-1 ключа клиента и GUID-константы: браузер убеждается, что отвечает настоящий WS-сервер.
- Кадр несёт opcode, длину и, у клиента, 4-байтовую маску; границы сообщений сохраняются, фрагменты собирает библиотека.
- Close-кадр закрывает соединение вежливо; 1006 — локальная метка обрыва и по сети не передаётся.
- HTTP stateless и дружит с кэшами и CDN, WS stateful и прибит к инстансу — отсюда мосты вроде Redis между серверами.
- На проде только wss://: открытый ws читают все узлы пути, а https-страница ws:// не откроет из-за mixed content.
Домашнее задание
Откройте любой сайт с живым чатом, вкладку Network в DevTools и фильтр WS — найдите реальное рукопожатие: Sec-WebSocket-Key в запросе и Sec-WebSocket-Accept в ответе. Затем пересчитайте на бумажке служебный расход: сколько байт заголовков уносит один polling-запрос с вашими cookies против 2-14 байт кадра WebSocket.
Личные заметкиПусто
Вопросы и ответы по уроку
Работает ли WebSocket поверх HTTP/2?
Классическое рукопожатие — это HTTP/1.1: механика Upgrade и Connection в HTTP/2 отсутствует. RFC 8441 описывает туннелирование WS через расширенный CONNECT, но поддержка у браузеров и прокси неровная. На практике WebSocket живёт на HTTP/1.1 — это нормально и ничего не ломает.
Почему кадры от клиента маскируются, а от сервера нет?
Требование RFC 6455 с исторической целью: предсказуемый клиентский трафик позволял злонамеренному скрипту подделывать запросы так, что прокси принимали их за чужие ответы и портили кэши. Маска ломает предсказуемость. Сервер таких рисков не создаёт — его кадры ходят открыто.
Можно ли открыть WS-соединение с сервера на сервер?
Да: gorilla умеет Dialer — исходящее соединение с полноценным рукопожатием, без браузера. Микросервисы так подписываются друг на друга, а в курсе Dialer встретится в тестах против httptest-сервера. Браузерное ограничение Origin на сервер-сервер трафик не распространяется.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
RabbitMQ в Go: очереди сообщений на amqp091-go
Интерактивный курс RabbitMQ в Go: обменники, очереди, ack, prefetch, dead letter и RPC. 25 уроков amqp091-go с примерами кода и итоговым проектом.
- Подключаться к RabbitMQ из Go и управлять connection и channel
- Объявлять очереди, обменники и привязки с нужными свойствами
- Публиковать и потреблять сообщения с ручными ack
- Настраивать prefetch и справедливое распределение между воркерами