Zum Inhalt

Automatische Wiederverbindung

Die automatische Wiederverbindung ist ein kritisches Feature für resiliente Echtzeit-Anwendungen. Dieser Leitfaden behandelt Konfiguration und erweiterte Muster.

Inhaltsverzeichnis

Wie es funktioniert

Wiederverbindungsablauf

  1. Trennung: Die Verbindung zum Server geht verloren
  2. Warten: Es wird ReconnectDelay vor dem ersten Versuch gewartet
  3. Versuch: Es wird versucht, die Verbindung wiederherzustellen
  4. Backoff: Bei Fehler wird die Verzögerung exponentiell erhöht (begrenzt durch ReconnectDelayMax)
  5. Wiederholen: Wiederholung bis zu ReconnectAttempts mal
  6. Fehler: Wenn alle Versuche fehlschlagen, wird OnReconnectFailed ausgelöst

Exponentielles Backoff

Die Verzögerung zwischen Versuchen wächst exponentiell:

Versuch 1: ReconnectDelay (z.B.: 1s)
Versuch 2: ReconnectDelay * 2 (z.B.: 2s)
Versuch 3: ReconnectDelay * 4 (z.B.: 4s)
Versuch 4: min(ReconnectDelay * 8, ReconnectDelayMax) (z.B.: 5s wenn max=5s)
Versuch 5: ReconnectDelayMax (z.B.: 5s)

Konfiguration

Basiskonfiguration

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 5,                   // Maximal 5 Versuche
  ReconnectDelay:    time.Second,         // Erster Versuch nach 1s
  ReconnectDelayMax: 5 * time.Second,     // Maximal 5s zwischen Versuchen
})

Aggressive Wiederverbindung

Für Anwendungen, die sich schnell wieder verbinden müssen:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 10,                  // Viele Versuche
  ReconnectDelay:    500 * time.Millisecond, // Schnell versuchen
  ReconnectDelayMax: 3 * time.Second,     // Nicht lange warten
})

Konservative Wiederverbindung

Für Anwendungen, die den Server nicht überlasten wollen:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 3,                   // Wenige Versuche
  ReconnectDelay:    2 * time.Second,     // Länger warten
  ReconnectDelayMax: 30 * time.Second,    // Lange Verzögerungen erlauben
})

Ohne automatische Wiederverbindung

Für vollständige manuelle Kontrolle:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 0, // Automatische Wiederverbindung deaktivieren
})

client.OnDisconnect(func(reason string) {
  // Benutzerdefinierte Logik implementieren
  go manualReconnect()
})

Unendliche Wiederverbindung

Mit Vorsicht verwenden - nur für kritische Anwendungen:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: -1, // Unendliche Versuche
  ReconnectDelayMax: 60 * time.Second, // Maximale Verzögerung begrenzen
})

Wiederverbindungs-Events

OnReconnectAttempt

Wird vor jedem Wiederverbindungsversuch ausgeführt:

var attemptTime time.Time

client.OnReconnectAttempt(func(attempt int) {
  attemptTime = time.Now()
  fmt.Printf("🔄 Wiederverbindungsversuch #%d um %s\n",
    attempt, attemptTime.Format("15:04:05"))

  // Zusätzliche Logik
  if attempt > 3 {
    fmt.Println("⚠️  Mehrere Versuche fehlgeschlagen, prüfen Sie Ihre Verbindung")
  }
})

OnReconnect

Wird bei erfolgreicher Wiederverbindung ausgeführt:

client.OnReconnect(func(attempt int) {
  duration := time.Since(attemptTime)
  fmt.Printf("✅ Wiederverbunden nach %d Versuchen (%v)\n",
    attempt, duration)

  // Erneut Events abonnieren
  client.Emit("re-subscribe", previousSubscriptions)

  // Zustand synchronisieren
  syncLocalState()
})

OnReconnectError

Wird ausgeführt, wenn ein Versuch fehlschlägt:

client.OnReconnectError(func(err error) {
  fmt.Printf("❌ Fehler bei Wiederverbindung: %v\n", err)

  // Diagnose
  if strings.Contains(err.Error(), "connection refused") {
    fmt.Println("Der Server ist möglicherweise ausgefallen")
  } else if strings.Contains(err.Error(), "timeout") {
    fmt.Println("Netzwerkproblem oder langsamer Server")
  }
})

OnReconnectFailed

