Привет, Хабр!
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)
GrimAnEye
24.12.2023 15:49Статья - откровенный мусор и заманиловка на отличные специальные курсы!
Все кто знаком с Go (как упомянуто в начале статьи), уже по любому знакомы и с подобными крупными библиотеками, особенно, которые релизятся с 2016 года.
Всё что описано - почти полная перепечатка официальных примеров, например чат, без внесения чего либо нового или конструктивно отличающегося.
Если на курсах гоняют те же официальные примеры из документации - то они полная хрень
NeoCode
24.12.2023 15:49У меня есть идея, как использовать websocket для практических целей. Сейчас распространены браузерные расширения-прокси. Но иногда нужно использовать прокси не только из браузера, но и из других программ. Да, можно озаботиться независимым прокси-сервером или VPN, но почему не использовать эти многочисленные браузерные расширения?
Например, в соцсети VK есть группы, которые не удалены, а заблокированы по региональному признаку. Я сейчас в рамках изучения Go пишу пет-проект, позволяющий скачивать из VK интересующую меня информацию с помощью vkapi в локальную БД sqlite и работать с ней через веб-интерфейс на локалхосте. Но проблема в том, что vk-токен привязан к ip-адресу. Т.е. если токен получен с некоторого ip, то и использовать его можно только с этого ip. Токен получается в браузере, а используется в стороннем приложении на Go. Пока работаем с реального ip, всё хорошо. Но если браузер использует прокси-расширение, соответственно и стороннему приложению необходимо как-то использовать то же самое прокси-расширение... Но расширение функционирует только в браузере!
Идея в следующем. Что если написать еще одно расширение, которое будет выступать как прокси-сервер, доступный в системе через websocket? С ним связывается программа на Go, которая с другой стороны работает как обычный локальный прокси. То есть получаем связку "браузерное прокси-расширение <-> браузерное websocket-расширение <-> локальный прокси <-> приложение, использующее прокси". Вот такая идея.
gudvinr
24.12.2023 15:49Для работы с WebSocket мы будем использовать попсовую библиотеку
gorilla/websocket
gorilla toolkit уже не поддерживается, и никто не вызвался быть новым мейнтейнером. Так что это не попсовая библиотека уже, а постепенно кальцифицирующиеся останки
ASD2003ru
24.12.2023 15:49Вроде RH взяли. Недавно даже какое то шевеление было.
gudvinr
24.12.2023 15:49Моя ошибка, действительно в июле убрали из архива. Но в любом случае до этого как минимум 3 года репозитории стагнировали и пока не понятно, что будет. Может, будет развиваться, а может подобрали чтобы не развалился легаси код, который на горилле завязан
bogolt
Интересно, если ставить минусы статьям за идиотские нейрокартинки подействует ли это на авторов...
Уважаемый автор. Сюда люди приходят за текстом, а находят статьи из списка по названию. Изображение не являющееся пояснением к чему-либо это визуальный мусор. В большинстве случаев это изображение ненужно и вредно. Разумеется если случаи когда оно попадает прямо в точку и умело и емко отражает суть статьи... но это не ваш случай.
pda0
Ещё сжечь все книги O'Reilly... :-D
bogolt
Они там хотя бы смешные