Saltar a contenido

Uso Avanzado de Namespaces

Los namespaces son una característica poderosa de Socket.IO que permite multiplexar múltiples canales de comunicación sobre una única conexión WebSocket.

Tabla de Contenidos

¿Qué son los Namespaces?

Los namespaces son canales de comunicación lógicamente separados que comparten la misma conexión física WebSocket. Cada namespace:

  • Tiene su propio conjunto de eventos
  • Maneja su propia conexión/desconexión
  • Puede tener diferentes handlers
  • Comparte la conexión TCP/WebSocket subyacente

Namespace por Defecto

El namespace por defecto es /. Todos los eventos sin namespace específico van aquí:

// Estos tres códigos son equivalentes
client.On("event", handler)
client.Of("/").On("event", handler)
defaultNs := client.Of("/")
defaultNs.On("event", handler)

Casos de Uso

1. Separación por Funcionalidad

Separa diferentes áreas de tu aplicación en namespaces distintos:

// Chat público
publicChat := client.Of("/chat")
publicChat.On("message", handlePublicMessage)

// Notificaciones del sistema
notifications := client.Of("/notifications")
notifications.On("alert", handleAlert)

// API de datos en tiempo real
dataStream := client.Of("/stream")
dataStream.On("update", handleDataUpdate)

2. Separación por Nivel de Acceso

Usa namespaces para diferentes niveles de permisos:

// Namespace público (todos pueden acceder)
public := client.Of("/public")
public.On("announcement", handleAnnouncement)

// Namespace de usuarios autenticados
authenticated := client.Of("/authenticated")
authenticated.OnConnect(func() {
  authenticated.Emit("auth", map[string]interface{}{
    "token": userToken,
  })
})
authenticated.On("private-data", handlePrivateData)

// Namespace de administradores
admin := client.Of("/admin")
admin.OnConnect(func() {
  admin.Emit("auth", map[string]interface{}{
    "token": adminToken,
    "role": "admin",
  })
})
admin.On("admin-event", handleAdminEvent)

3. Salas Virtuales

Usa namespaces como salas virtuales separadas:

// Diferentes salas de chat
lobby := client.Of("/chat/lobby")
lobby.On("message", handleLobbyMessage)

gameRoom := client.Of("/chat/game-room-1")
gameRoom.On("message", handleGameRoomMessage)

privateRoom := client.Of("/chat/private-123")
privateRoom.On("message", handlePrivateMessage)

4. Microservicios

Cada microservicio puede tener su propio namespace:

// Servicio de usuarios
users := client.Of("/services/users")
users.On("user-update", handleUserUpdate)

// Servicio de pagos
payments := client.Of("/services/payments")
payments.On("payment-status", handlePaymentStatus)

// Servicio de notificaciones
notifications := client.Of("/services/notifications")
notifications.On("notify", handleNotification)

Arquitectura

Ciclo de Vida Independiente

Cada namespace tiene su propio ciclo de vida:

chat := client.Of("/chat")
api := client.Of("/api")

chat.OnConnect(func() {
  fmt.Println("Chat conectado")
  chat.Emit("join", "general")
})

chat.OnDisconnect(func(reason string) {
  fmt.Println("Chat desconectado:", reason)
})

api.OnConnect(func() {
  fmt.Println("API conectada")
  api.Emit("subscribe", []string{"users", "posts"})
})

api.OnDisconnect(func(reason string) {
  fmt.Println("API desconectada:", reason)
})

Eventos Aislados

Los eventos en un namespace no afectan otros namespaces:

chat := client.Of("/chat")
api := client.Of("/api")

// Este handler solo recibe eventos de /chat
chat.On("message", func(data ...interface{}) {
  fmt.Println("Mensaje de chat:", data[0])
})

// Este handler solo recibe eventos de /api
api.On("message", func(data ...interface{}) {
  fmt.Println("Mensaje de API:", data[0])
})

// Emitir en /chat no afecta /api
chat.Emit("message", "Hola desde chat")
api.Emit("message", "Hola desde API")

Patrones de Implementación

Patrón: Manager de Namespaces

Gestiona múltiples namespaces de forma centralizada:

