Introducción: Más Allá del Simple WebSocket en Go
Los ejemplos básicos de WebSocket en Go suelen mostrar una simple conexión de eco. En proyectos de producción reales, desde terminales de trading hasta juegos multijugador y sistemas de notificación, se requiere mucho más: gestión de salas, apagado graceful, escalabilidad y manejo adecuado de errores. En este artículo, analizaremos un patrón avanzado para construir aplicaciones en tiempo real en Go utilizando la librería gorilla/websocket.
Crearemos una arquitectura basada en Hub (despachador central) y Client (conexión individual), añadiremos soporte para salas, un mecanismo de tolerancia a fallos y mostraremos cómo apagar correctamente el servidor.
Arquitectura Hub y Client: La Base de un Servidor en Tiempo Real
¿Por qué Hub?
Cada cliente WebSocket es una goroutine separada. Gestionarlos directamente desde un manejador HTTP es peligroso: provoca condiciones de carrera y fugas de memoria. El patrón Hub resuelve este problema actuando como un canal único para registro, eliminación y difusión de mensajes.
Implementación del Hub
package main
import ( "log" "sync")
type Client struct { hub *Hub send chan []byte room string}
type Hub struct { clients map[*Client]bool broadcast chan Message register chan *Client unregister chan *Client mu sync.RWMutex}
type Message struct { Room string Data []byte Sender *Client}
func NewHub() *Hub { return &Hub{ clients: make(map[*Client]bool), broadcast: make(chan Message, 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() log.Printf("Client connected. Total: %d", len(h.clients))
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 { // Send only to the target room if client.room == message.Room || message.Room == "" { select { case client.send <- message.Data: default: // If the channel is full — disconnect the client h.mu.RUnlock() h.unregister <- client h.mu.RLock() } } } h.mu.RUnlock() } }}Puntos clave:
- Channels — register, unregister, broadcast — sincronizan goroutines de forma segura.
- RWMutex — permite la lectura simultánea de la lista de clientes pero bloquea las escrituras.
- Enrutamiento basado en salas — un mensaje se envía solo a los clientes en la misma sala.
Gestión de Salas y Suscripciones
Las salas son grupos lógicos de clientes. Por ejemplo, en un chat, cada conversación tiene su propia sala. En nuestro Hub, la sala se define mediante el campo room en Client. Veamos cómo un cliente se suscribe a una sala.
Manejador HTTP con Actualización WebSocket
import ( "net/http" "github.com/gorilla/websocket")
var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true // Para producción, reemplazar con verificación de origen },}
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err !=