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
- Konfiguration
- Wiederverbindungs-Events
- Implementierungsmuster
- Erweiterte Strategien
Wie es funktioniert¶
Wiederverbindungsablauf¶
- Trennung: Die Verbindung zum Server geht verloren
- Warten: Es wird
ReconnectDelayvor dem ersten Versuch gewartet - Versuch: Es wird versucht, die Verbindung wiederherzustellen
- Backoff: Bei Fehler wird die Verzögerung exponentiell erhöht (begrenzt durch
ReconnectDelayMax) - Wiederholen: Wiederholung bis zu
ReconnectAttemptsmal - Fehler: Wenn alle Versuche fehlschlagen, wird
OnReconnectFailedausgelö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")
})