WebSocket на Go: создание real-time приложений
Современные веб-приложения всё чаще требуют мгновенной передачи данных между клиентом и сервером. Традиционные HTTP-запросы с их циклом «запрос-ответ» не подходят для задач, где важна скорость: онлайн-чаты, биржевые котировки, многопользовательские игры или уведомления в реальном времени. Здесь на помощь приходит WebSocket — протокол, обеспечивающий постоянное двустороннее соединение. В этой статье мы разберём, как реализовать WebSocket на Go (Golang), от базового сервера до полноценного real-time приложения.
Что такое WebSocket и почему Go?
WebSocket — это протокол поверх TCP, который позволяет установить постоянное соединение между клиентом (браузером) и сервером. После рукопожатия (handshake) данные передаются в обоих направлениях без дополнительных HTTP-заголовков, что значительно снижает задержки.
Почему Go? Язык Go идеально подходит для WebSocket-серверов благодаря встроенной поддержке конкурентности (горутины и каналы), высокой производительности и простоте написания сетевого кода. В отличие от Node.js, Go не требует внешних циклов событий, а его компилируемая природа даёт преимущество в скорости.
Базовый WebSocket сервер на Go
Для работы с WebSocket в Go мы будем использовать популярную библиотеку gorilla/websocket. Она предоставляет удобный API и обрабатывает детали протокола.
Установите библиотеку:
go get github.com/gorilla/websocket
Теперь напишем простой эхо-сервер, который отправляет обратно всё, что получает от клиента.
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Для разработки. В продакшене проверяйте Origin.
},
}
func echoHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("Upgrade failed:", err)
return
}
defer conn.Close()
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("Read error:", err)
break
}
log.Printf("Получено: %s", message)
if err := conn.WriteMessage(messageType, message); err != nil {
log.Println("Write error:", err)
break
}
}
}
func main() {
http.HandleFunc("/ws", echoHandler)
log.Println("Сервер запущен на :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Объяснение кода:
Upgrader— объект, который «повышает» HTTP-соединение до WebSocket.CheckOrigin— функция проверки источника запроса. В продакшене обязательно настройте её.conn.ReadMessage()иconn.WriteMessage()— основные методы для чтения/записи сообщений.- Цикл
forподдерживает соединение открытым до возникновения ошибки.
Архитектура real-time чата на WebSocket
Эхо-сервер — это основа, но для real-time приложения, например чата, нужна более сложная логика. Рассмотрим архитектуру, где несколько клиентов могут обмениваться сообщениями через сервер.
Основные компоненты:
- Hub — центральный менеджер, хранящий список активных подключений.
- Client — структура, представляющая одно WebSocket-соединение.
- Каналы (channels) — для безопасной передачи данных между горутинами.
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
// Client представляет подключённого пользователя.
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
// Hub управляет всеми клиентами.
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)
}
}
}
}
}
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 {
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
break
}
}
}
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.Println("Чат-сервер запущен на :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Как это работает:
- При подключении клиента создаётся структура
Clientи запускаются две горутины:readPump(чтение) иwritePump(запись). Hubслушает каналы регистрации/отключения и рассылает сообщения всем активным клиентам.- Канал
sendс буфером 256 сообщений предотвращает блокиров