WebSocket в Go и причем тут горилла
- понедельник, 25 декабря 2023 г. в 00:00:12
 

Привет, Хабр!
WebSocket позволяет открыть интерактивный коммуникационный сеанс между пользовательским браузером и сервером. Здесь большое отличие от традиционного HTTP, который ограничен моделью запрос-ответ и не подходит для сценариев, требующих постоянного обмена данными
Go с помощью своей простоты и поддержкой конкурентности становится хорошим кандидатом для работы с WebSocket.
Горутины – это легковесные потоки исполнения, управляемые Go runtime. Они значительно более эффективны по сравнению с традиционными потоками операционной системы благодаря меньшему потреблению памяти и более низким накладным расходам на их создание и управление. Горутины позволяют писать асинхронный код, который может одновременно обрабатывать множество соединений или задач без блокировки и значительных накладных расходов.
Каналы в Go это средство для обмена данными между горутинами, обеспечивающий синхронизацию без явного использования блокировок или условий состояния. Они предоставляют безопасный и удобный способ передачи сообщений между горутинами, что мегаважно для обработки данных в реал таймме, как в случае с WebSocket.
WebSocket-серверы часто требуют одновременной обработки множества активных соединений. Каждое соединение WebSocket требует постоянной активности для поддержания связи и обмена данными в реальном времени. Используя горутины, Go позволяет управлять множеством параллельных соединений, где каждое соединение WebSocket может быть обработано отдельной горутиной. Это обеспечивает оч хорошую производительность и отзывчивость при минимальном оверхеде.
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 рассказывают в рамках онлайн-курсов. Также напоминаю о том, что в календаре мероприятий вы можете зарегистрироваться на ряд полезных бесплатных вебинаров.