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