type NamespaceManager struct {
  client     *socketio.Socket
  namespaces map[string]*socketio.Namespace
}

func NewNamespaceManager(url string) *NamespaceManager {
  return &NamespaceManager{
    client:     socketio.New(url),
    namespaces: make(map[string]*socketio.Namespace),
  }
}

func (m *NamespaceManager) Get(path string) *socketio.Namespace {
  if ns, exists := m.namespaces[path]; exists {
    return ns
  }

  ns := m.client.Of(path)
  m.namespaces[path] = ns
  return ns
}

func (m *NamespaceManager) Close() error {
  return m.client.Close()
}

// Uso
manager := NewNamespaceManager("ws://localhost:3000")
defer manager.Close()

chat := manager.Get("/chat")
chat.On("message", handleChatMessage)

api := manager.Get("/api")
api.On("data", handleAPIData)

Patrón: Namespace Wrapper

Envuelve namespaces con lógica adicional:

type ChatNamespace struct {
  ns       *socketio.Namespace
  username string
  room     string
}

func NewChatNamespace(client *socketio.Socket, username string) *ChatNamespace {
  cn := &ChatNamespace{
    ns:       client.Of("/chat"),
    username: username,
  }

  cn.setupHandlers()
  return cn
}

func (cn *ChatNamespace) setupHandlers() {
  cn.ns.OnConnect(func() {
    fmt.Printf("%s conectado al chat\n", cn.username)
  })

  cn.ns.On("message", func(data ...interface{}) {
    user := data[0].(string)
    msg := data[1].(string)
    fmt.Printf("[%s] %s: %s\n", cn.room, user, msg)
  })
}

func (cn *ChatNamespace) JoinRoom(room string) {
  cn.room = room
  cn.ns.Emit("join-room", room)
}

func (cn *ChatNamespace) SendMessage(message string) {
  cn.ns.Emit("message", cn.username, message)
}

func (cn *ChatNamespace) LeaveRoom() {
  if cn.room != "" {
    cn.ns.Emit("leave-room", cn.room)
    cn.room = ""
  }
}

// Uso
client := socketio.New("ws://localhost:3000")
chat := NewChatNamespace(client, "Juan")

chat.JoinRoom("general")
chat.SendMessage("Hola a todos!")

Patrón: Namespace con Estado

Mantén estado específico por namespace:

type APINamespace struct {
  ns          *socketio.Namespace
  subscriptions []string
  cache       map[string]interface{}
  mu          sync.RWMutex
}

func NewAPINamespace(client *socketio.Socket) *APINamespace {
  api := &APINamespace{
    ns:    client.Of("/api"),
    cache: make(map[string]interface{}),
  }

  api.setupHandlers()
  return api
}

func (a *APINamespace) setupHandlers() {
  a.ns.OnConnect(func() {
    // Re-subscribirse automáticamente al reconectar
    if len(a.subscriptions) > 0 {
      a.ns.Emit("subscribe", a.subscriptions)
    }
  })

  a.ns.On("update", func(data ...interface{}) {
    key := data[0].(string)
    value := data[1]

    a.mu.Lock()
    a.cache[key] = value
    a.mu.Unlock()

    fmt.Printf("Cache actualizado: %s\n", key)
  })
}

func (a *APINamespace) Subscribe(topics []string) {
  a.subscriptions = append(a.subscriptions, topics...)
  a.ns.Emit("subscribe", topics)
}

func (a *APINamespace) Get(key string) (interface{}, bool) {
  a.mu.RLock()
  defer a.mu.RUnlock()

  value, exists := a.cache[key]
  return value, exists
}

// Uso
client := socketio.New("ws://localhost:3000")
api := NewAPINamespace(client)

api.Subscribe([]string{"users", "posts", "comments"})

// Usar cache
if value, ok := api.Get("users"); ok {
  fmt.Println("Usuarios:", value)
}

Patrón: Dynamic Namespaces

Crea y destruye namespaces dinámicamente:

type DynamicNamespaceManager struct {
  client     *socketio.Socket
  namespaces sync.Map // map[string]*socketio.Namespace
}

func NewDynamicNamespaceManager(url string) *DynamicNamespaceManager {
  return &DynamicNamespaceManager{
    client: socketio.New(url),
  }
}

