WebSocket en C#: del prototipo a producción — técnicas avanzadas para aplicaciones en tiempo real
\n\nLa implementación estándar de WebSocket en C# utilizando bibliotecas como System.Net.WebSockets o SignalR resuelve tareas básicas de chats y notificaciones. Sin embargo, cuando los requisitos van más allá de "Hola, Mundo", el desarrollador se enfrenta a la necesidad de escalabilidad, seguridad, enrutamiento personalizado y optimización del rendimiento. En este artículo, analizaremos técnicas avanzadas para trabajar con WebSocket en .NET que le permitirán construir sistemas en tiempo real robustos y de alta carga.
1. Arquitectura de un servidor WebSocket escalable: patrones y clustering
\n\nUna de las principales limitaciones del WebSocket estándar es la vinculación de la conexión a una instancia específica del proceso. Al escalar (horizontalmente), un mensaje enviado al servidor A no llegará al cliente conectado al servidor B. Para resolver este problema se utilizan dos enfoques:
\n\n- \n
- Backplane centralizado: Uso de Redis Pub/Sub, RabbitMQ o Azure SignalR Service para intercambiar mensajes entre servidores. \n
- Sesiones persistentes (Sticky Sessions): El balanceador dirige las solicitudes del cliente siempre al mismo servidor. Método menos fiable, ya que si un nodo falla, todos sus clientes pierden la conexión. \n
Veamos un ejemplo de implementación de un backplane personalizado en Redis utilizando StackExchange.Redis y 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 // Nos suscribimos al canal "websocket:messages"\n _subscriber.Subscribe("websocket:messages", (channel, message) => {\n var data = Encoding.UTF8.GetString(message);\n // Parseamos el mensaje (asumimos formato: "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 // Publicamos en Redis para que todos los servidores reciban el mensaje\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. Protocolo personalizado y enrutamiento: WebSocket como transporte RPC
\n\nA menudo no solo es necesario enviar texto, sino invocar procedimientos remotos (RPC) a través de WebSocket. En lugar de analizar manualmente JSON y verificar comandos, se puede construir un enrutador de mensajes minimalista. Este enfoque se utiliza internamente en SignalR, pero usted puede implementarlo para tener control total.
\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 // Esperamos el forma