Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 2 из 25 ~16 мин +20 XP

HTTP против WebSocket: модель соединения

Спор websocket vs http — это не спор о скорости ради скорости: это два разных способа распорядиться одним TCP-кабелем, и понимание разницы избавит от половины ошибок при проектировании real-time. В прошлом уроке мы договорились, что серверу нужно звонить первым; теперь заглянем под капот — рукопожатие по заголовкам, анатомию кадра, состояния соединения и close-коды, а в конце соберём таблицу, где HTTP и WebSocket стоят рядом по шести пунктам.

Содержание урока · 9 разделов

У 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 руками

GO
key := "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: модель соединения#

КритерийHTTPWebSocket
Модельзапрос-ответ, клиент всегда начинаетпостоянный дуплекс, говорят обе стороны
Служебный расход на сообщениестартовая строка и заголовки, сотни байт2-14 байт служебных в кадре
Задержка доставки событиядо следующего запроса клиентамиллисекунды: сервер шлёт сразу
Состояниеstateless: запрос забыт после ответаstateful: соединение живёт и помнит контекст
Прокси, кэши, CDNзрелая экосистема: кэширование, ретраисквозь прокси проходит не всегда, кэша нет
Масштабированиелюбой инстанс ответит на любой запросклиент прибит к инстансу: нужен мост между ними

Типичная ошибка: переносить привычки HTTP на WS#

Годами работая с HTTP, команда накапливает рефлексы, и половина из них на WebSocket не просто бесполезна, а вредна. Рефлекс «повторю запрос при ошибке» на WS выглядит как переподключение — но соединение-то одно, и без backoff ретрай превращается в шторм reconnect-ов по всему серверу. Рефлекс «кэшируй ответ» не работает вовсе: кадры не кэшируются по определению, и попытки вложить HTTP-кэш между клиентом и сокетом заканчиваются прокси, который режет апгрейд. Рефлекс «серверы одинаковы» ломается о stateful: после рукопожатия клиент прирос к своему инстансу, и балансировщик больше не волен его переносить — отсюда sticky-сессии или мост между инстансами из плана курса. Наконец, рефлекс «ошибки — это статусы» меняет форму: протокол не приносит вам 500-е, соединение просто рвётся или уходит кодом закрытия, и логика ошибок переезжает в уровень сообщений. Ошибкой здесь не знание WS, а память HTTP: заметить рефлексы — значит избежать класса багов, который не ловится тестами на едином инстансе.

Кадры: что летит по проводу после 101#

Кадр WebSocket — короткий заголовок и полезная нагрузка. В начале идут мелкие поля: FIN-бит — признак последнего фрагмента сообщения; opcode — тип содержимого: 0x1 текст, 0x2 бинарные байты, а управляющие типы 0x8, 0x9 и 0xA — close, ping и pong; маска-бит и длина нагрузки, которая кодируется семью битами, а при переполнении — ещё двумя или восемью байтами. Отсюда и вилка 2-14 байт служебных. У клиентских кадров к заголовку добавляются четыре байта маски, которой закрывается нагрузка; серверные кадры ходят открыто. Цель маскирования — историческая защита от отравления кэшей недобросовестными прокси, и библиотека делает её сама. Большое сообщение может приехать фрагментами с FIN-битом на последнем куске — библиотека собирает фрагменты и отдаёт одним ReadMessage, границы сообщений сохраняются всегда. Управляющие кадры не могут быть длиннее 125 байт нагрузки. Итог: поверх потока TCP появляется конвертация в сообщения — именно поэтому ReadMessage возвращает готовое сообщение, а не куски байтов.

Проверка: заголовки рукопожатия

Какой заголовок браузер НЕ обязан отправлять в рукопожатии?

Проверка: маскирование

Сколько байт маски добавляется к каждому кадру от клиента к серверу?

Какой код закрытия придёт

Прочитайте код и выберите, что он выведет:

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 и дедлайнов разберём в шестом уроке.

Допишите рукопожатие

Заполните четыре пропуска: метод запроса, имя протокола, токен соединения и версию.

/ws HTTP/1.1 Host: localhost:8080 Upgrade: Connection: Sec-WebSocket-Version: Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Соберите расчёт Accept по шагам

Расставьте строки в порядке выполнения: от ключа клиента до готового заголовка ответа.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: протокол изнутри

Термин

Рукопожатие

нажмите, чтобы перевернуть
Определение

HTTP-обмен, которым соединение переезжает на WebSocket: GET с Upgrade, Connection, Sec-WebSocket-Key и версией 13; ответ — 101 с Sec-WebSocket-Accept.

1 / 6

Что с этим знанием делать дальше: в третьем уроке установка 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 против 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 на сервер-сервер трафик не распространяется.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

RabbitMQ25 уроков~11 чпрактика25 уроков уже доступны

RabbitMQ в Go: очереди сообщений на amqp091-go

Интерактивный курс RabbitMQ в Go: обменники, очереди, ack, prefetch, dead letter и RPC. 25 уроков amqp091-go с примерами кода и итоговым проектом.

  • Подключаться к RabbitMQ из Go и управлять connection и channel
  • Объявлять очереди, обменники и привязки с нужными свойствами
  • Публиковать и потреблять сообщения с ручными ack
  • Настраивать prefetch и справедливое распределение между воркерами