---
title: "HTTP против WebSocket: модель соединения"
description: "Websocket vs http: рукопожатие с Sec-WebSocket-Key, устройство кадра, маскирование, состояния и close-коды. Таблица сравнения моделей и код расчёта Accept."
course: "WebSocket в Go: real-time чаты и уведомления"
url: "https://golangmaster.ru/courses/websocket/lessons/02-http-vs-websocket/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

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

*Курс «WebSocket в Go: real-time чаты и уведомления», урок 2 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/websocket/lessons/02-http-vs-websocket/*

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

У HTTP и WebSocket много общего, и это не случайность: второй вырос из первого и стартует HTTP-запросом. Полезная метафора — почта против телефонной линии. HTTP — открытки: каждое сообщение самодостаточно, адрес и обратный адрес написаны на каждом конверте, а получатель после прочтения ничего не обязан помнить. Отсюда stateless — сервер спокойно забывает клиента между запросами, и любой инстанс за балансировщиком может ответить на любой запрос: горизонтальное масштабирование получается почти бесплатным. WebSocket — линия: представились один раз при соединении, а дальше говорят, когда хотят, и память о разговоре принадлежит этому конкретному соединению. Обратная сторона — stateful: клиент прибит к тому серверу, который принял рукопожатие, и перенести разговор на другой инстанс нельзя — отсюда позже в курсе появится мост между инстансами через Redis из курса про [Redis на Go](/courses/go-redis/). Оба протокола едут по одному и тому же 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

```go
key := "dGhlIHNhbXBsZSBub25jZQ=="

h := sha1.New()
h.Write([]byte(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
fmt.Println(base64.StdEncoding.EncodeToString(h.Sum(nil)))
```

**Что выведет программа?**

- ✅ s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- dGhlIHNhbXBsZSBub25jZQ==
- 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
- пустая строка: SHA-1 от константы равен нулю

Это контрольный вектор из RFC 6455: SHA-1 от строки-ключа и GUID, закодированный в base64, даёт ровно такой Accept — сервер вернул бы его в ответе 101. Остальные варианты — исходный ключ, сам GUID и бессмысленная пустая строка.

### 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: заметить рефлексы — значит избежать класса багов, который не ловится тестами на едином инстансе.

> **wss на проде — не пункт для дискуссий.** Открытый 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/2 не отменяет WebSocket.** Классическое рукопожатие — это HTTP/1.1: заголовки Upgrade и Connection — hop-by-hop механика, которой у HTTP/2 нет. Для HTTP/2 существует RFC 8441 с расширенным CONNECT, но поддержка в браузерах и прокси неровная. Практический вывод: планируйте WebSocket как HTTP/1.1-рукопожатие — так его и живёт почти весь real-time-мир.

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

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

- Sec-WebSocket-Version: 13
- Connection: Upgrade
- ✅ Authorization с токеном доступа
- Sec-WebSocket-Key

Обязательный минимум — Upgrade, Connection, Sec-WebSocket-Key и версия 13; браузеры добавляют Origin. Заголовки авторизации в рукопожатие не встроить: браузерного API для кастомных заголовков у WebSocket нет — токен передают в query или cookie, этому посвящён урок про авторизацию соединений.

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

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

- Ни одного: маскируют только серверные кадры
- ✅ 4 байта — обязательное маскирование клиентских кадров
- 16 байт — размер Sec-WebSocket-Key
- Зависит от длины сообщения

RFC 6455 требует маскировать все кадры от клиента: четыре случайных байта-ключа едут в заголовке кадра, нагрузка складывается с ними побайтово. Цель — историческая: предсказуемый клиентский трафик позволял злонамеренному скрипту отравлять кэши прокси. Серверные кадры ходят без маски.

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

```go
const socket = new WebSocket("ws://localhost:8080/ws");
socket.onclose = (e) => console.log(e.code);
// сервер недоступен: рукопожатие не состоялось
```

**Что выведет программа?**

- ✅ 1006 — аномальное закрытие, по сети не передаётся
- 1000 — нормальное закрытие
- 1011 — внутренняя ошибка сервера
- ничего: onclose при провале рукопожатия не вызывается

Код 1006 в спецификации запрещено передавать кадрами — это локальная метка браузера: соединение кончилось без close-рукопожатия. Нормальное закрытие — 1000, сервер уходит на рестарт — 1001, кадр слишком велик — 1009, ошибка на сервере — 1011. onclose срабатывает при любом исходе, даже когда рукопожатие провалилось.

### Состояния соединения и вежливое прощание

У соединения четыре состояния, которые браузер показывает в readyState: CONNECTING — рукопожатие в полёте; OPEN — кадры ходят; CLOSING — идёт прощание; CLOSED — всё кончено. Прощание по правилам выглядит как рукопожатие наоборот: одна сторона шлёт Close-кадр с кодом и причиной, вторая отвечает тем же — и только тогда сокет закрывается. Коды стандартизованы: 1000 — нормальное закрытие, 1001 — сервер или страница уходят, 1008 — нарушение политики, например не прошёл Origin, 1009 — кадр оказался слишком большим, 1011 — сервер словил внутреннюю ошибку. Отдельно живёт 1006: этот код по сети не передаётся никогда — это локальная метка обрыва, когда close-рукопожатия не было вовсе. Жёсткая правда real-time: при выдёргивании Wi-Fi ни одной стороне close не приедет, и соединение будет числиться открытым, пока кто-то не попробует писать или не проверит соединение ping-кадром — механизм heartbeat и дедлайнов разберём в шестом уроке.

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

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

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

Подсказка: Апгрейд начинается с GET; в заголовке Upgrade называется сам протокол, Connection несёт токен Upgrade, а живая версия RFC 6455 — тринадцатая.

Правильные ответы в пропусках: `GET`, `websocket`, `Upgrade`, `13`

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

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

Правильный порядок строк:

1. `key := r.Header.Get("Sec-WebSocket-Key")`
2. `h := sha1.New()`
3. `h.Write([]byte(key + magicGUID))`
4. `accept := base64.StdEncoding.EncodeToString(h.Sum(nil))`
5. `w.Header().Set("Sec-WebSocket-Accept", accept)`
6. `// соединение переключено на кадры WebSocket`

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

- **Рукопожатие** — HTTP-обмен, которым соединение переезжает на WebSocket: GET с Upgrade, Connection, Sec-WebSocket-Key и версией 13; ответ — 101 с Sec-WebSocket-Accept.
- **Sec-WebSocket-Accept** — base64 от SHA-1 ключа клиента и константы 258EAFA5-E914-47DA-95CA-C5AB0DC85B11. Доказывает браузеру, что отвечает настоящий WS-сервер, а не кэш или прокси.
- **Маскирование** — Четыре случайных байта в каждом клиентском кадре: нагрузка складывается с ключом по модулю. Серверные кадры не маскируются; библиотека делает всё сама.
- **FIN-бит** — Признак последнего фрагмента: длинное сообщение может ехать кусками, библиотека собирает фрагменты в одно ReadMessage с сохранёнными границами.
- **Close-кадр** — Управляющий кадр прощания с кодом: 1000 норма, 1001 уход, 1008 нарушение политики, 1009 кадр велик, 1011 ошибка сервера. 1006 по сети не ходит — локальная метка обрыва.
- **readyState** — Состояние сокета в браузере: 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED. Механику состояний в JS разберём в пятом уроке.

Что с этим знанием делать дальше: в [третьем уроке](/courses/websocket/lessons/03-ustanovka-gorilla/) установка gorilla/websocket покажет, что всё описанное рукопожатие умещается в один вызов Upgrade, а в [четвёртом](/courses/websocket/lessons/04-pervyj-server/) мы соберём эхо-сервер целиком. Напоминание про запуск: этот урок — про чтение протокола, код здесь не запускается даже локально, потому что живое рукопожатие требует сервера и клиента одновременно; проверять его мы начнём с пятого урока, где браузер встречается с Go. Если хочется углубить фундамент под Hijack и горутинами, держите под рукой [продвинутый курс Go](/courses/go-advanced/) — а карта всех 25 уроков лежит на [странице курса](/courses/websocket/). Таблица шести различий из этого урока — хороший чек-лист перед выбором протокола в новом проекте.

## Коротко: что запомнить

- Рукопожатие — обычный 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 на сервер-сервер трафик не распространяется.
