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

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

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

Урок 3 из 25 ~18 мин +20 XP

gorilla/websocket: установка и Upgrader

Gorilla websocket установка сводится к одной команде go get, но вся суть прячется в структуре websocket.Upgrader: именно она решает, кому открывать real-time-соединение, и превращает обычный HTTP-хендлер в живой WS-канал. В этом уроке пройдём путь от пустой папки до настроенного апгрейдера: go mod init, go get, поля Upgrader по одному, ловушка с middleware-обёртками и проверка Origin — с кодом, который вы потом возьмёте в четвёртый урок почти без правок.

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

Gorilla websocket установка: go mod init и go get#

Типичный сценарий: у вас уже есть сервис на net/http или на роутере — страницы отдаются, API живёт, и вдруг появляется задача с real-time: чат, уведомления, живой счётчик. Тащить фреймворк ради одной функции не хочется, и хорошо, что не нужно: WebSocket на Go — это одна библиотека поверх того, что уже есть. gorilla/websocket приносит три главных сущности: Upgrader, который проводит рукопожатие, Conn — само соединение, и Dialer — исходящее соединение для тестов и сервер-сервер трафика. Библиотека собрана на чистой стандартной библиотеке: в go.mod проекта появляется ровно одна запись, транзитивных зависимостей нет, а хендлеры остаются обычными http.HandlerFunc. Иными словами, WS-сервер — это всё ещё ваш старый сервер: те же маршруты, тот же ListenAndServe, просто один из хендлеров после апгрейда перестаёт говорить по HTTP. Разберём путь целиком — от go mod init до полей Upgrader, которыми решается, кого впускать.

Начните с чистой папки и модуля. go mod init example.com/ws101 объявляет модуль — без этого go get некуда записывать зависимость; имя модуля — это путь, а не URL, и его не нужно заводить в репозитории. Затем go get github.com/gorilla/websocket подтягивает актуальную версию первой мажорной ветки и пишет её в require go.mod — на момент курса это v1.5.x. Суффиксов в пути импорта нет и не будет: у chi пятая мажорная живёт прямо в пути как /v5, у gorilla путь короткий — github.com/gorilla/websocket, и это частый источник опечаток в туториалах в обратную сторону. В коде пакет импортируется одним именем — websocket — и дальше вы пишете websocket.Upgrader, websocket.Conn, websocket.TextMessage. go.sum получит хэши для воспроизводимой сборки, как у любой зависимости; go mod tidy вычистит лишнее, если вы удалили код. Три команды и одна строка импорта — вся церемония установки.

Три команды до зависимости

GO
mkdir ws101 && cd ws101
go mod init example.com/ws101
go get github.com/gorilla/websocket

Три команды — и зависимость в go.mod. Модуль обязателен: без go mod init команде go get некуда писать require. Путь импорта короткий — github.com/gorilla/websocket, без суффиксов версии: мажорная ветка у библиотеки первая. В go.sum появятся хэши для воспроизводимости сборки, как и у любой другой зависимости проекта.

Типичная ошибка: go get вне модуля и импорты из чужих туториалов#

Первая же попытка поставить библиотеку регулярно спотыкается на двух граблях, и обе лечатся пониманием, а не переустановкой. Грабля первая: go get запущен в папке без go mod init — инструмент честно отвечает, что go.mod не найден, и ничего не ставит. Модуль — не формальность, а адрес, куда пишется зависимость, поэтому три команды из блока выше выполняются строго в этом порядке. Грабля вторая — копипаст импортов из соседних туториалов: путь с /v5 из урока по chi не скомпилируется с gorilla, и наоборот — короткий путь без версии здесь не ошибка, а норма. Отдельная история — старые статьи: API gorilla стабилен, но примеры эпохи до 2018 года иногда тянут давно удалённые подпакеты. Если сборка ругается на неожиданный путь, сверяйтесь не с памятью, а с go.mod: что записано в require — то и импортируется. И приучите себя к go mod tidy после экспериментов: он убирает зависимости от удалённого кода и не даёт go.mod превратиться в свалку.

