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¶
- Desconexión: Se pierde la conexión con el servidor
- Espera: Se espera
ReconnectDelayantes del primer intento - Intento: Se intenta reconectar
- Backoff: Si falla, se incrementa el delay exponencialmente (limitado por
ReconnectDelayMax) - Repetir: Se repite hasta
ReconnectAttemptsveces - 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")
})