Saltar a contenido

Reconexión Automática

La reconexión automática es una característica crítica para aplicaciones resilientes en tiempo real. Esta guía cubre configuración y patrones avanzados.

Tabla de Contenidos

Cómo Funciona

Flujo de Reconexión

  1. Desconexión: Se pierde la conexión con el servidor
  2. Espera: Se espera ReconnectDelay antes del primer intento
  3. Intento: Se intenta reconectar
  4. Backoff: Si falla, se incrementa el delay exponencialmente (limitado por ReconnectDelayMax)
  5. Repetir: Se repite hasta ReconnectAttempts veces
  6. Fallo: Si todos los intentos fallan, se emite OnReconnectFailed

Backoff Exponencial

El delay entre intentos crece exponencialmente:

Intento 1: ReconnectDelay (ej: 1s)
Intento 2: ReconnectDelay * 2 (ej: 2s)
Intento 3: ReconnectDelay * 4 (ej: 4s)
Intento 4: min(ReconnectDelay * 8, ReconnectDelayMax) (ej: 5s si max=5s)
Intento 5: ReconnectDelayMax (ej: 5s)

Configuración

Configuración Básica

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 5,                   // Máximo 5 intentos
  ReconnectDelay:    time.Second,         // Primer intento después de 1s
  ReconnectDelayMax: 5 * time.Second,     // Máximo 5s entre intentos
})

Reconexión Agresiva

Para aplicaciones que necesitan reconectarse rápidamente:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 10,                  // Muchos intentos
  ReconnectDelay:    500 * time.Millisecond, // Intentar rápido
  ReconnectDelayMax: 3 * time.Second,     // No esperar mucho
})

Reconexión Conservadora

Para aplicaciones que no quieren saturar el servidor:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 3,                   // Pocos intentos
  ReconnectDelay:    2 * time.Second,     // Esperar más
  ReconnectDelayMax: 30 * time.Second,    // Permitir delays largos
})

Sin Reconexión Automática

Para control manual completo:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 0, // Desactivar reconexión automática
})

client.OnDisconnect(func(reason string) {
  // Implementar lógica personalizada
  go manualReconnect()
})

Reconexión Infinita

Usar con precaución - solo para aplicaciones críticas:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: -1, // Intentos infinitos
  ReconnectDelayMax: 60 * time.Second, // Limitar delay máximo
})

Eventos de Reconexión

OnReconnectAttempt

Se ejecuta antes de cada intento de reconexión:

var attemptTime time.Time

client.OnReconnectAttempt(func(attempt int) {
  attemptTime = time.Now()
  fmt.Printf("🔄 Intento de reconexión #%d a las %s\n",
    attempt, attemptTime.Format("15:04:05"))

  // Lógica adicional
  if attempt > 3 {
    fmt.Println("⚠️  Múltiples intentos fallidos, verifica tu conexión")
  }
})

OnReconnect

Se ejecuta cuando la reconexión es exitosa:

client.OnReconnect(func(attempt int) {
  duration := time.Since(attemptTime)
  fmt.Printf("✅ Reconectado después de %d intentos (%v)\n",
    attempt, duration)

  // Re-subscribirse a eventos
  client.Emit("re-subscribe", previousSubscriptions)

  // Sincronizar estado
  syncLocalState()
})

OnReconnectError

Se ejecuta cuando un intento falla:

client.OnReconnectError(func(err error) {
  fmt.Printf("❌ Error en reconexión: %v\n", err)

  // Diagnóstico
  if strings.Contains(err.Error(), "connection refused") {
    fmt.Println("El servidor puede estar caído")
  } else if strings.Contains(err.Error(), "timeout") {
    fmt.Println("Problema de red o servidor lento")
  }
})

OnReconnectFailed

Se ejecuta cuando todos los intentos fallan:

client.OnReconnectFailed(func() {
  fmt.Println("💥 Reconexión falló después de todos los intentos")

  // Notificar al usuario
  showUserNotification("Conexión perdida", "No se pudo reconectar al servidor")

  // Fallback
  switchToOfflineMode()

  // O solicitar reconexión manual
  promptManualReconnect()
})

Patrones de Implementación

Patrón: Estado de Reconexión

Mantener y mostrar estado de reconexión al usuario:

type ReconnectionState struct {
  Attempting     bool
  Attempt        int
  MaxAttempts    int
  LastError      error
  StartTime      time.Time
  mu             sync.RWMutex
}

func (rs *ReconnectionState) OnAttempt(attempt int) {
  rs.mu.Lock()
  defer rs.mu.Unlock()

  rs.Attempting = true
  rs.Attempt = attempt

  if rs.StartTime.IsZero() {
    rs.StartTime = time.Now()
  }
}