Wird ausgeführt, wenn alle Versuche fehlschlagen:

client.OnReconnectFailed(func() {
  fmt.Println("💥 Wiederverbindung nach allen Versuchen fehlgeschlagen")

  // Benutzer benachrichtigen
  showUserNotification("Verbindung verloren", "Konnte nicht zum Server verbinden")

  // Fallback
  switchToOfflineMode()

  // Oder manuelle Wiederverbindung anfordern
  promptManualReconnect()
})

Implementierungsmuster

Muster: Wiederverbindungszustand

Wiederverbindungszustand pflegen und Benutzer informieren:

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("✅ Wiederverbunden in %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("💥 Nach %v fehlgeschlagen\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 "Verbunden"
  }

  return fmt.Sprintf("Wiederverbindung... (Versuch %d/%d)",
    rs.Attempt, rs.MaxAttempts)
}

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

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

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

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

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

Muster: Zustandssynchronisation

Anwendungszustand nach Wiederverbindung synchronisieren:

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

func (app *AppState) Save() {
  // Zustand in localStorage, auf Disk, etc. speichern
}

func (app *AppState) Restore() {
  // Gespeicherten Zustand wiederherstellen
}

func setupReconnectionSync(client *socketio.Socket, state *AppState) {
  client.OnDisconnect(func(reason string) {
    // Zustand vor Verbindungsverlust speichern
    state.Save()
  })

  client.OnReconnect(func(attempt int) {
    // Zustand wiederherstellen
    state.Restore()

    // Erneut abonnieren
    for _, subscription := range state.subscriptions {
      client.Emit("subscribe", subscription)
    }

    // Räumen erneut beitreten
    for _, room := range state.rooms {
      client.Emit("join-room", room)
    }

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

Muster: Offline-Warteschlange

Operationen während Offline-Zeit in Warteschlange einreihen:

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("📦 Event in Warteschlange: %s (gesamt: %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("📤 Sende %d Events aus Warteschlange...\n", len(oq.queue))

  for _, qe := range oq.queue {
    // Prüfen ob Event nicht zu alt ist (z.B.: max 5 Minuten)
    if time.Since(qe.Time) < 5*time.Minute {
      oq.client.Emit(qe.Event, qe.Data...)
    } else {
      fmt.Printf("⏱️  Event zu alt, verworfen: %s\n", qe.Event)
    }
  }

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

// Verwendung
queue := NewOfflineQueue(client)

// Warteschlange statt Client direkt verwenden
queue.Emit("message", "Hallo") // Wird sofort gesendet oder in Warteschlange eingereiht

Muster: Wiederverbindung mit benutzerdefiniertem Backoff

Benutzerdefinierte Backoff-Strategie implementieren:

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, // Exponentielles Backoff mit Faktor 1.5
    jitter:      true, // Jitter hinzufügen um Thundering Herd zu vermeiden
  }

  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++

    // Verzögerung berechnen
    delay := cb.calculateDelay()
    fmt.Printf("⏱️  Warte %v vor Versuch %d/%d\n",
      delay, cb.attempts, cb.maxAttempts)

    time.Sleep(delay)

    // Wiederverbindung versuchen
    fmt.Printf("🔄 Versuche Wiederverbindung... (#%d)\n", cb.attempts)

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

  fmt.Println("💥 Wiederverbindung nach allen Versuchen fehlgeschlagen")
}

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

  // Auf Maximum begrenzen
  if delay > float64(cb.maxDelay) {
    delay = float64(cb.maxDelay)
  }

  // Jitter hinzufügen (Zufallsverteilung ±25%)
  if cb.jitter {
    jitterAmount := delay * 0.25
    delay += (rand.Float64()*2 - 1) * jitterAmount
  }

  return time.Duration(delay)
}

Erweiterte Strategien

Strategie: Adaptiv nach Netzwerkqualität

Strategie je nach Netzwerkqualität anpassen:

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"
  }

  // Konfiguration je nach Qualität anpassen
  switch ar.networkQuality {
  case "good":
    // Gutes Netzwerk: schnell versuchen
    ar.client.ReconnectDelay = time.Second
    ar.client.ReconnectDelayMax = 5 * time.Second
    ar.client.ReconnectAttempts = 5

  case "poor":
    // Schlechtes Netzwerk: konservativer sein
    ar.client.ReconnectDelay = 3 * time.Second
    ar.client.ReconnectDelayMax = 30 * time.Second
    ar.client.ReconnectAttempts = 10

  case "offline":
    // Offline: lange zwischen Versuchen warten
    ar.client.ReconnectDelay = 10 * time.Second
    ar.client.ReconnectDelayMax = 60 * time.Second
    ar.client.ReconnectAttempts = -1 // Unendlich
  }
}

