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

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

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

Урок 20 из 25 ~24 мин +50 XP

WebSocket в Gin

Прошлый урок закончился обещанием: SSE умеет только шептать в одну сторону, а WebSocket разговорит обе — и сейчас мы это обещание выполним. После SSE из [урока про прокси и стриминг](/courses/gin/lessons/19-proksi-streaming/) вы уже знаете односторонние потоки; теперь двусторонний обмен поверх одного TCP-соединения: браузер и сервер шлют кадры друг другу когда угодно, без новых запросов. Разберём gin websocket по-честному: рукопожатие HTTP-апгрейда и заголовок Sec-WebSocket-Accept, метод upgrader.Upgrade, который через Hijack забирает соединение из-под net/http, типы кадров от текста до ping, эхо-сервер в двадцать строк и хаб клиентов для чата с одной пишущей горутиной на соединение. Отдельный разговор — heartbeat: ping/pong и дедлайны, без которых мёртвые соединения копятся, и CheckOrigin, без которого чужие страницы разговаривают от имени ваших пользователей. websocket golang-экосистемы — это прежде всего gorilla/websocket, и мы будем ходить по её исходникам, как ходили по исходникам gin. Параллельно вспомним звенья из [урока про middleware](/courses/gin/lessons/06-middleware/): некоторые обёртки писателя ломают апгрейд — увидим, какие и почему. Конверт ошибок из [урока 17](/courses/gin/lessons/17-oshibki/) здесь заканчивается на границе апгрейда: после 101 соединению уже не понять HTTP-JSON, поэтому ошибки рукопожатия gorilla пишет сама, а ошибки протокола — кадрами Close.

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

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

GO
package 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.TextMessage1текст UTF-8: чат, JSON, события
websocket.BinaryMessage2байты как есть: файлы, protobuf, медиа
websocket.CloseMessage8рукопожатие закрытия: код и причина
websocket.PingMessage9проверка живости — получатель обязан ответить pong
websocket.PongMessage10ответ на 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: боевой скелет

GO
const (
        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. Один читатель (хендлер), один писатель (горутина канала) — правило соблюдено архитектурой, а не дисциплиной.

Хаб клиентов и чат-хендлер

GO
type 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, чем закрываем соединение, что продлевает дедлайн и как выглядит пара чтения и эха.

r.GET("/ws", func(c *gin.Context) { conn, err := upgrader.Upgrade(c., c., nil) if err != nil { return // Upgrade уже ответил клиенту ошибкой } defer conn.() conn.SetReadDeadline(time.Now().Add(pongWait)) conn.Set(func(string) error { return conn.SetReadDeadline(time.Now().Add(pongWait)) }) for { mt, msg, err := conn.() if err != nil { return } conn.(mt, msg) // эхо тем же типом кадра } })

Соберите жизнь одного WS-сообщения

Пользователь печатает в чат «привет». Разложите путь сообщения: от рукопожатия до раздачи всем и закрытия.

Строки

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

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

Карточки: WebSocket в gin

Термин

websocket.Upgrader

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

Структура с HandshakeTimeout, ReadBufferSize/WriteBufferSize, Subprotocols, EnableCompression и CheckOrigin. Метод Upgrade(w, r, responseHeader) проверяет рукопожатие, пишет 101 и возвращает *websocket.Conn; при ошибке сам отвечает клиенту HTTP-кодом (400/403/405/426) и возвращает error.

1 / 6

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

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

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

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

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 выставить не позволяет.

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

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

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

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

Echo: минималистичный быстрый фреймворк

Курс Echo для Go: маршруты, middleware, валидация, JWT, SSE, тестирование и деплой. 25 уроков с примерами и итоговым API-проектом.

  • Строить REST API на Echo
  • Использовать типизированные запросы/ответы (Request/Response)
  • Управлять ошибками через echo.HTTPError
  • Писать middleware, включая JWT-аутентификацию