Перейти к основному содержимому

WebSocket в Go

Разработчику

WebSocket даёт постоянное двустороннее соединение поверх HTTP upgrade. Подходит для чатов, live-дашбордов, игр и push-уведомлений, где REST с polling слишком медленный. Транспорт ниже HTTP — в TCP и UDP, основа HTTP — веб на stdlib.

См. также: WebSocket — обзор · Асинхронность.


WebSocket, HTTP и TCP

ПротоколМодельТипичное применение
REST/HTTPЗапрос → ответCRUD API
WebSocketДолгое двустороннееЧат, тикеры, совместное редактирование
Raw TCPПроизвольные байтыСвой бинарный протокол

WebSocket начинается как обычный HTTP GET с заголовками Upgrade: websocket и Connection: Upgrade. После handshake соединение остаётся открытым — фреймы текста или бинарных данных идут в обе стороны.


Библиотека

Стандартная библиотека не содержит полноценного WebSocket-стека. На практике используют github.com/gorilla/websocket или nhooyr.io/websocket. Ниже — gorilla.

go get github.com/gorilla/websocket

Upgrader и сервер

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
// в проде — whitelist Origin, не return true всегда
return r.Host == "localhost:8080"
},
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("upgrade:", err)
return
}
defer conn.Close()

for {
mt, msg, err := conn.ReadMessage()
if err != nil {
break
}
log.Printf("recv type=%d len=%d", mt, len(msg))
if err := conn.WriteMessage(mt, msg); err != nil {
break
}
}
}

func main() {
http.HandleFunc("/ws", wsHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Разбор:

  • Upgrader.Upgrade переводит HTTP-соединение в WebSocket; до upgrade ответ уже нельзя писать через обычный ResponseWriter.
  • CheckOrigin защищает от CSRF-подобных подключений с чужих сайтов — в dev часто ослабляют, в prod проверяют Origin.
  • ReadMessage / WriteMessage работают с целыми фреймами; для потоковой обработки — NextReader / NextWriter.
  • Цикл завершается при закрытии клиентом или сетевой ошибке; defer conn.Close() освобождает ресурсы.
Origin и TLS

WebSocket в браузере идёт как wss:// поверх TLS. Сервер поднимают через ListenAndServeTLS или reverse proxy (nginx, Caddy), который проксирует Upgrade.


Hub — рассылка нескольким клиентам

Типичный паттерн hub: центральная горутина регистрирует клиентов и рассылает broadcast.

type hub struct {
clients map[*websocket.Conn]struct{}
broadcast chan []byte
register chan *websocket.Conn
unregister chan *websocket.Conn
}

func (h *hub) run() {
for {
select {
case c := <-h.register:
h.clients[c] = struct{}{}
case c := <-h.unregister:
delete(h.clients, c)
c.Close()
case msg := <-h.broadcast:
for c := range h.clients {
if err := c.WriteMessage(websocket.TextMessage, msg); err != nil {
delete(h.clients, c)
c.Close()
}
}
}
}
}

Разбор:

  • Одна goroutine-hub сериализует доступ к clients — без mutex.
  • register / unregister вызывают из HTTP-handler после Upgrade.
  • При ошибке записи клиент удаляют из map, иначе hub «залипнет» на мёртвом соединении.
  • Broadcast через канал отделяет producers (HTTP, другие goroutine) от рассылки.

Связь с select и каналами: hub — классический event loop на select.


Клиент (Go)

conn, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()

if err := conn.WriteMessage(websocket.TextMessage, []byte("hello")); err != nil {
log.Fatal(err)
}
_, msg, err := conn.ReadMessage()

Разбор:

  • DefaultDialer.Dial выполняет client-side handshake.
  • Таймауты и TLS настраивают через поля Dialer (HandshakeTimeout, TLSClientConfig).
  • Для нагрузочных клиентов переиспользуют одно соединение, а не Dial на каждое сообщение.

Ping, pong и дедлайны

conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})

Разбор:

  • Без read deadline «тихие» клиенты держат goroutine и file descriptor.
  • Ping/pong (RFC 6455) обнаруживает обрыв; gorilla может слать ping из фоновой goroutine.
  • Паттерн совпадает с SetDeadline на TCP — см. 27.md.

WebSocket и горутины

На каждое соединение обычно одна goroutine на чтение и одна на запись (или hub). Ограничивайте число клиентов семафором — как для TCP-серверов.

При shutdown сервиса закрывают listener, затем все websocket.Conn, чтобы goroutine завершились до exit.


Ключевые тезисы

  • WebSocket — upgrade HTTP, дальше фреймы, а не новый TCP-port.
  • CheckOrigin, TLS и лимиты соединений обязательны в production.
  • Hub на каналах масштабирует broadcast без гонок на map клиентов.

Мини-практикум

  1. Echo-сервер /ws и клиент из websocket.DefaultDialer.
  2. Добавьте hub с broadcast «новое сообщение всем».
  3. Сравните задержку push через WebSocket и polling REST раз в секунду.

Типичные ошибки

ОшибкаПоследствие
CheckOrigin: true в prodПодключение с чужого сайта
Писать в ResponseWriter после UpgradeСломанный протокол
Нет ping/deadlineУтечка goroutine
Concurrent WriteMessage без mutexRace, corrupted frames

Связанные материалы

  • TCP и UDP в Go — UNIX domain sockets, сырой транспорт
  • Веб на stdlib — HTTP до upgrade
  • Gin — REST рядом с WebSocket на том же порту через http.Server

Основа по протоколу

Обзор WebSocket в сетевом разделе — WebSocket.

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").