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