websocket.Upgrader: структура, которая решает всё#

Upgrader — конфигурация рукопожатия, и его поля стоит знать до того, как писать код. ReadBufferSize и WriteBufferSize задают размеры внутренних буферов ввода-вывода на соединение; ноль означает дефолт в 4096 байт. HandshakeTimeout — дедлайн на фазу рукопожатия: клиент, приславший GET и замолчавший, не будет держать соединение вечно. CheckOrigin — функция-фильтр, решающая, чужой ли Origin пришёл; дефолт строгий, о нём отдельно ниже. Subprotocols выбирает субпротокол из заголовка Sec-WebSocket-Protocol, EnableCompression включает расширение permessage-deflate для сжатия кадров, а Error позволяет заменить стандартный способ записи HTTP-ошибки при провале апгрейда на свой — например, чтобы логировать причину. Сам апгрейд делает метод Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header): третий аргумент — заголовки, которые хочется добавить в ответ 101 до его отправки, скажем cookie; чаще всего передают nil. Возвращает метод conn — соединение — и ошибку, и обращение с ошибкой здесь нестандартное.

Производственный Upgrader и первый апгрейд

GO
var upgrader = websocket.Upgrader{
        ReadBufferSize:   1024,
        WriteBufferSize:  1024,
        HandshakeTimeout: 10 * time.Second,
        CheckOrigin: func(r *http.Request) bool {
                return r.Header.Get("Origin") == "https://app.example.com"
        },
}

func ws(w http.ResponseWriter, r *http.Request) {
        conn, err := upgrader.Upgrade(w, r, nil)
        if err != nil {
                log.Println("отказ:", err) // клиент уже получил HTTP-ответ
                return
        }
        defer conn.Close()
        // дальше — цикл чтения, тема четвёртого урока
}

Upgrader объявляют один раз на весь процесс: структура дешёвая по ресурсам, но дорогая по смыслу. Upgrade проводит всё рукопожатие — проверяет метод, заголовки, версию и Origin, отвечает 101 и возвращает conn. При любой неудаче функция сама записывает HTTP-ответ с подходящим кодом ошибки, поэтому хендлеру остаётся залогировать и выйти: второй ответ писать нельзя.

Проверка: ошибка апгрейда

Upgrade вернул ошибку, и хендлер после неё вызвал w.WriteHeader(http.StatusInternalServerError). Что получит клиент?

CheckOrigin: почему по умолчанию только свой домен#

Браузер всегда прикладывает к рукопожатию Origin — адрес страницы, открывшей соединение. У WebSocket нет CORS в HTTP-смысле; вместо него — CheckOrigin. Дефолт gorilla строгий: Origin должен совпасть с Host самого запроса, иначе рукопожатие отклоняется с 403. Логика простая: страница чужого сайта не должна открывать сокет к вашему сервису и читать сообщения от имени залогиненного пользователя — cookies-то браузер приложит автоматически. В реальной разработке дефолт напомнит о себе сразу: фронтенд на localhost:3000, сервер на localhost:8080 — и рукопожатие отвергнуто, нужен свой CheckOrigin, пропускающий локальные адреса. Опасность в другом: в интернете полно советов написать return true и жить дальше. На dev это терпимо, в проде — открытая дверь для чужих страниц: отдельный урок курса про origins разбирает атаку и правильные фильтры, а здесь запомните форму: CheckOrigin — функция из http.Request в bool.

Проверка: два порта на локалке

Фронтенд открыт с http://localhost:3000, сервер на localhost:8080, CheckOrigin не задан. Чем закончится new WebSocket?

Что вернёт Upgrade на POST

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

// в upgrader.Upgrade ушёл запрос с методом POST
conn, err := upgrader.Upgrade(w, r, nil)
fmt.Println(conn, err)

Буферы: за что отвечают ReadBufferSize и WriteBufferSize#

