WebSocket на Go: создание real-time приложений на Golang
Современные веб-приложения всё чаще требуют мгновенной передачи данных между клиентом и сервером. Традиционный HTTP-запрос-ответ не подходит для чатов, онлайн-игр, торговых платформ и мониторинга в реальном времени. На помощь приходит протокол WebSocket, который устанавливает постоянное двустороннее соединение. В этой статье на pythonlib.ru мы разберём, как создать real-time приложение на Go (Golang) с использованием WebSocket.
Go идеально подходит для этой задачи благодаря встроенной поддержке конкурентности (горутины и каналы) и высокой производительности. Вместе мы напишем простой WebSocket-сервер, реализуем чат и рассмотрим типичные проблемы.
Что такое WebSocket и зачем он нужен в Go?
WebSocket — это протокол, работающий поверх TCP. Он инициируется как HTTP-запрос (handshake), после чего соединение "апгрейдится" до постоянного двустороннего канала. В отличие от long polling или Server-Sent Events, WebSocket позволяет серверу и клиенту отправлять данные в любой момент без повторного создания соединения.
Преимущества использования WebSocket на Go:
- Конкурентность: горутины легко обрабатывают тысячи одновременных подключений.
- Производительность: минимальное потребление памяти на каждое соединение.
- Простота: стандартная библиотека
net/httpи пакетgorilla/websocketделают код лаконичным.
WebSocket часто применяется в:
- Чат-системах и мессенджерах
- Онлайн-редакторах (Google Docs)
- Торговых терминалах (биржевые котировки)
- Играх и симуляторах
Установка и настройка WebSocket в Go
В Go нет встроенной поддержки WebSocket в стандартной библиотеке, но есть отличный пакет gorilla/websocket. Установим его:
go get github.com/gorilla/websocket
Создадим минимальный HTTP-сервер, который будет обслуживать WebSocket-соединения. Для начала напишем функцию-обработчик, которая "апгрейдит" HTTP-запрос до WebSocket.
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // разрешаем все источники (для разработки)
},
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("Upgrade error:", err)
return
}
defer conn.Close()
// Бесконечный цикл чтения сообщений
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("Read error:", err)
break
}
log.Printf("Received: %s", message)
// Эхо-ответ: отправляем то же сообщение обратно
if err := conn.WriteMessage(messageType, message); err != nil {
log.Println("Write error:", err)
break
}
}
}
func main() {
http.HandleFunc("/ws", handleWebSocket)
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Этот код создаёт простой эхо-сервер. Клиент подключается к ws://localhost:8080/ws и получает обратно все отправленные сообщения.
Создание real-time чата на Go с WebSocket
Теперь усложним задачу: сделаем чат, где все подключённые клиенты видят сообщения друг друга. Для этого нам понадобится управлять списком соединений и рассылать сообщения.
Основные компоненты:
- Hub — центральный менеджер, который регистрирует/удаляет клиентов и рассылает сообщения.
- Client — обёртка над WebSocket-соединением, работающая в своей горутине.
Реализуем Hub с использованием каналов:
type Client struct {
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)
}
}
}
}
}
Теперь модифицируем обработчик WebSocket, чтобы он регистрировал клиента в Hub и запускал две горутины: на чтение и на запись.
func handleChatWebSocket(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := &Client{conn: conn, send: make(chan []byte, 256)}
hub.register <- client
// Горутина записи: берёт сообщения из канала и отправляет в сокет
go func() {
defer func() {
conn.Close()
}()
for message := range client.send {
if err := conn.WriteMessage(websocket.TextMessage, message); err != nil {
return
}
}
}()
// Горутина чтения: читает из сокета и отправляет в broadcast
go func() {
defer func() {
hub.unregister <- client
conn.Close()
}()
for {
_, message, err := conn.ReadMessage()
if err != nil {
break
}
hub.broadcast <- message
}
}()
}
В main() запускаем Hub и регистрируем новый обработчик:
func main() {
hub := NewHub()
go hub.Run()
http.HandleFunc("/chat", func(w http.ResponseWriter, r *http.Request) {
handleChatWebSocket(hub, w, r)
})
log.Println("Chat server started on :8080")
log.F