WebSocket em C#: do protótipo à produção — técnicas avançadas de aplicações em tempo real
\n\nA implementação padrão de WebSocket em C# usando bibliotecas como System.Net.WebSockets ou SignalR resolve tarefas básicas de chats e notificações. No entanto, quando os requisitos vão além do "Hello, World", o desenvolvedor se depara com a necessidade de escalabilidade, segurança, roteamento personalizado e otimização de desempenho. Neste artigo, abordaremos técnicas avançadas de trabalho com WebSocket no .NET que permitirão construir sistemas de tempo real robustos e de alta carga.
1. Arquitetura de servidor WebSocket escalável: padrões e clusterização
\n\nUma das principais limitações do WebSocket padrão é a vinculação da conexão a uma instância específica do processo. Ao escalar horizontalmente, uma mensagem enviada ao servidor A não alcançará o cliente conectado ao servidor B. Para resolver esse problema, são usadas duas abordagens:
\n\n- \n
- Backplane centralizado: Uso de Redis Pub/Sub, RabbitMQ ou Azure SignalR Service para troca de mensagens entre servidores. \n
- Sessões persistentes (Sticky Sessions): O balanceador direciona as requisições do cliente sempre para o mesmo servidor. Método menos confiável, pois, se o nó cair, todos os seus clientes perdem a conexão. \n
Vejamos um exemplo de implementação de um backplane personalizado no Redis usando StackExchange.Redis e 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 // Inscrevemos no canal "websocket:messages"\n _subscriber.Subscribe("websocket:messages", (channel, message) => {\n var data = Encoding.UTF8.GetString(message);\n // Analisamos a mensagem (assumimos 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 no Redis para que a mensagem seja recebida por todos os servidores\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 e roteamento: WebSocket como transporte RPC
\n\nFrequentemente, não é necessário apenas enviar texto, mas sim chamar procedimentos remotos (RPC) via WebSocket. Em vez de analisar manualmente JSON e verificar comandos, podemos construir um roteador de mensagens minimalista. Essa abordagem é usada internamente pelo SignalR, mas você pode implementá-la para ter controle 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 o formato