WebSocket in C#: From Prototype to Production — Advanced Techniques for Real-time Applications
\n\nThe standard implementation of WebSocket in C# using libraries like System.Net.WebSockets or SignalR solves basic tasks like chats and notifications. However, when requirements go beyond "Hello, World", developers face the need for scaling, security, custom routing, and performance optimization. In this article, we will explore advanced techniques for working with WebSocket in .NET that will allow you to build reliable and high-load real-time systems.
1. Architecture of a Scalable WebSocket Server: Patterns and Clustering
\n\nOne of the main limitations of standard WebSocket is the binding of a connection to a specific process instance. When scaling horizontally, a message sent to server A will not reach a client connected to server B. Two approaches are used to solve this problem:
\n\n- \n
- Centralized Backplane: Using Redis Pub/Sub, RabbitMQ, or Azure SignalR Service to exchange messages between servers. \n
- Sticky Sessions: The load balancer always directs client requests to the same server. A less reliable method, as when a node fails, all its clients lose the connection. \n
Let's look at an example of implementing a custom backplane on Redis using StackExchange.Redis and 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 // Subscribe to the "websocket:messages" channel\n _subscriber.Subscribe("websocket:messages", (channel, message) => {\n var data = Encoding.UTF8.GetString(message);\n // Parse the message (assumes format: "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 // Publish to Redis so all servers receive the message\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. Custom Protocol and Routing: WebSocket as an RPC Transport
\n\nOften it is necessary not just to send text, but to call remote procedures (RPC) via WebSocket. Instead of manually parsing JSON and checking commands, you can build a minimalistic message router. This approach is used under the hood in SignalR, but you can implement it for full control.
\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 // Expects format