func (m *DynamicNamespaceManager) Join(path string) *socketio.Namespace {
  if ns, loaded := m.namespaces.LoadOrStore(path, m.client.Of(path)); loaded {
    return ns.(*socketio.Namespace)
  }

  ns := m.client.Of(path)
  m.namespaces.Store(path, ns)

  fmt.Printf("Namespace creado: %s\n", path)
  return ns
}

func (m *DynamicNamespaceManager) Leave(path string) {
  if ns, ok := m.namespaces.LoadAndDelete(path); ok {
    fmt.Printf("Namespace eliminado: %s\n", path)
    // Nota: No hay método Close() en Namespace,
    // simplemente deja de usar el namespace
    _ = ns
  }
}

func (m *DynamicNamespaceManager) List() []string {
  var paths []string
  m.namespaces.Range(func(key, value interface{}) bool {
    paths = append(paths, key.(string))
    return true
  })
  return paths
}

// Uso
manager := NewDynamicNamespaceManager("ws://localhost:3000")

// Usuario se une a una sala
roomNs := manager.Join("/rooms/room-123")
roomNs.On("message", handleRoomMessage)

// Usuario sale de la sala
manager.Leave("/rooms/room-123")

// Ver salas activas
fmt.Println("Salas activas:", manager.List())

Mejores Prácticas

1. Organización Clara

Usa convenciones de nombres claras y consistentes:

// ✅ Bueno: estructura clara y jerárquica
client.Of("/chat/public")
client.Of("/chat/private")
client.Of("/api/v1")
client.Of("/api/v2")
client.Of("/admin/users")
client.Of("/admin/settings")

// ❌ Malo: nombres inconsistentes
client.Of("/chat")
client.Of("/PRIVATE-CHAT")
client.Of("/api_v1")
client.Of("/AdminPanel")

2. Manejo de Conexión por Namespace

Cada namespace debe manejar su propia conexión:

chat := client.Of("/chat")
api := client.Of("/api")

// ✅ Bueno: cada namespace maneja su estado
chat.OnConnect(func() {
  chat.Emit("join", "general")
})

api.OnConnect(func() {
  api.Emit("authenticate", token)
})

// ❌ Malo: asumir que todos se conectan al mismo tiempo
client.OnConnect(func() {
  // Esto solo se ejecuta para el namespace por defecto
  chat.Emit("join", "general")  // Puede fallar si /chat no está conectado
})

3. Evitar Namespace Explosion

No crees demasiados namespaces innecesariamente:

// ✅ Bueno: namespaces por categoría
chat := client.Of("/chat")
chat.Emit("join-room", "general")
chat.Emit("join-room", "random")

// ❌ Malo: un namespace por sala
general := client.Of("/chat/general")
random := client.Of("/chat/random")
tech := client.Of("/chat/tech")
// ... 100 más

4. Documentar Namespaces

Documenta qué hace cada namespace:

// Namespace /chat: Mensajería en tiempo real
// Eventos: message, user-joined, user-left, typing
// Autenticación: Requerida
chat := client.Of("/chat")

// Namespace /api: Datos en tiempo real
// Eventos: update, delete, create
// Autenticación: Token JWT
api := client.Of("/api")

// Namespace /admin: Panel de administración
// Eventos: stats, user-action, system-alert
// Autenticación: Admin token
admin := client.Of("/admin")

5. Testing por Namespace

Prueba cada namespace independientemente:

func TestChatNamespace(t *testing.T) {
  client := socketio.New("ws://localhost:3000")
  defer client.Close()

  chat := client.Of("/chat")

  var received bool
  chat.On("welcome", func(data ...interface{}) {
    received = true
  })

  // Esperar conexión
  time.Sleep(100 * time.Millisecond)

  if !received {
    t.Error("No se recibió evento welcome")
  }
}

6. Cleanup de Namespaces

Limpia namespaces cuando ya no los necesites:

type RoomManager struct {
  client *socketio.Socket
  rooms  sync.Map
}

func (rm *RoomManager) JoinRoom(roomId string) {
  ns := rm.client.Of("/rooms/" + roomId)
  rm.rooms.Store(roomId, ns)

  ns.OnConnect(func() {
    fmt.Printf("Conectado a sala %s\n", roomId)
  })
}

