WebSockets com gorilla/websocket
1. Introdução ao WebSocket e gorilla/websocket
WebSockets representam uma evolução significativa na comunicação cliente-servidor, oferecendo conexões full-duplex persistentes através de uma única conexão TCP. Diferentemente do HTTP polling tradicional, onde o cliente precisa fazer requisições repetidas para verificar novas informações, ou do Server-Sent Events (SSE), que permite apenas comunicação unidirecional do servidor para o cliente, os WebSockets permitem que ambas as partes enviem dados a qualquer momento, de forma eficiente e com baixa latência.
A biblioteca gorilla/websocket é a implementação mais madura e amplamente adotada no ecossistema Go. Ela oferece uma API limpa e segura para concorrência, com suporte completo ao protocolo WebSocket RFC 6455. Sua popularidade se deve à estabilidade, documentação extensa e integração natural com o padrão de concorrência do Go.
Para instalar a biblioteca, execute:
go get github.com/gorilla/websocket
2. Estabelecendo uma Conexão WebSocket
O coração de um servidor WebSocket em Go é o Upgrader, responsável por realizar o handshake que transforma uma requisição HTTP comum em uma conexão WebSocket.
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // Em produção, valide a origem adequadamente
},
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Erro no handshake: %v", err)
return
}
defer conn.Close()
// Conexão estabelecida com sucesso
log.Printf("Cliente conectado: %s", conn.RemoteAddr())
}
A configuração do Upgrader é crucial: CheckOrigin deve ser configurado para validar origens em produção, prevenindo ataques CSRF. ReadBufferSize e WriteBufferSize definem os buffers para operações de I/O.
3. Leitura e Escrita de Mensagens
O gorilla/websocket suporta diferentes tipos de mensagens: Text, Binary, Close, Ping e Pong. A leitura e escrita seguem um padrão simples:
func handleConnection(conn *websocket.Conn) {
defer conn.Close()
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Printf("Erro ao ler mensagem: %v", err)
break
}
switch messageType {
case websocket.TextMessage:
log.Printf("Mensagem texto recebida: %s", message)
// Processar mensagem de texto
case websocket.BinaryMessage:
log.Printf("Mensagem binária recebida: %d bytes", len(message))
// Processar dados binários
case websocket.CloseMessage:
log.Println("Cliente solicitou fechamento")
return
case websocket.PingMessage:
conn.WriteMessage(websocket.PongMessage, nil)
}
// Ecoar mensagem de volta
if err := conn.WriteMessage(messageType, message); err != nil {
log.Printf("Erro ao escrever mensagem: %v", err)
break
}
}
}
Para escrita concorrente segura, utilize o WriteLock ou o método WriteJSON:
// Escrita segura com mutex interno
conn.WriteMessage(websocket.TextMessage, []byte("mensagem segura"))
// Envio de dados JSON
type Message struct {
User string `json:"user"`
Content string `json:"content"`
}
conn.WriteJSON(Message{User: "Alice", Content: "Olá mundo!"})
4. Gerenciamento de Conexões Concorrentes
Em aplicações reais, precisamos gerenciar múltiplas conexões simultaneamente. Um padrão comum é o Hub centralizador:
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
mu sync.RWMutex
}
func newHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
broadcast: make(chan []byte, 256),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
h.mu.Unlock()
case message := <-h.broadcast:
h.mu.RLock()
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
h.mu.RUnlock()
}
}
}
5. Controle de Ciclo de Vida e Heartbeat
Manter conexões saudáveis requer implementação de ping/pong e timeouts:
func handleClient(hub *Hub, conn *websocket.Conn) {
client := &Client{
conn: conn,
send: make(chan []byte, 256),
}
hub.register <- client
// Configurar handlers de ping/pong
conn.SetPingHandler(func(appData string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return conn.WriteMessage(websocket.PongMessage, []byte(appData))
})
conn.SetPongHandler(func(appData string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
// Goroutine para enviar pings periódicos
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
case <-client.send:
// Canal fechado, encerrar
return
}
}
}()
// Loop principal de leitura
defer func() {
hub.unregister <- client
conn.Close()
}()
for {
_, message, err := conn.ReadMessage()
if err != nil {
break
}
hub.broadcast <- message
}
}
6. Tratamento de Erros e Reconexão
Erros comuns incluem CloseError, ErrCloseSent e ErrBadHandshake. Implemente uma estratégia de reconexão no cliente:
func connectWithRetry(url string) (*websocket.Conn, error) {
var conn *websocket.Conn
var err error
for retries := 0; retries < 5; retries++ {
conn, _, err = websocket.DefaultDialer.Dial(url, nil)
if err == nil {
return conn, nil
}
if closeErr, ok := err.(*websocket.CloseError); ok {
log.Printf("Erro de fechamento: %d", closeErr.Code)
if closeErr.Code == websocket.CloseNormalClosure {
return nil, err
}
}
waitTime := time.Duration(math.Pow(2, float64(retries))) * time.Second
log.Printf("Tentativa %d falhou, reconectando em %v", retries+1, waitTime)
time.Sleep(waitTime)
}
return nil, fmt.Errorf("falha após 5 tentativas: %v", err)
}
7. Exemplo Prático: Chat em Tempo Real
Aqui está um servidor de chat completo integrando todos os conceitos:
package main
import (
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
type ChatMessage struct {
User string `json:"user"`
Content string `json:"content"`
Time int64 `json:"time"`
}
type ChatClient struct {
conn *websocket.Conn
send chan ChatMessage
user string
}
type ChatHub struct {
clients map[*ChatClient]bool
broadcast chan ChatMessage
register chan *ChatClient
unregister chan *ChatClient
mu sync.RWMutex
}
func newChatHub() *ChatHub {
return &ChatHub{
clients: make(map[*ChatClient]bool),
broadcast: make(chan ChatMessage, 256),
register: make(chan *ChatClient),
unregister: make(chan *ChatClient),
}
}
func (h *ChatHub) run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
log.Printf("Usuário %s conectado", client.user)
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
h.mu.Unlock()
case message := <-h.broadcast:
h.mu.RLock()
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
h.mu.RUnlock()
}
}
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func handleChat(hub *ChatHub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Erro no upgrade: %v", err)
return
}
client := &ChatClient{
conn: conn,
send: make(chan ChatMessage, 256),
user: r.URL.Query().Get("user"),
}
if client.user == "" {
client.user = "Anônimo"
}
hub.register <- client
// Configurar heartbeat
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
// Goroutine para escrita
go func() {
ticker := time.NewTicker(30 * time.Second)
defer func() {
ticker.Stop()
conn.Close()
}()
for {
select {
case message, ok := <-client.send:
if !ok {
conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := conn.WriteJSON(message); err != nil {
return
}
case <-ticker.C:
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}()
// Loop de leitura
defer func() {
hub.unregister <- client
conn.Close()
}()
for {
var msg ChatMessage
if err := conn.ReadJSON(&msg); err != nil {
break
}
msg.User = client.user
msg.Time = time.Now().Unix()
hub.broadcast <- msg
}
}
func main() {
hub := newChatHub()
go hub.run()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
handleChat(hub, w, r)
})
log.Println("Servidor WebSocket iniciado na porta :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Referências
- Documentação Oficial gorilla/websocket — Documentação completa da API, incluindo tipos, funções e exemplos detalhados de uso.
- WebSocket RFC 6455 — Especificação oficial do protocolo WebSocket pelo IETF, referência técnica fundamental.
- Tutorial: WebSockets in Go — Guia prático com implementação passo a passo de servidores e clientes WebSocket em Go.
- Gorilla WebSocket Examples — Repositório oficial com exemplos reais de chat, echo server, file watch e outras aplicações.
- Building Real-time Applications with WebSockets — Artigo técnico abordando padrões avançados como broadcast, rooms e autenticação em WebSockets.