Introduction: Why WebSocket in C# is the Choice of Professionals
In modern web development, real-time interaction has become the standard. Trading platforms, monitoring systems, online games, and chats — all require instant data transmission. WebSocket technology in C# (.NET) provides a powerful and flexible tool for creating such applications. Unlike HTTP polling, WebSocket establishes a persistent bidirectional connection, which radically reduces latency and server load.
In this article, we will explore advanced aspects of working with WebSocket in C#. You will learn how to build a fault-tolerant server, manage thousands of concurrent connections, handle errors, and integrate WebSocket with popular frameworks. The material is intended for developers already familiar with the basics of .NET and asynchronous programming.
Architecture of an Advanced WebSocket Server in C#
A basic example of a WebSocket server in C# is often limited to a single handler. Production solutions require a well-thought-out architecture. Let's consider the key components.
Connection Management and Client Identification
Instead of a simple list of connections, use a ConcurrentDictionary for thread-safe storage. Each client is assigned a unique identifier (GUID) and metadata is stored (e.g., username, group).
public class WebSocketConnectionManager{ private readonly ConcurrentDictionary<string, WebSocket> _connections = new(); private readonly ConcurrentDictionary<string, UserMetadata> _users = new();
public string AddConnection(WebSocket socket, UserMetadata metadata) { var id = Guid.NewGuid().ToString(); _connections.TryAdd(id, socket); _users.TryAdd(id, metadata); return id; }
public async Task RemoveConnection(string id) { if (_connections.TryRemove(id, out var socket) && socket.State == WebSocketState.Open) { await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed", CancellationToken.None); } _users.TryRemove(id, out _); }
public WebSocket? GetSocketById(string id) => _connections.GetValueOrDefault(id);}This approach allows you to easily send messages to specific users, groups, or everyone at once.
Handling Fragmented Messages
In real applications, data may arrive in fragments. The standard ReceiveAsync method returns a single fragment. To assemble a complete message, use a loop:
public async Task<byte[]> ReceiveFullMessageAsync(WebSocket socket, CancellationToken ct){ using var ms = new MemoryStream(); WebSocketReceiveResult result; var buffer = new byte[1024 * 4]; // 4KB buffer
do { result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), ct); ms.Write(buffer, 0, result.Count); } while (!result.EndOfMessage);
return ms.ToArray();}This is critical for transmitting large JSON objects or binary data (images, files).
Scaling: SignalR vs Raw WebSocket
For complex applications, the question arises of choosing between low-level WebSocket and SignalR — a library from Microsoft that abstracts WebSocket. Let's consider the scenarios.
When to Use Raw WebSocket
- Maximum performance and minimal overhead.
- Custom transmission protocol (binary data, custom frames).
- Integration with non-.NET clients (e.g., Go or Rust).
- Full control over connection management.
When to Choose SignalR
- Automatic scaling via Redis Backplane or Azure SignalR Service.
- Support for fallback transports (Long Polling, Server-Sent