func (rs *ReconnectionState) OnSuccess() {
  rs.mu.Lock()
  defer rs.mu.Unlock()

  duration := time.Since(rs.StartTime)
  fmt.Printf("✅ Reconectado en %v\n", duration)

  rs.reset()
}

func (rs *ReconnectionState) OnError(err error) {
  rs.mu.Lock()
  defer rs.mu.Unlock()

  rs.LastError = err
}

func (rs *ReconnectionState) OnFailed() {
  rs.mu.Lock()
  defer rs.mu.Unlock()

  duration := time.Since(rs.StartTime)
  fmt.Printf("💥 Falló después de %v\n", duration)

  rs.reset()
}

func (rs *ReconnectionState) reset() {
  rs.Attempting = false
  rs.Attempt = 0
  rs.LastError = nil
  rs.StartTime = time.Time{}
}

func (rs *ReconnectionState) GetStatus() string {
  rs.mu.RLock()
  defer rs.mu.RUnlock()

  if !rs.Attempting {
    return "Conectado"
  }

  return fmt.Sprintf("Reconectando... (intento %d/%d)",
    rs.Attempt, rs.MaxAttempts)
}

// Uso
state := &ReconnectionState{MaxAttempts: 5}

client.OnReconnectAttempt(func(attempt int) {
  state.OnAttempt(attempt)
  updateUI(state.GetStatus())
})

client.OnReconnect(func(attempt int) {
  state.OnSuccess()
  updateUI("Conectado")
})

client.OnReconnectError(func(err error) {
  state.OnError(err)
})

client.OnReconnectFailed(func() {
  state.OnFailed()
  updateUI("Desconectado")
})

Patrón: Re-sincronización de Estado

Sincronizar estado de la aplicación después de reconectar:

type AppState struct {
  subscriptions []string
  rooms         []string
  lastMessageId string
  userData      map[string]interface{}
}

func (app *AppState) Save() {
  // Guardar estado en localStorage, disco, etc.
}

func (app *AppState) Restore() {
  // Restaurar estado guardado
}

func setupReconnectionSync(client *socketio.Socket, state *AppState) {
  client.OnDisconnect(func(reason string) {
    // Guardar estado antes de perder conexión
    state.Save()
  })

  client.OnReconnect(func(attempt int) {
    // Restaurar estado
    state.Restore()

    // Re-subscribirse
    for _, subscription := range state.subscriptions {
      client.Emit("subscribe", subscription)
    }

    // Re-unirse a salas
    for _, room := range state.rooms {
      client.Emit("join-room", room)
    }

    // Sincronizar mensajes perdidos
    if state.lastMessageId != "" {
      client.Emit("sync-messages", map[string]interface{}{
        "since": state.lastMessageId,
      })
    }
  })
}

Patrón: Offline Queue

Encolar operaciones mientras está desconectado:

type OfflineQueue struct {
  client *socketio.Socket
  queue  []QueuedEvent
  mu     sync.Mutex
}

type QueuedEvent struct {
  Event string
  Data  []interface{}
  Time  time.Time
}

func NewOfflineQueue(client *socketio.Socket) *OfflineQueue {
  oq := &OfflineQueue{
    client: client,
    queue:  make([]QueuedEvent, 0),
  }

  oq.setupHandlers()
  return oq
}

func (oq *OfflineQueue) setupHandlers() {
  oq.client.OnReconnect(func(attempt int) {
    oq.flush()
  })
}

func (oq *OfflineQueue) Emit(event string, data ...interface{}) {
  if oq.client.IsConnected() {
    oq.client.Emit(event, data...)
  } else {
    oq.enqueue(event, data...)
  }
}

func (oq *OfflineQueue) enqueue(event string, data ...interface{}) {
  oq.mu.Lock()
  defer oq.mu.Unlock()

  oq.queue = append(oq.queue, QueuedEvent{
    Event: event,
    Data:  data,
    Time:  time.Now(),
  })

  fmt.Printf("📦 Evento encolado: %s (total: %d)\n", event, len(oq.queue))
}

func (oq *OfflineQueue) flush() {
  oq.mu.Lock()
  defer oq.mu.Unlock()

  if len(oq.queue) == 0 {
    return
  }

  fmt.Printf("📤 Enviando %d eventos encolados...\n", len(oq.queue))

  for _, qe := range oq.queue {
    // Verificar si el evento no es muy viejo (ej: max 5 minutos)
    if time.Since(qe.Time) < 5*time.Minute {
      oq.client.Emit(qe.Event, qe.Data...)
    } else {
      fmt.Printf("⏱️  Evento muy viejo, descartado: %s\n", qe.Event)
    }
  }

  oq.queue = make([]QueuedEvent, 0)
  fmt.Println("✅ Cola vaciada")
}

// Uso
queue := NewOfflineQueue(client)