Strategie: Health Checks

Verbindungsgesundheit proaktiv prüfen:

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)

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

  // Auf Antwort mit Timeout warten
  select {
  case healthy := <-done:
    if healthy {
      hc.unhealthy = false
      fmt.Println("💚 Verbindung gesund")
    } else {
      hc.handleUnhealthy()
    }
  case <-time.After(hc.timeout):
    hc.handleUnhealthy()
  }
}

func (hc *ConnectionHealthChecker) handleUnhealthy() {
  if !hc.unhealthy {
    hc.unhealthy = true
    fmt.Println("⚠️  Verbindung ungesund, erzwinge Wiederverbindung...")

    // Wiederverbindung erzwingen
    hc.client.Close()
  }
}

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

Strategie: Fallback zu HTTP Long-Polling

Bei WebSocket-Fehler zu zuverlässigerem Transport wechseln:

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++

    // Nach 3 Fehlern zu Polling wechseln
    if ft.failureCount >= 3 && !ft.usingPolling {
      fmt.Println("🔄 Wechsle zu HTTP Long-Polling...")
      ft.switchToPolling()
    }
  })

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

    // Nach erfolgreicher Wiederverbindung zurück zu WebSocket versuchen
    if ft.usingPolling {
      go ft.tryWebSocketAgain()
    }
  })
}

func (ft *FallbackTransport) switchToPolling() {
  ft.client.Close()
  // Neuen Client mit Polling erstellen (erfordert Änderung in der Bibliothek)
  // ft.client = socketio.NewWithTransport(ft.pollingURL, "polling")
  ft.usingPolling = true
}

func (ft *FallbackTransport) tryWebSocketAgain() {
  time.Sleep(5 * time.Minute) // Vor erneutem Versuch warten

  if ft.usingPolling {
    fmt.Println("🔄 Versuche zurück zu WebSocket...")
    // WebSocket-Client erstellen versuchen
    // Bei Fehler Polling beibehalten
  }
}

Best Practices

1. Angemessene Timeouts konfigurieren

// ✅ Gut: alle zugehörigen Timeouts konfigurieren
client := socketio.New("ws://localhost:3000", socketio.Options{
  Timeout:           30 * time.Second, // Initiale Verbindung
  AckTimeout:        5 * time.Second,  // Acknowledgments
  ReconnectDelay:    time.Second,
  ReconnectDelayMax: 5 * time.Second,
  ReconnectAttempts: 5,
})

2. Alle Events behandeln

// ✅ Gut: alle Wiederverbindungs-Events behandeln
client.OnReconnectAttempt(func(attempt int) {
  log.Printf("Wiederverbindung... (#%d)", attempt)
})

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

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

client.OnReconnectFailed(func() {
  log.Println("Wiederverbindung fehlgeschlagen")
  notifyUser()
})

3. Nach Wiederverbindung erneut abonnieren

// ✅ Gut: immer erneut abonnieren
var subscriptions []string

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

client.OnReconnect(func(attempt int) {
  // Automatisch erneut abonnieren
  for _, sub := range subscriptions {
    client.Emit("subscribe", sub)
  }
})

4. Nicht auf ständig verfügbare Verbindung vertrauen

// ✅ Gut: Verbindung vor kritischen Operationen prüfen
if !client.IsConnected() {
  return fmt.Errorf("keine Verbindung")
}

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

// ❌ Schlecht: annehmen, dass immer verbunden
client.Emit("important-event", data) // Kann stillschweigend fehlschlagen

5. Benutzer benachrichtigen

// ✅ Gut: Benutzer über Status informieren
client.OnReconnectAttempt(func(attempt int) {
  showNotification("Wiederverbindung...", fmt.Sprintf("Versuch %d", attempt))
})

client.OnReconnect(func(attempt int) {
  showNotification("Verbunden", "Verbindung wiederhergestellt")
})

client.OnReconnectFailed(func() {
  showError("Keine Verbindung", "Konnte nicht zum Server verbinden")
})

Siehe auch