WebSocket in Go: Fortgeschrittene Techniken für Echtzeitanwendungen
\n\nDie Entwicklung von Echtzeitanwendungen in Go ist eine der größten Stärken der Sprache. Die eingebaute Unterstützung für Nebenläufigkeit durch Goroutinen und Kanäle macht Go zur idealen Wahl für WebSocket-Server. Wenn es jedoch um Produktionssysteme mit Tausenden von gleichzeitigen Verbindungen geht, funktioniert das einfache Beispiel "Hello, WebSocket" nicht mehr. In diesem Artikel behandeln wir fortgeschrittene Techniken: von der Optimierung des Verbindungspools bis zur Skalierung über Message Broker.
\n\n1. Architektur des Verbindungspools: Verwaltung Tausender Clients
\n\nIm Basisbeispiel erstellen wir eine separate Goroutine für jede Verbindung. Das ist für 100-200 Clients in Ordnung, aber bei 10.000+ Verbindungen werden der Speicherverbrauch und der Overhead durch Kontextwechsel kritisch. Ein fortgeschrittener Ansatz ist die Verwendung eines Verbindungspools mit einer festen Anzahl von Workern und Kanälen.
\n\n// Pool — Verbindungspool mit Goroutinen-Begrenzung\ntype Pool struct {\n connections map[*websocket.Conn]bool\n broadcast chan []byte\n register chan *websocket.Conn\n unregister chan *websocket.Conn\n}\n\nfunc NewPool(maxGoroutines int) *Pool {\n return &Pool{\n connections: make(map[*websocket.Conn]bool),\n broadcast: make(chan []byte, 256),\n register: make(chan *websocket.Conn),\n unregister: make(chan *websocket.Conn),\n }\n}\n\nfunc (pool *Pool) Start() {\n for {\n select {\n case conn := <-pool.register:\n pool.connections[conn] = true\n case conn := <-pool.unregister:\n if _, ok := pool.connections[conn]; ok {\n delete(pool.connections, conn)\n conn.Close()\n }\n case message := <-pool.broadcast:\n for conn := range pool.connections {\n go func(c *websocket.Conn) {\n if err := c.WriteMessage(websocket.TextMessage, message); err != nil {\n pool.unregister <- c\n }\n }(conn)\n }\n }\n }\n}\n\nWarum funktioniert das? Kanäle fungieren als Puffer und verhindern Blockaden. Ein einziger Select verwaltet alle Ereignisse, und Goroutinen zum Schreiben werden nur bei Bedarf erstellt. Dies reduziert die Spitzenlast des Garbage Collectors (GC).
\n\n2. Graceful Shutdown und Verwaltung des Verbindungszustands
\n\nBeim Neustart des Servers ist es wichtig, alle WebSocket-Verbindungen ordnungsgemäß zu schließen und den Clients eine Nachricht über die Beendigung zu senden. Das Ignorieren dieses Schrittes führt zu "hängenden" Verbindungen und Speicherlecks.
\n\n// Server — WebSocket-Server mit Graceful Shutdown\ntype Server struct {\n pool *Pool\n httpSrv *http.Server\n}\n\nfunc (s *Server) Shutdown(ctx context.Context) error {\n // 1. HTTP-Server schließen (keine neuen Verbindungen mehr annehmen)\n if err := s.httpSrv.Shutdown(ctx); err != nil {\n return err\n }\n\n // 2. Allen Clients eine Nachricht über die Beendigung senden\n shutdownMsg := []byte(`{"type":"shutdown","message":"server is going down"}`)\n s.pool.broadcast <- shutdownMsg\n\n // 3. Alle Verbindungen im Pool schließen\n for conn := range s.pool.connections {\n conn.WriteMessage(websocket.CloseMessage, \n websocket.FormatCloseMessage(websocket.CloseNormalClosure, "server shutdown"))\n conn.Close()\n }\n\n return nil\n}\n\n// Verwendung in main\nfunc main() {\n pool := NewPool(1000)\n go pool.Start()\n\n srv := &Server{pool: pool}\n srv.httpSrv = &http.Server{Addr: ":8080"}\n\n // Signalverarbeitung\n go func() {\n sigint := make(chan os.Signal, 1)\n signal.Notify(sigint, os.Interrupt, syscall.SIGTERM)\n <-sigint\n ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n defer cancel()\n srv.Shutdown(ctx)\n }()\n\n log.Fatal(srv.httpSrv.ListenAndServ