На каждое соединение gorilla выделяет два буфера — на чтение и на запись — размеров ровно из Upgrader, по умолчанию по 4096 байт каждый. От размера зависит баланс: крупный буфер режет число системных вызовов на больших сообщениях и ускоряет ввод-вывод, но множит память — буферы живут у каждого соединения, пока оно открыто. Грубая прикидка с условием: десять тысяч открытых клиентов при паре буферов по 4096 байт — это порядка восьмидесяти мегабайт только на буферы; при 1024 — десять. Для чата и уведомлений, где сообщения короткие, значения 1024-4096 закрывают потребности; для передачи крупных бинарных кусков крупнее буфер оправдан. Писать в этих буферах вручную не придётся: ReadMessage и WriteMessage из четвёртого урока прячут всю механику. Знать поля стоит по другой причине: при отладке памяти под нагрузкой первые два подозреваемых — размеры буферов Upgrader и количество живых соединений.

Поля Upgrader, которые встретятся в курсе#

  • HandshakeTimeout — дедлайн фазы рукопожатия: молчаливый клиент не удержит соединение вечно.
  • CheckOrigin — фильтр Origin: дефолт строго свой домен, кастом нужен при раздельных портах.
  • Subprotocols — согласование субпротокола из заголовка Sec-WebSocket-Protocol.
  • EnableCompression — расширение permessage-deflate: сжатие кадров на лету.
  • ReadBufferSize и WriteBufferSize — размеры буферов ввода-вывода на соединение, дефолт 4096.
  • Error — своя функция записи HTTP-ошибки при провале апгрейда: лог вместо дефолтного текста.

Апгрейд внутри роутера chi

GO
r := chi.NewRouter()

r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
        conn, err := upgrader.Upgrade(w, r, nil)
        if err != nil {
                return // ответ клиенту уже записан библиотекой
        }
        defer conn.Close()
        // цикл чтения — как в следующем уроке
})

http.ListenAndServe(":8080", r)

chi.Router реализует http.Handler, поэтому апгрейд работает без адаптеров: WS-маршрут регистрируется обычным Get рядом с REST-эндпоинтами. Единственная оговорка — стек middleware: слои с собственной обёрткой ResponseWriter надо ставить так, чтобы /ws их не проходил, иначе Hijack не найдётся. Подробно про chi — в отдельном курсе сайта.

Кейс: WS-эндпоинт в готовом сервисе за nginx#

Частая постановка: сервис с REST уже живёт за nginx, и в него добавляют /ws. На локалке всё работает, а в проде соединение умирает через минуту тишины — классика с тремя причинами. Первая: nginx по умолчанию не прокидывает заголовки апгрейда, и рукопожатие даже не начинается; лечится блоком location /ws с proxy_set_header для Upgrade и Connection плюс proxy_http_version 1.1. Вторая: proxy_read_timeout по умолчанию — шестьдесят секунд, и молчаливое WS-соединение прокси рвёт, хотя оба конца живы; значение поднимают, а правильнее — вешают ping-механизм из шестого урока, чтобы кадры ходили чаще таймаута. Третья: gzip-слой или обёртка ResponseWriter в цепочке приложения ломает Hijack — об этом была врезка выше. Полезно понять общий принцип: для WS-маршрута весь путь от браузера до Go-процесса должен быть прозрачным для апгрейда, и каждый прокси в цепочке проверяется отдельно. Этот чек-лист я прогоняю перед любым демо real-time: девять из десяти жалоб «сервер падает» оказываются прокси.

Вариация: тот же апгрейд в coder/websocket#

Для полноты картины — как то же рукопожатие выглядит в альтернативной библиотеке. Пакет coder/websocket не имеет Upgrader: функцией Accept(w, r) вы принимаете соединение и сразу получаете conn, жизнью соединения управляет контекст запроса, а чтение построено на io.Reader вместо ReadMessage — сообщение приходит потоком, который вы читаете сами. Концептуально всё то же самое: тот же HTTP-запрос, тот же ответ 101, то же требование одной горутины-писателя на соединение. Меняется стиль: gorilla держит состояние в структуре Conn с полями-дедлайнами, coder — в контекстах и ридерах, что ближе к современному Go, но непривычнее после учебников. Мой практический критерий не меняется от направления: если в проекте уже есть код на gorilla или команда его знает — переезд не оправдан, API учится за вечер; для нового сервиса в ctx-first стиле честнее coder. Остальной курс идёт на gorilla — по нему написано больше всего, а навык переносится на любую из библиотек.