func (rm *RoomManager) LeaveRoom(roomId string) {
  if ns, ok := rm.rooms.LoadAndDelete(roomId); ok {
    // El namespace dejará de recibir eventos
    fmt.Printf("Saliendo de sala %s\n", roomId)
    _ = ns
  }
}

func (rm *RoomManager) Cleanup() {
  rm.rooms.Range(func(key, value interface{}) bool {
    rm.rooms.Delete(key)
    return true
  })
}

Ejemplo Completo: Sistema de Chat Multi-Sala

package main

import (
  "fmt"
  "sync"
  socketio "github.com/arcaela/socket.io-client-go"
)

type ChatSystem struct {
  client     *socketio.Socket
  user       string
  rooms      map[string]*socketio.Namespace
  roomsMutex sync.RWMutex
}

func NewChatSystem(url, username string) *ChatSystem {
  return &ChatSystem{
    client: socketio.New(url),
    user:   username,
    rooms:  make(map[string]*socketio.Namespace),
  }
}

func (cs *ChatSystem) JoinRoom(roomName string) error {
  cs.roomsMutex.Lock()
  defer cs.roomsMutex.Unlock()

  if _, exists := cs.rooms[roomName]; exists {
    return fmt.Errorf("ya estás en la sala %s", roomName)
  }

  // Crear namespace para la sala
  room := cs.client.Of("/chat/" + roomName)

  // Configurar handlers
  room.OnConnect(func() {
    fmt.Printf("✅ Conectado a sala: %s\n", roomName)
    room.Emit("join", cs.user)
  })

  room.OnDisconnect(func(reason string) {
    fmt.Printf("❌ Desconectado de sala %s: %s\n", roomName, reason)
  })

  room.On("message", func(data ...interface{}) {
    user := data[0].(string)
    msg := data[1].(string)
    fmt.Printf("[%s] %s: %s\n", roomName, user, msg)
  })

  room.On("user-joined", func(data ...interface{}) {
    user := data[0].(string)
    fmt.Printf("[%s] 👤 %s se unió\n", roomName, user)
  })

  room.On("user-left", func(data ...interface{}) {
    user := data[0].(string)
    fmt.Printf("[%s] 👋 %s salió\n", roomName, user)
  })

  cs.rooms[roomName] = room
  return nil
}

func (cs *ChatSystem) LeaveRoom(roomName string) error {
  cs.roomsMutex.Lock()
  defer cs.roomsMutex.Unlock()

  room, exists := cs.rooms[roomName]
  if !exists {
    return fmt.Errorf("no estás en la sala %s", roomName)
  }

  room.Emit("leave", cs.user)
  delete(cs.rooms, roomName)
  fmt.Printf("Saliste de la sala: %s\n", roomName)

  return nil
}

func (cs *ChatSystem) SendMessage(roomName, message string) error {
  cs.roomsMutex.RLock()
  room, exists := cs.rooms[roomName]
  cs.roomsMutex.RUnlock()

  if !exists {
    return fmt.Errorf("no estás en la sala %s", roomName)
  }

  return room.Emit("message", cs.user, message)
}

func (cs *ChatSystem) ListRooms() []string {
  cs.roomsMutex.RLock()
  defer cs.roomsMutex.RUnlock()

  rooms := make([]string, 0, len(cs.rooms))
  for name := range cs.rooms {
    rooms = append(rooms, name)
  }
  return rooms
}

func (cs *ChatSystem) Close() error {
  return cs.client.Close()
}

func main() {
  chat := NewChatSystem("ws://localhost:3000", "Juan")
  defer chat.Close()

  // Unirse a múltiples salas
  chat.JoinRoom("general")
  chat.JoinRoom("tech")
  chat.JoinRoom("random")

  // Enviar mensajes
  chat.SendMessage("general", "Hola a todos!")
  chat.SendMessage("tech", "¿Alguien usa Go?")

  // Ver salas activas
  fmt.Println("Salas activas:", chat.ListRooms())

  // Salir de una sala
  chat.LeaveRoom("random")

  select {}
}

Ver También