// Usar queue en lugar de client directamente
queue.Emit("message", "Hola") // Se enviará inmediatamente o se encolará

Patrón: Reconexión con Backoff Personalizado

Implementar estrategia de backoff personalizada:

type CustomBackoff struct {
  client         *socketio.Socket
  attempts       int
  maxAttempts    int
  baseDelay      time.Duration
  maxDelay       time.Duration
  multiplier     float64
  jitter         bool
  reconnecting   bool
  mu             sync.Mutex
}

func NewCustomBackoff(client *socketio.Socket) *CustomBackoff {
  cb := &CustomBackoff{
    client:      client,
    maxAttempts: 10,
    baseDelay:   time.Second,
    maxDelay:    30 * time.Second,
    multiplier:  1.5, // Backoff exponencial con factor 1.5
    jitter:      true, // Agregar jitter para evitar thundering herd
  }

  cb.setupHandlers()
  return cb
}

func (cb *CustomBackoff) setupHandlers() {
  cb.client.OnDisconnect(func(reason string) {
    if reason != "io client disconnect" {
      go cb.startReconnection()
    }
  })
}

func (cb *CustomBackoff) startReconnection() {
  cb.mu.Lock()
  if cb.reconnecting {
    cb.mu.Unlock()
    return
  }
  cb.reconnecting = true
  cb.attempts = 0
  cb.mu.Unlock()

  defer func() {
    cb.mu.Lock()
    cb.reconnecting = false
    cb.mu.Unlock()
  }()

  for cb.attempts < cb.maxAttempts {
    cb.attempts++

    // Calcular delay
    delay := cb.calculateDelay()
    fmt.Printf("⏱️  Esperando %v antes del intento %d/%d\n",
      delay, cb.attempts, cb.maxAttempts)

    time.Sleep(delay)

    // Intentar reconectar
    fmt.Printf("🔄 Intentando reconectar... (#%d)\n", cb.attempts)

    if cb.client.IsConnected() {
      fmt.Println("✅ Reconectado exitosamente")
      cb.attempts = 0
      return
    }
  }

  fmt.Println("💥 Reconexión falló después de todos los intentos")
}

func (cb *CustomBackoff) calculateDelay() time.Duration {
  // Backoff exponencial: baseDelay * (multiplier ^ (attempts - 1))
  delay := float64(cb.baseDelay) * math.Pow(cb.multiplier, float64(cb.attempts-1))

  // Limitar al máximo
  if delay > float64(cb.maxDelay) {
    delay = float64(cb.maxDelay)
  }

  // Agregar jitter (randomización ±25%)
  if cb.jitter {
    jitterAmount := delay * 0.25
    delay += (rand.Float64()*2 - 1) * jitterAmount
  }

  return time.Duration(delay)
}

Estrategias Avanzadas

Estrategia: Adaptativa según Red

Ajustar estrategia según calidad de red:

type AdaptiveReconnection struct {
  client           *socketio.Socket
  networkQuality   string // "good", "poor", "offline"
  failureCount     int
  successCount     int
}

func (ar *AdaptiveReconnection) adjustStrategy() {
  if ar.failureCount > 3 {
    ar.networkQuality = "poor"
  } else if ar.successCount > 5 {
    ar.networkQuality = "good"
  }

  // Ajustar configuración según calidad
  switch ar.networkQuality {
  case "good":
    // Red buena: intentar rápido
    ar.client.ReconnectDelay = time.Second
    ar.client.ReconnectDelayMax = 5 * time.Second
    ar.client.ReconnectAttempts = 5

  case "poor":
    // Red mala: ser más conservador
    ar.client.ReconnectDelay = 3 * time.Second
    ar.client.ReconnectDelayMax = 30 * time.Second
    ar.client.ReconnectAttempts = 10

  case "offline":
    // Offline: esperar mucho entre intentos
    ar.client.ReconnectDelay = 10 * time.Second
    ar.client.ReconnectDelayMax = 60 * time.Second
    ar.client.ReconnectAttempts = -1 // Infinito
  }
}

Estrategia: Health Checks

Verificar salud de conexión proactivamente:

type ConnectionHealthChecker struct {
  client      *socketio.Socket
  interval    time.Duration
  timeout     time.Duration
  stop        chan struct{}
  unhealthy   bool
}

func NewHealthChecker(client *socketio.Socket) *ConnectionHealthChecker {
  hc := &ConnectionHealthChecker{
    client:   client,
    interval: 30 * time.Second,
    timeout:  5 * time.Second,
    stop:     make(chan struct{}),
  }

  go hc.start()
  return hc
}

func (hc *ConnectionHealthChecker) start() {
  ticker := time.NewTicker(hc.interval)
  defer ticker.Stop()

  for {
    select {
    case <-hc.stop:
      return
    case <-ticker.C:
      hc.check()
    }
  }
}

