WebSocket in C#: vom Prototyp zur Produktion – fortgeschrittene Techniken für Real-Time-Anwendungen
\n\nDie Standardimplementierung von WebSocket in C# mit Bibliotheken wie System.Net.WebSockets oder SignalR löst grundlegende Aufgaben von Chats und Benachrichtigungen. Wenn die Anforderungen jedoch über "Hello, World" hinausgehen, steht der Entwickler vor der Notwendigkeit von Skalierung, Sicherheitsmaßnahmen, benutzerdefiniertem Routing und Leistungsoptimierung. In diesem Artikel behandeln wir fortgeschrittene Techniken für die Arbeit mit WebSocket in .NET, mit denen Sie zuverlässige und hochbelastbare Real-Time-Systeme erstellen können.
1. Architektur eines skalierbaren WebSocket-Servers: Muster und Clustering
\n\nEine der größten Einschränkungen von Standard-WebSocket ist die Bindung einer Verbindung an eine bestimmte Prozessinstanz. Bei der (horizontalen) Skalierung erreicht eine an Server A gesendete Nachricht nicht den Client, der mit Server B verbunden ist. Zur Lösung dieses Problems werden zwei Ansätze verwendet:
\n\n- \n
- Zentralisiertes Backplane: Verwendung von Redis Pub/Sub, RabbitMQ oder Azure SignalR Service zum Austausch von Nachrichten zwischen Servern. \n
- Sticky Sessions (klebrige Sitzungen): Der Load Balancer leitet Client-Anfragen immer an denselben Server weiter. Eine weniger zuverlässige Methode, da bei einem Knotenausfall alle seine Clients die Verbindung verlieren. \n
Betrachten wir ein Beispiel für die Implementierung eines benutzerdefinierten Backplanes mit Redis unter Verwendung von StackExchange.Redis und System.Net.WebSockets.
using StackExchange.Redis;\nusing System.Net.WebSockets;\nusing System.Text;\n\npublic class RedisBackplaneManager\n{\n private readonly ConnectionMultiplexer _redis;\n private readonly ISubscriber _subscriber;\n private readonly ConcurrentDictionary<string, WebSocket> _connections;\n\n public RedisBackplaneManager(string redisConnectionString)\n {\n _redis = ConnectionMultiplexer.Connect(redisConnectionString);\n _subscriber = _redis.GetSubscriber();\n _connections = new ConcurrentDictionary<string, WebSocket>();\n \n // Abonnieren des Kanals "websocket:messages"\n _subscriber.Subscribe("websocket:messages", (channel, message) => {\n var data = Encoding.UTF8.GetString(message);\n // Parsen der Nachricht (Format angenommen: "connectionId:payload")\n var parts = data.Split(':', 2);\n if (parts.Length == 2 && _connections.TryGetValue(parts[0], out var ws))\n {\n ws.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(parts[1])), \n WebSocketMessageType.Text, true, CancellationToken.None);\n }\n });\n }\n\n public async Task SendToClientAsync(string connectionId, string message)\n {\n // Veröffentlichen in Redis, damit alle Server die Nachricht erhalten\n var payload = $"{connectionId}:{message}";\n await _subscriber.PublishAsync("websocket:messages", payload);\n }\n\n public void AddConnection(string id, WebSocket ws) => _connections.TryAdd(id, ws);\n public void RemoveConnection(string id) => _connections.TryRemove(id, out _);\n}\n\n2. Benutzerdefiniertes Protokoll und Routing: WebSocket als RPC-Transport
\n\nOft ist es notwendig, nicht nur Text zu senden, sondern entfernte Prozeduren (RPC) über WebSocket aufzurufen. Anstatt JSON manuell zu parsen und Befehle zu überprüfen, kann ein minimalistischer Nachrichten-Router erstellt werden. Dieser Ansatz wird intern in SignalR verwendet, aber Sie können ihn für die vollständige Kontrolle implementieren.
\n\npublic class WebSocketRouter\n{\n private readonly Dictionary<string, Func<string, Task>> _routes = new();\n\n public void On(string action, Func<string, Task> handler)\n {\n _routes[action] = handler;\n }\n\n public async Task RouteAsync(string rawMessage)\n {\n // Erwartetes Format