Содержание урока · 13 разделов
- gin websocket: зачем протокол поверх HTTP
- Рукопожатие: как HTTP превращается в WebSocket
- Апгрейд изнутри: Hijack и чужие обёртки
- Кадры и типы сообщений
- Типы сообщений gorilla/websocket
- Heartbeat: ping/pong и дедлайны
- CheckOrigin: чья страница разговаривает
- Хаб: от эха к чату
- Грабли: прокси, таймауты и молчащие соединения
- Шесть правил WebSocket на gin
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
gin websocket: зачем протокол поверх HTTP#
HTTP построен как вопрос-ответ: клиент спросил — сервер ответил, и на этом транзакция закончена. Для чата, игры, совместного редактирования и живых дашбордов эта модель неудобна: серверу нужно самому сказать клиенту «случилось событие», а не ждать, пока тот спросит. Опрос раз в секунду (polling) жжёт соединения и запаздывает; SSE из прошлого урока решает половину задачи — сервер шлёт, но клиент только слушает. WebSocket закрывает вторую половину: после рукопожатия обе стороны шлют кадры в любой момент, в обе стороны, с сохранением границ сообщений. Ценой становится сам протокол: это уже не HTTP, его не понимают старые прокси, gzip-звенья и половина инфраструктуры, заточенной под запрос-ответ. Выбор честный: уведомления и ленты — SSE на чистом HTTP; чат, игры, двусторонние редакторы, бинарные кадры — WebSocket. В экосистеме websocket golang-стандартом де-факто стала gorilla/websocket: проект в 2022 году ставился на паузу, но в 2023-м снова живёт под крылом организации gorilla, и его API десятилетиями был образцом стабильности. gin собственных WebSocket-средств не имеет — и не нужно: gin отдаёт вам c.Writer и c.Request, а дальше работает стандартный механизм апгрейда net/http. Именно так мы и сделаем.
Рукопожатие: как HTTP превращается в WebSocket#
Апгрейд начинается как обычный HTTP-запрос. Браузер, увидев new WebSocket("ws://localhost:8080/ws"), шлёт GET с тремя характерными заголовками: Upgrade: websocket (хочу другой протокол), Connection: Upgrade (это относится к текущему соединению, hop-by-hop, как мы разбирали в уроке 19), Sec-WebSocket-Key — случайные 16 байт в base64, и Sec-WebSocket-Version: 13. Сервер, согласный на апгрейд, отвечает статусом 101 Switching Protocols и заголовком Sec-WebSocket-Accept — это base64 от SHA-1-хэша клиентского ключа, склеенного с магической константой 258EAFA5-E914-47DA-95CA-C5AB0DC85B11. Зачем эта арифметика? Браузер убеждается, что ответ пришёл от настоящего WebSocket-сервера, а не от кэша или тупого прокси, который решил, что 101 — это ошибка. С этого момента тот же TCP-сокет несёт уже не HTTP, а кадры WebSocket. Обратите внимание на схему: ws:// — открытый канал, wss:// — тот же протокол внутри TLS; в проде всегда wss, иначе кадры читает каждый роутер на пути. И ещё: браузер всегда прикладывает заголовок Origin — откуда именно пришла страница. Его проверка станет главным вопросом безопасности во второй половине урока.
Эхо-сервер: минимум на gin и gorilla
GOpackage main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return r.Host == "localhost:8080"
},
}
func main() {
r := gin.Default()
r.GET("/ws", func(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Println("апгрейд не удался:", err)
return
}
defer conn.Close()
for {
mt, msg, err := conn.ReadMessage()
if err != nil {
break // клиент ушёл — соединение закрыто
}
if err := conn.WriteMessage(mt, msg); err != nil {
break
}
}
})
if err := r.Run(":8080"); err != nil {
panic(err)
}
}Ставим зависимость командой go get github.com/gorilla/websocket и получаем пару Writer/Request из gin-контекста — больше от фреймворка ничего не нужно. Upgrade возвращает *websocket.Conn и ошибку; при ошибке он уже сам ответил клиенту HTTP-кодом, писать второй ответ нельзя. Дальше горутина хендлера превращается в read-цикл: ReadMessage блокируется до прихода кадра, WriteMessage возвращает кадр обратно тем же типом. Каждый подключившийся клиент получает свою горутину — это обычная модель net/http, ничего нового.
Проверка: обычный GET вместо апгрейда
Клиент (не браузер) отправил на /ws самый обычный GET без заголовков Upgrade и Connection. Что произойдёт?
Апгрейд изнутри: Hijack и чужие обёртки#
Что физически делает Upgrade? Он вызывает Hijack() у ResponseWriter: соединение вынимается из-под управления net/http — сервер больше не знает про этот сокет, не пишет ему свои заголовки и не ведёт учёт времени. Дальше библиотека вручную пишет в сокет строку «HTTP/1.1 101 Switching Protocols» с Accept-заголовком, и канал переходит к кадрам. Для gin это работает из коробки, потому что штатный gin-писатель реализует http.Hijacker — мы в этом убедимся в следующем уроке, а исходники response_writer.go подтвердят. Но обёртки поверх писателя — нет: gzip-звено с собственной буферизацией, звено, считающее байты, любая middleware, которая подсунула в цепочку свой ResponseWriter без метода Hijack, — и Upgrade упадёт с ошибкой «websocket: response does not implement http.Hijacker». Лечение — роутить WS-маршруты мимо таких звеньев, как мы обходили gzip для SSE. Третий аргумент Upgrade — responseHeader: заголовки, которые хочется добавить в ответ 101 до его отправки, например cookie. Там же договариваются о субпротоколе. Полезное поле структуры Upgrader — HandshakeTimeout: дедлайн на фазу рукопожатия, чтобы зависший клиент не держал соединение вечно. И помните про модель исполнения: горутина хендлера после апгрейда принадлежит уже не HTTP-миру — она живёт, пока вы сами не выйдете из цикла чтения.
Кадры и типы сообщений#
WebSocket делит всё, что летит по соединению, на кадры. Данных два типа: TextMessage — текст в UTF-8, и BinaryMessage — байты как есть, без единой перекодировки. Отдельно живут управляющие кадры: Ping, Pong и Close — они маленькие, не могут быть фрагментированы и обслуживаются протоколом почти без вашего участия. Два отличия от сырого TCP, которые спасают нервы. Первое — границы: ReadMessage возвращает ровно то сообщение, которое отправил отправитель; длинный JSON не приедет половинками, как в TCP-потоке, склеивать куски не нужно. Если отправитель разбил большое сообщение на фрагменты — библиотека соберёт их за вас и отдаст одним куском. Второе — маскирование: каждый кадр от клиента к серверу обязан быть замаскирован четырьмя случайными байтами-ключом (защита от отравления кэшей промежуточными прокси, наследие идеи из безопасности HTTP), кадры от сервера идут без маски. Вы этого не увидите в коде — библиотека маскирует и распаковывает сама, — но понимать стоит: поэтому клиентские кадры чуть больше на 4 байта, а сервер никогда не должен принимать немаскированные кадры — gorilla это отсекает на уровне протокола.
Типы сообщений gorilla/websocket#
| Константа | Значение | Роль |
|---|---|---|
| websocket.TextMessage | 1 | текст UTF-8: чат, JSON, события |
| websocket.BinaryMessage | 2 | байты как есть: файлы, protobuf, медиа |
| websocket.CloseMessage | 8 | рукопожатие закрытия: код и причина |
| websocket.PingMessage | 9 | проверка живости — получатель обязан ответить pong |
| websocket.PongMessage | 10 | ответ на ping и повод продлить дедлайн чтения |
Проверка: что за int в ReadMessage
mt, msg, err := conn.ReadMessage(). Что лежит в mt и почему его нельзя игнорировать?
Heartbeat: ping/pong и дедлайны#
Соединение WebSocket может умереть незаметно: пользователь ушёл в лифт, мобильный оператор съел NAT-таблицу, ноутбук уснул. TCP этого не заметит — пакеты просто никуда не идут, и read-цикл будет вечно ждать кадр, который никогда не придёт. Лечится heartbeat-механикой из трёх констант. pongWait — сколько сервер готов ждать признаков жизни от клиента, обычно 60 секунд. pingPeriod — как часто сервер сам шлёт ping, и правило одно: чуть меньше pongWait, классика — девять десятых, чтобы пинг успел добежать до самого дедлайна. Механика: перед каждым ожиданием ставим SetReadDeadline(now + pongWait); таймер-тикер каждые 54 секунды шлёт кадр Ping через WriteControl; клиент (браузер делает это автоматически, без вашего JS) отвечает Pong; pong приезжает в read-цикл, там срабатывает SetPongHandler, и в нём мы продлеваем ReadDeadline. Если клиент мёртв — ни pong, ни кадров, дедлайн истекает, ReadMessage возвращает ошибку, соединение закрывается. Тонкость параллельности: писать Ping из отдельной горутины тикера разрешено, потому что WriteControl — единственный метод записи, который gorilla официально разрешает звать параллельно с остальными. А вот про продление дедлайна без чтения мечтать зря: пинг-, понг- и клоуз-кадры обрабатываются только внутри чтения — read-цикл обязан жить, иначе весь heartbeat мёртв. Лишний бонус: SetReadLimit обрезает обжор — кадр больше лимита, и соединение закрывается с кодом 1009 Message Too Big.
Эхо с heartbeat: боевой скелет
GOconst (
writeWait = 10 * time.Second // дедлайн на одну запись
pongWait = 60 * time.Second // сколько ждём признаков жизни
pingPeriod = (pongWait * 9) / 10 // пинг чуть раньше дедлайна
)
func echo(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return // Upgrade уже ответил клиенту ошибкой
}
defer conn.Close()
conn.SetReadLimit(4096) // обжора получает разрыв с кодом 1009
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPongHandler(func(string) error {
return conn.SetReadDeadline(time.Now().Add(pongWait))
})
go func() {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for range ticker.C {
if err := conn.WriteControl(websocket.PingMessage,
nil, time.Now().Add(writeWait)); err != nil {
return // соединение мертво — гасим тикер
}
}
}()
for {
mt, msg, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err,
websocket.CloseNormalClosure,
websocket.CloseGoingAway) {
log.Println("неожиданный обрыв:", err)
}
return
}
conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := conn.WriteMessage(mt, msg); err != nil {
return
}
}
}Три константы связаны формулой: pingPeriod — девять десятых pongWait. PongHandler продлевает ReadDeadline — без него дедлайн истечёт даже у живого клиента. WriteControl шлёт ping из горутины тикера параллельно с read-циклом — это единственный параллельно-безопасный метод записи. При обрыве IsUnexpectedCloseError отделяет штатное закрытие (клиент ушёл со страницей, код 1000/1001) от аварийного — шуметь в логах стоит только о втором. Каждый WriteMessage получает свежий WriteDeadline: застрять в записи навсегда так же опасно, как и в чтении.
Что вернёт ReadMessage на обжоре
Прочитайте код и выберите, что он выведет:
conn.SetReadLimit(64)
// клиент отправил одно текстовое сообщение на 100 байт
mt, msg, err := conn.ReadMessage()CheckOrigin: чья страница разговаривает#
Браузер прикладывает к рукопожатию заголовок Origin — адрес страницы, где работает ваш JS. Дефолтный CheckOrigin в gorilla строг: он разрешает рукопожатие, только если хост из Origin совпадает с хостом запроса — иначе Upgrade отвечает 403 Forbidden. Зачем? Соединение WebSocket — это не fetch: заголовки CORS на него не действуют, а cookie на домен браузер приложит сам. Без проверки Origin любой сайт в интернете мог бы открыть ws://your-api и говорить с вашим сервером от имени залогиненного пользователя — это классическая cross-site WebSocket hijacking, двоюродная сестра CSRF из урока про CORS. Поэтому return true в демо-примерах — бомба замедленного действия: в бою держите белый список своих фронтенд-доменов и сверяйте Origin с ним. Второй вопрос — аутентификация. Cookie из урока про сессии едут в рукопожатие сами — читайте их из c.Request до апгрейда. Для JWT из урока 15 у схемы токена в заголовке проблема: браузерный WebSocket API не позволяет ставить кастомные заголовки, поэтому токен либо в query-параметре (попадает в логи — дорогая цена), либо в субпротоколе, либо вы сначала выдаёте одноразовый короткоживущий тикет обычным REST-запросом, а им апгрейдитесь. И не забывайте про лимиты из урока 18: до апгрейда запрос — обычный HTTP, per-IP ведро сработает как обычно и отрежет поток рукопожатий до того, как они станут горутинами.
Хаб: от эха к чату#
Эхо — разминка. Чат значит fan-out: сообщение от одного клиента должно доехать до остальных. До SSE мы уже собирали хаб каналов с broadcast под мьютексом — для WebSocket конструкция та же, но появляется жёсткое правило gorilla: соединение не терпит параллельной записи. Один читатель и один писатель на conn — максимум; попытка двух горутин вызвать WriteMessage одновременно кончается паникой «concurrent write to websocket connection». Исключения ровно два — Close и WriteControl, они параллельно-безопасны. Поэтому у каждого клиента в хабе есть своя дверь — буферизованный канал send, и единственная горутина-писатель, которая читает из канала и пишет в conn. Все остальные (broadcast, REST-эндпоинт, события домена) кладут байты в канал и не трогают сокет. Буфер канала — это защита от медленного клиента: broadcast с default-веткой, как в уроке 19, просто пропустит сообщение для того, чей буфер переполнен, вместо того чтобы замораживать всех. А читатель живёт прямо в хендлере: ReadMessage, разбор, broadcast. Один читатель (хендлер), один писатель (горутина канала) — правило соблюдено архитектурой, а не дисциплиной.
Хаб клиентов и чат-хендлер
GOtype client struct {
conn *websocket.Conn
send chan []byte // единственная дверь к писателю
}
type hub struct {
mu sync.Mutex
clients map[*client]struct{}
}
func (h *hub) join(cl *client) {
h.mu.Lock()
h.clients[cl] = struct{}{}
h.mu.Unlock()
}
func (h *hub) leave(cl *client) {
h.mu.Lock()
delete(h.clients, cl)
h.mu.Unlock()
}
func (h *hub) broadcast(msg []byte) {
h.mu.Lock()
defer h.mu.Unlock()
for cl := range h.clients {
select {
case cl.send <- msg:
default: // буфер полон — отстающий клиент пропустит
}
}
}
var h = &hub{clients: make(map[*client]struct{})}
r.GET("/ws", func(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer conn.Close()
cl := &client{conn: conn, send: make(chan []byte, 16)}
h.join(cl)
go func() { // писатель: только он касается WriteMessage
defer conn.Close() // ошибка записи будит читателя
for msg := range cl.send {
conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := conn.WriteMessage(
websocket.TextMessage, msg); err != nil {
return
}
}
}()
for { // читатель живёт в хендлере
_, msg, err := conn.ReadMessage()
if err != nil {
h.leave(cl) // сначала убрать из карты
close(cl.send) // теперь в канал не пошлёт никто
return
}
h.broadcast(msg)
}
})Карта клиентов под мьютексом, join при подключении, leave при разрыве, broadcast раскладывает байты всем с default-веткой против переполненного буфера. Писатель-горутина единственная вызывает WriteMessage; её defer conn.Close() — страховка: если запись сломалась, соединение закрывается, ReadMessage читателя разблокируется ошибкой, и обе горутины аккуратно завершаются. Порядок в читателе принципиален: сперва leave под мьютексом, и только потом close(cl.send) — иначе гонка с broadcast, который мог уже взять этот канал, дала бы панику «send on closed channel».
Проверка: параллельная запись
В чате появилась горутина «напоминания», которая раз в минуту зовёт conn.WriteMessage напрямую, минуя канал send, пока писатель раздаёт broadcast. Что будет?
Судьба сообщений у медленного клиента
Прочитайте код и выберите, что он выведет:
// буфер канала клиента — 16
cl := &client{conn: conn, send: make(chan []byte, 16)}
h.join(cl)
// писатель клиента застрял: сеть не двигается, WriteMessage блокирован
// тем временем другой клиент шлёт 20 сообщений подряд через broadcastГрабли: прокси, таймауты и молчащие соединения#
Первый грабль — дорога через прокси. Upgrade и Connection — hop-by-hop заголовки, обычный ReverseProxy из урока 19 их вырезает, и апгрейд не пролезает. Нужен туннель: nginx с версии 1.3.13 умеет превращать соединение в туннель на 101 — конфиг с map $http_upgrade $connection_upgrade, proxy_http_version 1.1 и заголовками Upgrade/Connection мы уже разбирали в уроке про прокси, там же про hop-by-hop. Второй — таймауты http.Server: WriteTimeout или ReadTimeout, поставленные на весь сервер в уроке про деплой, обрежут долгоживущие WS-соединения молча и по расписанию; для WS-сервера их обнуляют, а дедлайны ставят точечно через SetReadDeadline/SetWriteDeadline — heartbeat управляет временем сам. Третий — звенья-обёртки: gzip или подсчёт байт поверх писателя ломают Hijack — WS-группа должна идти мимо них, как шла SSE-группа. Четвёртый — умерший read-цикл: пинг-, понг- и клоуз-кадры обрабатываются только при чтении; перестали читать — heartbeat мёртв, дедлайны не продлеваются, соединение висит полумёртвым. Пятый — забытый defer conn.Close(): горутина хендлера уйдёт, а сокет и его буферы останутся. Шестой — ошибки рукопожатия без диагностики: Upgrade уже ответил клиенту, но в ваш лог ничего не попал — оборачивайте его вызов в лог со статусом и причиной, иначе «фронтенд говорит, WS не работает» останется навсегда.
Шесть правил WebSocket на gin#
- Апгрейд — это upgrader.Upgrade(c.Writer, c.Request, nil): при ошибке клиенту уже ответили, хендлеру остаётся лог и return; писатель gin реализует Hijacker, обёртки middleware — не факт.
- Рукопожатие — стандарт HTTP: Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key, ответ 101 c Sec-WebSocket-Accept = base64(SHA-1(key+GUID)); дальше тот же TCP несёт кадры.
- Один читатель, один писатель на conn: горутина хендлера читает, горутина канала send пишет; WriteControl и Close — единственные параллельно-безопасные исключения.
- Heartbeat обязателен: pongWait 60 секунд, pingPeriod — девять десятых, SetPongHandler продлевает ReadDeadline, каждый WriteMessage — со свежим WriteDeadline.
- CheckOrigin по белому списку: дефолт требует совпадения хоста Origin и хоста запроса, иначе 403; return true в бою — cross-site hijacking; аутентификация — cookie в рукопожатии или одноразовый тикет.
- Дорога и таймауты: nginx-туннель на 101 с proxy_http_version 1.1, WriteTimeout сервера — обнулить для WS-маршрутов, SetReadLimit — против кадра-обжоры с кодом 1009.
Соберите хендлер /ws
Допишите каркас апгрейда и heartbeat: откуда берутся Writer и Request, чем закрываем соединение, что продлевает дедлайн и как выглядит пара чтения и эха.
Соберите жизнь одного WS-сообщения
Пользователь печатает в чат «привет». Разложите путь сообщения: от рукопожатия до раздачи всем и закрытия.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: WebSocket в gin
Поднимите сервер из урока и откройте в браузере консоль: const ws = new WebSocket("ws://localhost:8080/ws"); ws.onmessage = e => console.log(e.data); в двух вкладках — и шлите ws.send("привет") из каждой: эхо и хаб покажут сообщения во обоих окнах. Загляните в Network-вкладку, строка /ws: увидите 101 Switching Protocols и кадры во фреймах — рукопожатие станет наглядным. Второй терминал — для убитых соединений: закройте вкладку и найдите в логе IsUnexpectedCloseError, а с включённым тикером выключите сеть — дедлайн поймает мертвеца за pongWait. В браузерной песочнице сайта сетевой код не исполняется, поэтому интерактивы здесь quiz, predict, fill, order и флешкарты, а примеры живут в локальном go run, как и весь курс gin. Дальше — 21-testirovanie: httptest, PerformRequest и мок-зависимости; там же покажем, что даже WebSocket тестируется без браузера — через httptest.NewServer и Dialer из той же gorilla.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- WebSocket — апгрейд обычного HTTP: GET с Upgrade/Connection/Sec-WebSocket-Key, ответ 101 с Sec-WebSocket-Accept, дальше тот же TCP несёт кадры в обе стороны с сохранением границ сообщений.
- В gin всё начинается с upgrader.Upgrade(c.Writer, c.Request, nil): при ошибке Upgrade сам ответил клиенту и вернул error; gin-писатель реализует Hijacker, а обёртки middleware без Hijack ломают апгрейд.
- Контракт gorilla — один читатель и один писатель на соединение: хаб с каналами send и писатель-горутиной делает правило архитектурным; WriteControl и Close — единственные параллельно-безопасные методы.
- Heartbeat обязателен: pongWait 60 секунд, pingPeriod девять десятых, SetPongHandler продлевает ReadDeadline, WriteDeadline — перед каждой записью; SetReadLimit закрывает обжор кодом 1009.
- CheckOrigin — главная защита: дефолт требует совпадения хоста Origin и хоста запроса, иначе 403; аутентификация — cookie в рукопожатии или одноразовый тикет, JWT-заголовок браузерный API не умеет.
- Инфраструктура: nginx-туннель на 101 с proxy_http_version 1.1 (разбор в уроке 19), WriteTimeout сервера обнуляется для WS-маршрутов, gzip-звенья обходятся, read-цикл обязан жить до самого конца.
Домашнее задание
Дорастите эхо до чата: хаб с картой клиентов под мьютексом, буфер send 16, писатель-горутина на клиента с defer conn.Close() и leave перед close(send) в читателе. Поставьте heartbeat целиком: writeWait 10 секунд, pongWait 60, pingPeriod девять десятых, SetPongHandler с продлением дедлайна и SetReadLimit 4096. Поднимите CheckOrigin на белый список из вашего фронтенд-хоста и проверьте: страница с чужого хоста получает 403. Логи раздели: IsUnexpectedCloseError только для аварийных обрывов. Дополнительно: отправляйте в чат JSON {"user":"имя","text":"..."}, доставайте имя из cookie сессии из урока про cookie и при переполнении буфера отключайте клиента кадром Close с кодом 1013. Проверьте через две вкладки браузера, что сообщение уходит обоим, и через выключение сети — что мёртвое соединение закрывается само не позже чем через pongWait.
Личные заметкиПусто
Вопросы и ответы по уроку
gorilla/websocket или coder/websocket (бывший nhooyr)?
Обе библиотеки живые и хорошие. gorilla/websocket — самая распространённая, с API в стиле net/http (Upgrader, Hijack, ReadMessage), огромная база примеров и привычный хаб-паттерн; проект вернулся под активную разработку в 2023-м. coder/websocket ниже уровнем к контекстам: идиоматичный ctx, автопинг, меньше ручной работы с дедлайнами. Курс идёт на gorilla ради каноничности: понимая её механику, вы разберётесь и во второй.
Почему паника concurrent write to websocket connection?
gorilla сознательно не ставит мьютекс на запись: сериализация каждой записи добавила бы задержку всем. Контракт такой — одна пишущая горутина на conn, а Broadcast-сценарии строятся на каналах: все отправители кладут байты в канал, единственный писатель достаёт и пишет. WriteControl исключён из правила, потому что служебные ping/close должны уходить даже пока основной писатель занят.
Как пустить WebSocket через nginx?
Нужен туннель: заголовки Upgrade и Connection — hop-by-hop, обычное проксирование их вырежет. Конфиг: map $http_upgrade $connection_upgrade, proxy_pass на бэкенд, proxy_http_version 1.1 (дефолт 1.0 не умеет туннель) и proxy_set_header Upgrade $http_upgrade; Connection $connection_upgrade. Полный разбор с таймаутами proxy_read_timeout — в уроке 19 про прокси и стриминг: там же объяснено, почему SSE проходит легче.
Как аутентифицировать WebSocket-клиента?
До апгрейда это обычный HTTP-запрос — всё, что умеет net/http, доступно. Cookie сессии из урока про cookie приезжают в рукопожатие сами: читайте их из c.Request.Cookie и проверяйте до Upgrade. Для JWT из урока 15 проще выдать обычным REST-запросом одноразовый тикет с TTL в минуту и апгрейдиться с ним в query — долгоживущий токен в URL не попадёт. А вот токен в заголовке Authorization браузерный WebSocket API выставить не позволяет.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Echo: минималистичный быстрый фреймворк
Курс Echo для Go: маршруты, middleware, валидация, JWT, SSE, тестирование и деплой. 25 уроков с примерами и итоговым API-проектом.
- Строить REST API на Echo
- Использовать типизированные запросы/ответы (Request/Response)
- Управлять ошибками через echo.HTTPError
- Писать middleware, включая JWT-аутентификацию