func (hc *ConnectionHealthChecker) check() {
  if !hc.client.IsConnected() {
    return
  }

  done := make(chan bool, 1)

  // Enviar ping
  hc.client.EmitWithAck("ping", func(response ...interface{}) {
    done <- response != nil
  }, time.Now().Unix())

  // Esperar respuesta con timeout
  select {
  case healthy := <-done:
    if healthy {
      hc.unhealthy = false
      fmt.Println("💚 Conexión saludable")
    } else {
      hc.handleUnhealthy()
    }
  case <-time.After(hc.timeout):
    hc.handleUnhealthy()
  }
}

func (hc *ConnectionHealthChecker) handleUnhealthy() {
  if !hc.unhealthy {
    hc.unhealthy = true
    fmt.Println("⚠️  Conexión no saludable, forzando reconexión...")

    // Forzar reconexión
    hc.client.Close()
  }
}

func (hc *ConnectionHealthChecker) Stop() {
  close(hc.stop)
}

Estrategia: Fallback a HTTP Long-Polling

Cambiar a transporte más confiable si WebSocket falla:

type FallbackTransport struct {
  client         *socketio.Socket
  wsURL          string
  pollingURL     string
  failureCount   int
  usingPolling   bool
}

func (ft *FallbackTransport) setupHandlers() {
  ft.client.OnReconnectError(func(err error) {
    ft.failureCount++

    // Después de 3 fallos, intentar polling
    if ft.failureCount >= 3 && !ft.usingPolling {
      fmt.Println("🔄 Cambiando a HTTP Long-Polling...")
      ft.switchToPolling()
    }
  })

  ft.client.OnReconnect(func(attempt int) {
    ft.failureCount = 0

    // Intentar volver a WebSocket después de reconexión exitosa
    if ft.usingPolling {
      go ft.tryWebSocketAgain()
    }
  })
}

func (ft *FallbackTransport) switchToPolling() {
  ft.client.Close()
  // Crear nuevo cliente con polling (requiere modificación en la librería)
  // ft.client = socketio.NewWithTransport(ft.pollingURL, "polling")
  ft.usingPolling = true
}

func (ft *FallbackTransport) tryWebSocketAgain() {
  time.Sleep(5 * time.Minute) // Esperar antes de reintentar

  if ft.usingPolling {
    fmt.Println("🔄 Intentando volver a WebSocket...")
    // Intentar crear cliente WebSocket
    // Si falla, mantener polling
  }
}

Mejores Prácticas

1. Configurar Timeouts Apropiados

// ✅ Bueno: configurar todos los timeouts relacionados
client := socketio.New("ws://localhost:3000", socketio.Options{
  Timeout:           30 * time.Second, // Conexión inicial
  AckTimeout:        5 * time.Second,  // Acknowledgments
  ReconnectDelay:    time.Second,
  ReconnectDelayMax: 5 * time.Second,
  ReconnectAttempts: 5,
})

2. Manejar Todos los Eventos

// ✅ Bueno: manejar todos los eventos de reconexión
client.OnReconnectAttempt(func(attempt int) {
  log.Printf("Reconectando... (#%d)", attempt)
})

client.OnReconnect(func(attempt int) {
  log.Printf("Reconectado (#%d)", attempt)
  resyncState()
})

client.OnReconnectError(func(err error) {
  log.Printf("Error: %v", err)
})

client.OnReconnectFailed(func() {
  log.Println("Reconexión falló")
  notifyUser()
})

3. Re-subscribirse después de Reconectar

// ✅ Bueno: siempre re-subscribirse
var subscriptions []string

client.OnConnect(func() {
  for _, sub := range subscriptions {
    client.Emit("subscribe", sub)
  }
})

client.OnReconnect(func(attempt int) {
  // Re-subscribirse automáticamente
  for _, sub := range subscriptions {
    client.Emit("subscribe", sub)
  }
})

4. No Confiar en Conexión Siempre Disponible

// ✅ Bueno: verificar conexión antes de operaciones críticas
if !client.IsConnected() {
  return fmt.Errorf("sin conexión")
}

client.Emit("important-event", data)

// ❌ Malo: asumir que siempre está conectado
client.Emit("important-event", data) // Puede fallar silenciosamente

5. Notificar al Usuario

// ✅ Bueno: informar al usuario sobre el estado
client.OnReconnectAttempt(func(attempt int) {
  showNotification("Reconectando...", fmt.Sprintf("Intento %d", attempt))
})

client.OnReconnect(func(attempt int) {
  showNotification("Conectado", "Conexión restablecida")
})

client.OnReconnectFailed(func() {
  showError("Sin Conexión", "No se pudo conectar al servidor")
})

Ver También