WebSocket на Go: создание real-time приложений с нуля

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

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

Начать курс

WebSocket на Go: создание real-time приложений

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

Что такое WebSocket и почему Go?

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

Почему Go? Язык Go идеально подходит для WebSocket-серверов благодаря встроенной поддержке конкурентности (горутины и каналы), высокой производительности и простоте написания сетевого кода. В отличие от Node.js, Go не требует внешних циклов событий, а его компилируемая природа даёт преимущество в скорости.

Базовый WebSocket сервер на Go

Для работы с WebSocket в Go мы будем использовать популярную библиотеку gorilla/websocket. Она предоставляет удобный API и обрабатывает детали протокола.

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

go get github.com/gorilla/websocket

Теперь напишем простой эхо-сервер, который отправляет обратно всё, что получает от клиента.

package main

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

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true // Для разработки. В продакшене проверяйте Origin.
    },
}

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

    for {
        messageType, message, err := conn.ReadMessage()
        if err != nil {
            log.Println("Read error:", err)
            break
        }
        log.Printf("Получено: %s", message)

        if err := conn.WriteMessage(messageType, message); err != nil {
            log.Println("Write error:", err)
            break
        }
    }
}

func main() {
    http.HandleFunc("/ws", echoHandler)
    log.Println("Сервер запущен на :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Объяснение кода:

  • Upgrader — объект, который «повышает» HTTP-соединение до WebSocket.
  • CheckOrigin — функция проверки источника запроса. В продакшене обязательно настройте её.
  • conn.ReadMessage() и conn.WriteMessage() — основные методы для чтения/записи сообщений.
  • Цикл for поддерживает соединение открытым до возникновения ошибки.

Архитектура real-time чата на WebSocket

Эхо-сервер — это основа, но для real-time приложения, например чата, нужна более сложная логика. Рассмотрим архитектуру, где несколько клиентов могут обмениваться сообщениями через сервер.

Основные компоненты:

  • Hub — центральный менеджер, хранящий список активных подключений.
  • Client — структура, представляющая одно WebSocket-соединение.
  • Каналы (channels) — для безопасной передачи данных между горутинами.
package main

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

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool { return true },
}

// Client представляет подключённого пользователя.
type Client struct {
    hub  *Hub
    conn *websocket.Conn
    send chan []byte
}

// Hub управляет всеми клиентами.
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)
                }
            }
        }
    }
}

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 {
        if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
            break
        }
    }
}

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.Println("Чат-сервер запущен на :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Как это работает:

  • При подключении клиента создаётся структура Client и запускаются две горутины: readPump (чтение) и writePump (запись).
  • Hub слушает каналы регистрации/отключения и рассылает сообщения всем активным клиентам.
  • Канал send с буфером 256 сообщений предотвращает блокиров

Блоги

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