WebSocket en Go: técnicas avanzadas para aplicaciones en tiempo real
\n\nEl desarrollo de aplicaciones en tiempo real en Go es uno de los puntos fuertes del lenguaje. El soporte integrado para concurrencia a través de gorutinas y canales hace de Go una opción ideal para servidores WebSocket. Sin embargo, cuando se trata de sistemas en producción con miles de conexiones simultáneas, el simple ejemplo "Hola, WebSocket" deja de funcionar. En este artículo, analizaremos técnicas avanzadas: desde la optimización del pool de conexiones hasta la escalabilidad a través de brokers de mensajes.
\n\n1. Arquitectura del pool de conexiones: gestión de miles de clientes
\n\nEn el ejemplo básico, creamos una gorutina separada para cada conexión. Esto es aceptable para 100-200 clientes, pero con 10 000+ conexiones, el consumo de memoria y la sobrecarga del cambio de contexto se vuelven críticos. El enfoque avanzado es utilizar un pool de conexiones con un número fijo de workers y canales.
\n\n// Pool — pool de conexiones con límite de gorutinas\ntype Pool struct {\n connections map[*websocket.Conn]bool\n broadcast chan []byte\n register chan *websocket.Conn\n unregister chan *websocket.Conn\n}\n\nfunc NewPool(maxGoroutines int) *Pool {\n return &Pool{\n connections: make(map[*websocket.Conn]bool),\n broadcast: make(chan []byte, 256),\n register: make(chan *websocket.Conn),\n unregister: make(chan *websocket.Conn),\n }\n}\n\nfunc (pool *Pool) Start() {\n for {\n select {\n case conn := <-pool.register:\n pool.connections[conn] = true\n case conn := <-pool.unregister:\n if _, ok := pool.connections[conn]; ok {\n delete(pool.connections, conn)\n conn.Close()\n }\n case message := <-pool.broadcast:\n for conn := range pool.connections {\n go func(c *websocket.Conn) {\n if err := c.WriteMessage(websocket.TextMessage, message); err != nil {\n pool.unregister <- c\n }\n }(conn)\n }\n }\n }\n}\n\n¿Por qué funciona esto? Los canales actúan como búferes, evitando bloqueos. Un único select gestiona todos los eventos, y las gorutinas para escritura se crean solo cuando es necesario. Esto reduce la carga máxima en el GC (recolector de basura).
\n\n2. Apagado graceful y gestión del estado de las conexiones
\n\nAl reiniciar el servidor, es importante cerrar correctamente todas las conexiones WebSocket, enviando a los clientes un mensaje de finalización. Ignorar este paso provoca conexiones "colgantes" y fugas de memoria.
\n\n// Server — servidor WebSocket con apagado graceful\ntype Server struct {\n pool *Pool\n httpSrv *http.Server\n}\n\nfunc (s *Server) Shutdown(ctx context.Context) error {\n // 1. Cerramos el servidor HTTP (no se aceptan nuevas conexiones)\n if err := s.httpSrv.Shutdown(ctx); err != nil {\n return err\n }\n\n // 2. Enviamos a todos los clientes un mensaje de finalización\n shutdownMsg := []byte(`{"type":"shutdown","message":"el servidor se está cerrando"}`)\n s.pool.broadcast <- shutdownMsg\n\n // 3. Cerramos todas las conexiones en el pool\n for conn := range s.pool.connections {\n conn.WriteMessage(websocket.CloseMessage, \n websocket.FormatCloseMessage(websocket.CloseNormalClosure, "cierre del servidor"))\n conn.Close()\n }\n\n return nil\n}\n\n// Uso en main\nfunc main() {\n pool := NewPool(1000)\n go pool.Start()\n\n srv := &Server{pool: pool}\n srv.httpSrv = &http.Server{Addr: ":8080"}\n\n // Manejo de señales\n go func() {\n sigint := make(chan os.Signal, 1)\n signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)\n <-sigint\n ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n defer cancel()\n srv.Shutdown(ctx)\n }()\n\n log.Fatal(srv.httpSrv.ListenAndServ