WebSocket в Go и причем тут горилла

горилла

горилла

Привет, Хабр!

WebSocket позволяет открыть интерактивный коммуникационный сеанс между пользовательским браузером и сервером. Здесь большое отличие от традиционного HTTP, который ограничен моделью запрос-ответ и не подходит для сценариев, требующих постоянного обмена данными

Go с помощью своей простоты и поддержкой конкурентности становится хорошим кандидатом для работы с WebSocket.

Значение конкурентности в Go для работы с WebSocket.

Горутины — это легковесные потоки исполнения, управляемые Go runtime. Они значительно более эффективны по сравнению с традиционными потоками операционной системы благодаря меньшему потреблению памяти и более низким накладным расходам на их создание и управление. Горутины позволяют писать асинхронный код, который может одновременно обрабатывать множество соединений или задач без блокировки и значительных накладных расходов.

Каналы в Go это средство для обмена данными между горутинами, обеспечивающий синхронизацию без явного использования блокировок или условий состояния. Они предоставляют безопасный и удобный способ передачи сообщений между горутинами, что мегаважно для обработки данных в реал таймме, как в случае с WebSocket.

WebSocket-серверы часто требуют одновременной обработки множества активных соединений. Каждое соединение WebSocket требует постоянной активности для поддержания связи и обмена данными в реальном времени. Используя горутины, Go позволяет управлять множеством параллельных соединений, где каждое соединение WebSocket может быть обработано отдельной горутиной. Это обеспечивает оч хорошую производительность и отзывчивость при минимальном оверхеде.

WebSocket предполагает двустороннюю коммуникацию, где сообщения могут исходить как от клиента, так и от сервера. Асинхронная природа горутин позволяет легко реализовать обработку входящих и исходящих сообщений одновременно. Каналы в Go могут использоваться для передачи сообщений между горутинами, занимающимися обработкой соединений и бизнес-логикой приложения.

WebSocket-сервер на Go

Предполагается, что вы уже имеете golang ^^

Создайте новую директорию для вашего проекта и инициализируйте её как модуль Go, используя команду go mod init your_project_name. Это создаст новый файл go.mod, управляющий зависимостями вашего проекта.

Про гориллу

Для работы с WebSocket мы будем использовать попсовую библиотеку gorilla/websocket. Да, вот причем тут горилла. Немного про неё:

websocket.Upgrader используется для обновления HTTP-соединения до протокола WebSocket. Это основной компонент для создания WebSocket-сервера. Он позволяет настроить различные параметры, такие как размеры буфера чтения и записи, проверку исходящего запроса и другие опции безопасности.

websocket.Conn представляет собой WebSocket-соединение. Этот тип обеспечивает интерфейсы для чтения и записи сообщений WebSocket. Он поддерживает текстовые и двоичные сообщения и позволяет управлять такими деталями, как время ожидания, закрытие соединения и управление пингами/понгами.

Немного про методы с этими фунциями:

Upgrader.Upgrade используется для преобразования HTTP-запроса в WebSocket-соединение. Этот метод возвращает *websocket.Conn и используется на стороне сервера для начала сессии WebSocket.

Conn.ReadMessage() и Conn.WriteMessage() используются для чтения и записи сообщений. ReadMessage блокирует вызывающий поток до получения сообщения или возникновения ошибки. WriteMessage используется для отправки сообщений клиенту.

NextWriter и NextReaderпредоставляют более низкоуровневый доступ к потокам чтения и записи WebSocket. NextWriter возвращает writer для следующего сообщения, а NextReader очевидно возвращает reader для чтения следующего сообщения.

Также:

Библиотека поддерживает управление закрытием соединения, включая отправку и обработку соответствующих управляющих сообщений WebSocket. В библиотеке есть поддержка пинг/понг обработчиков для поддержания активности соединения и определения его состояния.

Поддерживает сжатие сообщений WebSocket, что может быть полезно для уменьшения объема передаваемых данных.

Позволяет управлять фреймами данных.

Чтобы добавить её в ваш проект, выполните команду go get github.com/gorilla/websocket.

Подробнее про библиотеку на гит хабе.

Напишем код для создания простого сервера

Импортируем нашу гориллу:

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

websocket.Upgraderбудем юзать для обновления HTTP-соединений до протокола WebSocket

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

Создадим функцию, которая будет обрабатывать входящие WebSocket-соединения:

func handleConnections(w http.ResponseWriter, r *http.Request) {
    // обновление соединения до WebSocket
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Fatal(err)
    }
    defer ws.Close()

    // цикл обработки сообщений
    for {
        messageType, message, err := ws.ReadMessage()
        if err != nil {
            log.Println(err)
            break
        }
        log.Printf("Received: %s", message)

        // эхо ансвер
        if err := ws.WriteMessage(messageType, message); err != nil {
            log.Println(err)
            break
        }
    }
}

Зарегистрируем функцию handleConnections как обработчик маршрута:

func main() {
    http.HandleFunc("/ws", handleConnections)
    log.Println("http server started on :8000")
    err := http.ListenAndServe(":8000", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

Как тестировать?

Выполняем команду go run your_project_name.go для запуска сервера и проверяем, что сервер запускается без ошибок и доступен на http://localhost:8000/ws.

Можно также использовать клиент WebSocket, например, браузерное расширение, чтобы установить соединение с вашим сервером и отправить сообщения.

Поддержание

WebSocket-соединение начинается с HTTP-запроса, который затем «обновляется» до протокола WebSocket. Этот процесс называется «Handshake». Начальный запрос должен соответствовать стандартам WebSocket, включая правильные заголовки (Upgrade: websocket и Connection: Upgrade).

На стороне сервера важно проверять поле Origin в HTTP-запросе, чтобы предотвратить атаки типа Cross-Site WebSocket Hijacking.

Используя гориллу в Go, можно настроить параметры соединения, включая размеры буферов, таймауты и механизмы сжатия:

Настроим Upgrader:

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

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,  // Размер буфера чтения
    WriteBufferSize: 1024,  // Размер буфера записи
    // Позволяет определить, должен ли сервер сжимать сообщения
    EnableCompression: true,
}

func handleUpgrade(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        // обработка ошибки
        return
    }
    defer conn.Close()
    // дальнейшая обработка соединения
}

upgrader, который используется для преобразования HTTP-запросов в WebSocket-соединении

Таймауты:

func handleConnections(conn *websocket.Conn) {
    // Установка таймаута для чтения сообщения
    conn.SetReadDeadline(time.Now().Add(60 * time.Second))
    
    for {
        _, message, err := conn.ReadMessage()
        if err != nil {
            // обработка ошибки
            break
        }
        // обработка сообщения

        // Обновление таймаута после успешного чтения сообщения
        conn.SetReadDeadline(time.Now().Add(60 * time.Second))
    }
}

Устанавливаем таймаут для операций чтения на WebSocket-соединении. SetReadDeadline используется для определения времени, по истечении которого соединение будет закрыто, если не будет получено новое сообщение.

WebSocket поддерживает фреймы управления, такие как пинг (ping) и понг (pong).

Отправка пингов с сервера на клиент помогает удостовериться, что клиент все еще подключен и активен.

Как это можно реализовать:

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

conn.SetPingHandler(func(appData string) error {
    return conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(writeWait))
})

На сервере можно реализовать горутину, которая будет периодически отправлять пинги клиентам:

ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()

for {
    select {
    case <-ticker.C:
        conn.SetWriteDeadline(time.Now().Add(writeWait))
        if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
            return // или обработать ошибку
        }
    }
}

Понги используются в ответ на пинги и помогают серверу узнать, что клиент все еще подключен.

На сервере можно настроить обработчик понгов для обновления состояния соединения:

conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })

Для защиты данных, передаваемых по WebSocket, следует использовать WSS (аналог HTTPS для WebSocket), который обеспечивает шифрование данных. На сервере стоит установить ограничения на количество одновременно открытых соединений, размер принимаемых сообщений и другие параметры для защиты от перегрузок и атак.

Про масштабирование

WebSocket поддерживает длительные соединения. Однако, при масштабировании, оч важно управлять этими соединениями. В Go, это обычно достигается за счет использования горутин для каждого соединения:

package main

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

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

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

    go handleConnection(conn)
}

func handleConnection(conn *websocket.Conn) {
    for {
        messageType, p, err := conn.ReadMessage()
        if err != nil {
            return
        }
        // обработка сообщения...
    }
}

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

При масштабировании WebSocket-сервера в Go нужно обеспечить обработку входящего и исходящего трафика. Это может включать использование буферизации, асинхронной отправки/получения данных и обработки ошибок:

func handleConnection(conn *websocket.Conn) {
    for {
        // Чтение месседжа
        _, message, err := conn.ReadMessage()
        if err != nil {
            break
        }

        // асинхрон отправка
        go func(msg []byte) {
            err = conn.WriteMessage(websocket.TextMessage, msg)
            if err != nil {
                return
            }
        }(message)
    }
}

Горутины и каналы — хороший асинхронный инструмент:

func handleConnection(conn *websocket.Conn) {
    msgChan := make(chan []byte)

    go func() {
        for {
            message, ok := <-msgChan
            if !ok {
                return
            }
            conn.WriteMessage(websocket.TextMessage, message)
        }
    }()

    for {
        _, message, err := conn.ReadMessage()
        if err != nil {
            close(msgChan)
            break
        }
        msgChan <- message
    }
}

Часто используют промежуточные слои для управления аутентификацией, логированием, ограничением скорости:

func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("Получен запрос:", r.URL)
        next.ServeHTTP(w, r)
    }
}

func main() {
    http.HandleFunc("/ws", loggingMiddleware(handler))
    http.ListenAndServe(":8080", nil)


}

WebSocket в Go имеет множество возможностей. Это позволяет создавать интерактивные и реагирующие в real-time приложения. Go хороший выбор для вебсокетов, благодаря своей производительности, фичам конкурентности и простоте интеграции.

«Горилла» — это не только сильное животное, но и хороший инструмент в вашем арсенале разработчика.

Про другие, не менее полезные инструменты, мои коллеги из OTUS рассказывают в рамках онлайн-курсов. Также напоминаю о том, что в календаре мероприятий вы можете зарегистрироваться на ряд полезных бесплатных вебинаров.

© Habrahabr.ru