Введение в WebSocket на Go: почему real-time?
Современные веб-приложения всё чаще требуют мгновенной доставки данных. Традиционный HTTP-запрос/ответ уже не справляется с задачами чатов, онлайн-игр, биржевых котировок и панелей мониторинга. Здесь на помощь приходит WebSocket — протокол, обеспечивающий постоянное двустороннее соединение между клиентом и сервером. В этой статье мы разберём, как создавать real-time приложения на Go (Golang) с использованием WebSocket, от установки до продакшн-решений.
Go идеально подходит для WebSocket благодаря своей лёгкой модели конкурентности (горутины) и высокой производительности. Мы пройдём путь от простого эхо-сервера до полноценного чата и системы мониторинга.
Основы WebSocket и настройка окружения
Что такое WebSocket?
WebSocket — это протокол, работающий поверх TCP. Он начинается с HTTP-рукопожатия (upgrade), после чего соединение переключается на постоянный двунаправленный канал. В отличие от HTTP, сервер может отправлять данные клиенту без запроса — это основа real-time.
Установка и библиотеки
В Go стандартной библиотеки для WebSocket нет, но есть популярная библиотека gorilla/websocket. Установим её:
go get github.com/gorilla/websocket
Также можно использовать nhooyr.io/websocket (более современная альтернатива), но в этой статье мы остановимся на gorilla как на самой распространённой.
Первый WebSocket сервер на Go
Напишем простой эхо-сервер, который получает сообщение и отправляет его обратно:
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // для разработки
},
}
func echoHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("Upgrade:", err)
return
}
defer conn.Close()
for {
mt, message, err := conn.ReadMessage()
if err != nil {
log.Println("Read:", err)
break
}
log.Printf("Received: %s", message)
err = conn.WriteMessage(mt, message)
if err != nil {
log.Println("Write:", err)
break
}
}
}
func main() {
http.HandleFunc("/echo", echoHandler)
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Это минимальный рабочий пример. Обратите внимание на Upgrader — он проверяет заголовки HTTP и переключает соединение.
Архитектура real-time приложения: чат на Go
Управление подключениями
Для чата нужно управлять множеством клиентов. Создадим структуру Hub, которая будет хранить все активные соединения и рассылать сообщения:
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
}
func newHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case message := <-h.broadcast:
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}
Чтение и запись для каждого клиента
Каждый клиент запускает две горутины: readPump (чтение из сокета) и writePump (запись в сокет). Это стандартный паттерн для gorilla/websocket:
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
break
}
c.hub.broadcast <- message
}
}
func (c *Client) writePump() {
defer c.conn.Close()
for message := range c.send {
err := c.conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
break
}
}
}
Запуск чата
В функции main запускаем Hub и регистрируем обработчик:
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
hub.register <- client
go client.writePump()
go client.readPump()
}
func main() {
hub := newHub()
go hub.run()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(hub, w, r)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
Этот код образует основу любого real-time чата. Клиент (браузер) подключается к /ws, и все сообщения рассылаются всем участникам.
Продвинутые техники: мониторинг и масштабирование
Real-time мониторинг системы
WebSocket отлично подходит для дашбордов. Создадим сервер, который каждые 2 секунды отправляет клиенту данные о загрузке CPU:
import (
"runtime"
"time"
)
func monitorHandler(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
defer conn.Close()
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
var m runtime.MemStats
runtime.ReadMemStats(&m)
msg := fmt.Sprintf(`{"cpu": %d, "mem": %d}`,
runtime.NumGoroutine(), m.Alloc/1024/1024)
if err := conn.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {
break