WebSocket на Go: real-time приложения от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в WebSocket на Go: почему real-time?

Современные веб-приложения всё чаще требуют мгновенной доставки данных. Традиционный HTTP-запрос/ответ уже не справляется с задачами чатов, онлайн-игр, биржевых котировок и панелей мониторинга. Здесь на помощь приходит WebSocket — протокол, обеспечивающий постоянное двустороннее соединение между клиентом и сервером. В этой статье мы разберём, как создавать real-time приложения на Go (Golang) с использованием WebSocket, от установки до продакшн-решений.

Go идеально подходит для WebSocket благодаря своей лёгкой модели конкурентности (горутины) и высокой производительности. Мы пройдём путь от простого эхо-сервера до полноценного чата и системы мониторинга.

Основы WebSocket и настройка окружения

Что такое WebSocket?

WebSocket — это протокол, работающий поверх TCP. Он начинается с HTTP-рукопожатия (upgrade), после чего соединение переключается на постоянный двунаправленный канал. В отличие от HTTP, сервер может отправлять данные клиенту без запроса — это основа real-time.

Установка и библиотеки

В Go стандартной библиотеки для WebSocket нет, но есть популярная библиотека gorilla/websocket. Установим её:

go get github.com/gorilla/websocket

Также можно использовать nhooyr.io/websocket (более современная альтернатива), но в этой статье мы остановимся на gorilla как на самой распространённой.

Первый WebSocket сервер на Go

Напишем простой эхо-сервер, который получает сообщение и отправляет его обратно:

package main

import (
    "log"
    "net/http"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true // для разработки
    },
}

func echoHandler(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, message, err := conn.ReadMessage()
        if err != nil {
            log.Println("Read:", err)
            break
        }
        log.Printf("Received: %s", message)
        err = conn.WriteMessage(mt, message)
        if err != nil {
            log.Println("Write:", err)
            break
        }
    }
}

func main() {
    http.HandleFunc("/echo", echoHandler)
    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Это минимальный рабочий пример. Обратите внимание на Upgrader — он проверяет заголовки HTTP и переключает соединение.

Архитектура real-time приложения: чат на Go

Управление подключениями

Для чата нужно управлять множеством клиентов. Создадим структуру Hub, которая будет хранить все активные соединения и рассылать сообщения:

type Client struct {
    hub  *Hub
    conn *websocket.Conn
    send chan []byte
}

type Hub struct {
    clients    map[*Client]bool
    broadcast  chan []byte
    register   chan *Client
    unregister chan *Client
}

func newHub() *Hub {
    return &Hub{
        clients:    make(map[*Client]bool),
        broadcast:  make(chan []byte),
        register:   make(chan *Client),
        unregister: make(chan *Client),
    }
}

func (h *Hub) run() {
    for {
        select {
        case client := <-h.register:
            h.clients[client] = true
        case client := <-h.unregister:
            if _, ok := h.clients[client]; ok {
                delete(h.clients, client)
                close(client.send)
            }
        case message := <-h.broadcast:
            for client := range h.clients {
                select {
                case client.send <- message:
                default:
                    close(client.send)
                    delete(h.clients, client)
                }
            }
        }
    }
}

Чтение и запись для каждого клиента

Каждый клиент запускает две горутины: readPump (чтение из сокета) и writePump (запись в сокет). Это стандартный паттерн для gorilla/websocket:

func (c *Client) readPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()
    for {
        _, message, err := c.conn.ReadMessage()
        if err != nil {
            break
        }
        c.hub.broadcast <- message
    }
}

func (c *Client) writePump() {
    defer c.conn.Close()
    for message := range c.send {
        err := c.conn.WriteMessage(websocket.TextMessage, message)
        if err != nil {
            break
        }
    }
}

Запуск чата

В функции main запускаем Hub и регистрируем обработчик:

func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
        return
    }
    client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
    hub.register <- client
    go client.writePump()
    go client.readPump()
}

func main() {
    hub := newHub()
    go hub.run()
    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
        serveWs(hub, w, r)
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Этот код образует основу любого real-time чата. Клиент (браузер) подключается к /ws, и все сообщения рассылаются всем участникам.

Продвинутые техники: мониторинг и масштабирование

Real-time мониторинг системы

WebSocket отлично подходит для дашбордов. Создадим сервер, который каждые 2 секунды отправляет клиенту данные о загрузке CPU:

import (
    "runtime"
    "time"
)

func monitorHandler(w http.ResponseWriter, r *http.Request) {
    conn, _ := upgrader.Upgrade(w, r, nil)
    defer conn.Close()

    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()

    for range ticker.C {
        var m runtime.MemStats
        runtime.ReadMemStats(&m)
        msg := fmt.Sprintf(`{"cpu": %d, "mem": %d}`, 
            runtime.NumGoroutine(), m.Alloc/1024/1024)
        if err := conn.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {
            break                

Блоги

Рекомендаций по книгам