горилла
горилла

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

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 рассказывают в рамках онлайн-курсов. Также напоминаю о том, что в календаре мероприятий вы можете зарегистрироваться на ряд полезных бесплатных вебинаров.

Комментарии (9)


  1. bogolt
    24.12.2023 15:49

    Интересно, если ставить минусы статьям за идиотские нейрокартинки подействует ли это на авторов...

    Уважаемый автор. Сюда люди приходят за текстом, а находят статьи из списка по названию. Изображение не являющееся пояснением к чему-либо это визуальный мусор. В большинстве случаев это изображение ненужно и вредно. Разумеется если случаи когда оно попадает прямо в точку и умело и емко отражает суть статьи... но это не ваш случай.


    1. pda0
      24.12.2023 15:49

      Ещё сжечь все книги O'Reilly... :-D


      1. bogolt
        24.12.2023 15:49

        Они там хотя бы смешные


  1. GrimAnEye
    24.12.2023 15:49

    Статья - откровенный мусор и заманиловка на отличные специальные курсы!

    Все кто знаком с Go (как упомянуто в начале статьи), уже по любому знакомы и с подобными крупными библиотеками, особенно, которые релизятся с 2016 года.

    Всё что описано - почти полная перепечатка официальных примеров, например чат, без внесения чего либо нового или конструктивно отличающегося.

    Если на курсах гоняют те же официальные примеры из документации - то они полная хрень


  1. NeoCode
    24.12.2023 15:49

    У меня есть идея, как использовать websocket для практических целей. Сейчас распространены браузерные расширения-прокси. Но иногда нужно использовать прокси не только из браузера, но и из других программ. Да, можно озаботиться независимым прокси-сервером или VPN, но почему не использовать эти многочисленные браузерные расширения?

    Например, в соцсети VK есть группы, которые не удалены, а заблокированы по региональному признаку. Я сейчас в рамках изучения Go пишу пет-проект, позволяющий скачивать из VK интересующую меня информацию с помощью vkapi в локальную БД sqlite и работать с ней через веб-интерфейс на локалхосте. Но проблема в том, что vk-токен привязан к ip-адресу. Т.е. если токен получен с некоторого ip, то и использовать его можно только с этого ip. Токен получается в браузере, а используется в стороннем приложении на Go. Пока работаем с реального ip, всё хорошо. Но если браузер использует прокси-расширение, соответственно и стороннему приложению необходимо как-то использовать то же самое прокси-расширение... Но расширение функционирует только в браузере!

    Идея в следующем. Что если написать еще одно расширение, которое будет выступать как прокси-сервер, доступный в системе через websocket? С ним связывается программа на Go, которая с другой стороны работает как обычный локальный прокси. То есть получаем связку "браузерное прокси-расширение <-> браузерное websocket-расширение <-> локальный прокси <-> приложение, использующее прокси". Вот такая идея.


  1. gudvinr
    24.12.2023 15:49

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

    gorilla toolkit уже не поддерживается, и никто не вызвался быть новым мейнтейнером. Так что это не попсовая библиотека уже, а постепенно кальцифицирующиеся останки


    1. ASD2003ru
      24.12.2023 15:49

      Вроде RH взяли. Недавно даже какое то шевеление было.


      1. gudvinr
        24.12.2023 15:49

        Моя ошибка, действительно в июле убрали из архива. Но в любом случае до этого как минимум 3 года репозитории стагнировали и пока не понятно, что будет. Может, будет развиваться, а может подобрали чтобы не развалился легаси код, который на горилле завязан


  1. azudem
    24.12.2023 15:49

    У вас орфографическая ошибка уже в названии статьи.