Соберите производственный Upgrader

Заполните три пропуска: поле-фильтр Origin, единицы дедлайна и метод рукопожатия.

var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, : func(r *http.Request) bool { return r.Header.Get("Origin") == "https://app.example.com" }, HandshakeTimeout: 10 * time., } conn, err := upgrader.(w, r, nil)

От пустой папки до апгрейда

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

Строки

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

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

Карточки: установка и Upgrader

Термин

websocket.Upgrader

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

Структура с настройками рукопожатия: буферы, дедлайн, CheckOrigin, субпротоколы, сжатие. Объявляется один раз и переиспользуется всеми хендлерами.

1 / 6

Дальше — четвёртый урок: полный эхо-сервер, где под апгрейдером появится цикл ReadMessage-WriteMessage и первая реальная жизнь соединения. Если что-то из протокола рукопожатия осталось туманным, вернитесь ко второму уроку — там каждая проверка Upgrade разобрана на уровне заголовков. Практический совет напоследок: апгрейд рождает долгоживущую горутину, поэтому перед хабом стоит освежить конкурентность в продвинутом курсе Go — там каналы, select и завершение горутин. Запускать код этого урока в песочнице бессмысленно — рукопожатие требует живого TCP и клиента напротив; поднимайте локальный Go, а карта всех уроков лежит на странице курса.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Установка — go get github.com/gorilla/websocket: один пакет без транзитивных зависимостей, путь импорта без суффиксов версии.
  • Upgrader объявляют один раз: буферы, HandshakeTimeout, CheckOrigin и остальные поля настраивают под деплой.
  • Upgrade проверяет метод, заголовки, версию и Origin, отвечает 101 и возвращает conn; при ошибке HTTP-ответ пишет сам.
  • Дефолтный CheckOrigin пропускает только совпадение Origin с Host — для dev на двух портах нужен свой фильтр.
  • Middleware с обёрткой ResponseWriter ломает апгрейд: у писателя должен быть Hijack, а Upgrade — первая строка хендлера.
  • ReadBufferSize и WriteBufferSize — компромисс между числом syscalls и памятью на каждое соединение.

Домашнее задание

Разверните пустой модуль и поставьте gorilla/websocket командами из урока. Соберите Upgrader с HandshakeTimeout и CheckOrigin, который пропускает только ваш Origin, и хендлером, где Upgrade — первая строка. Поднимите сервер на :8080 и убедитесь curl-ом, что обычный GET на /ws получает ошибку, а не 101: рукопожатие без заголовков апгрейда обязано провалиться.

Личные заметкиПусто
Хранятся только в вашем браузере (WebSocket · gorilla/websocket: установка и Upgrader)

Вопросы и ответы по уроку

Почему у gorilla нет /v5 в пути импорта, как у chi?

Семантическое версионирование Go требует суффикс только со второй мажорной версии. gorilla/websocket живёт в первой: путь короткий — github.com/gorilla/websocket. Если однажды выйдет вторая, импорт станет github.com/gorilla/websocket/v2, и старый код заметит это по ошибке компиляции.

Можно ли обойтись без Upgrader и написать рукопожатие руками?

Технически да: проверить заголовки, посчитать Sec-WebSocket-Accept, сделать Hijack. Это сотни строк точной работы с протоколом, включая маскирование и фрагментацию кадров. Upgrader существует именно поэтому; руками рукопожатие пишут разве что ради понимания, как во втором уроке.

Что выбрать — gorilla/websocket или coder/websocket?

gorilla — самый распространённый API: его знает каждый туториал и большинство собеседований; coder/websocket современнее — контексты и io.Reader. Для первого real-time проекта и этого курса gorilla честнее: навык переносится, а переезд на coder потом занимает вечер.

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

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

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

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

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

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

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