C# and .NET WebSocket Implementation
C# and .NET provide one of the most comprehensive and mature ecosystems for WebSocket development, offering everything from low-level ClientWebSocket
APIs to high-level frameworks like SignalR. This guide covers the full spectrum of WebSocket implementation in the .NET ecosystem, from simple client-server connections to enterprise-scale real-time applications.
The .NET platform’s approach to WebSocket development emphasizes developer productivity, type safety, and performance. The combination of C#‘s powerful language features, the extensive .NET Base Class Library, and the rich ecosystem of third-party packages creates an environment where developers can focus on business logic rather than low-level protocol implementation details.
Why Choose C# and .NET for WebSocket Development?
The .NET ecosystem offers compelling advantages for WebSocket development that make it a top choice for enterprise and scalable applications:
Comprehensive Framework Support: From the built-in ClientWebSocket
class to ASP.NET Core’s sophisticated WebSocket middleware, .NET provides extensive native support for WebSocket protocols without requiring external dependencies.
Enterprise-Grade Performance: .NET’s just-in-time compilation, garbage collection optimization, and runtime performance make it capable of handling thousands of concurrent WebSocket connections efficiently.
Rich Ecosystem: SignalR for high-level real-time communication, ASP.NET Core for web applications, and Unity integration for game development provide solutions for every use case.
Cross-Platform Compatibility: With .NET Core/.NET 5+, your WebSocket applications run on Windows, Linux, macOS, and in containers with consistent performance characteristics.
Excellent Async Programming Support: C#‘s async/await pattern provides intuitive, efficient handling of WebSocket operations, making it easier to write scalable applications that can handle thousands of concurrent connections without complex threading logic.
Strong Type System: C#‘s static typing helps catch WebSocket protocol errors and serialization issues at compile time, reducing runtime errors and improving overall application reliability. The type system also provides excellent IntelliSense support, making development faster and less error-prone.
Mature Tooling Ecosystem: The .NET ecosystem includes excellent debugging tools, performance profilers, and monitoring solutions that are essential for diagnosing and optimizing WebSocket applications in production environments. Visual Studio and Visual Studio Code provide sophisticated debugging capabilities for real-time applications.
Strong Typing and Tooling: C#‘s static typing system prevents many runtime errors, while Visual Studio and other IDEs provide excellent debugging, profiling, and development experiences.
Security and Reliability: Built-in security features, extensive logging capabilities, and proven stability make .NET ideal for mission-critical real-time applications.
Scalability Options: From simple in-memory solutions to Redis backplanes and Azure Service Bus, .NET offers multiple paths to scale WebSocket applications horizontally.
Setting Up Your .NET WebSocket Project
Let’s establish a comprehensive project structure that demonstrates both client and server implementations across different .NET scenarios.
Project Structure and Setup
Create a solution with multiple projects to demonstrate different WebSocket implementation patterns:
# Create solution and projectsdotnet new sln -n WebSocketGuidecd WebSocketGuide
# Core library for shared models and interfacesdotnet new classlib -n WebSocketGuide.Core
# ASP.NET Core WebSocket serverdotnet new web -n WebSocketGuide.Server
# Console client applicationdotnet new console -n WebSocketGuide.Client
# SignalR demodotnet new web -n WebSocketGuide.SignalR
# Unity client (create manually)mkdir WebSocketGuide.Unity
# Test projectsdotnet new xunit -n WebSocketGuide.Tests
# Add projects to solutiondotnet sln add **/*.csproj
Core Library Setup
In WebSocketGuide.Core/WebSocketGuide.Core.csproj
:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup>
<ItemGroup> <PackageReference Include="System.Text.Json" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" /> </ItemGroup></Project>
Server Project Setup
In WebSocketGuide.Server/WebSocketGuide.Server.csproj
:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup>
<ItemGroup> <ProjectReference Include="../WebSocketGuide.Core/WebSocketGuide.Core.csproj" /> </ItemGroup>
<ItemGroup> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" /> <PackageReference Include="StackExchange.Redis" Version="2.7.0" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="8.0.1" /> </ItemGroup></Project>
Configuration System
Create a robust configuration system in WebSocketGuide.Core/Configuration/WebSocketOptions.cs
:
using Microsoft.Extensions.Options;using System.ComponentModel.DataAnnotations;
namespace WebSocketGuide.Core.Configuration;
public class WebSocketOptions : IValidateOptions<WebSocketOptions>{ public const string SectionName = "WebSocket";
[Range(1, 100000)] public int MaxConnections { get; set; } = 10000;
[Range(1024, 1024 * 1024)] // 1KB to 1MB public int ReceiveBufferSize { get; set; } = 4 * 1024;
[Range(1, 300)] // 1 to 300 seconds public int KeepAliveIntervalSeconds { get; set; } = 30;
[Range(1, 3600)] // 1 second to 1 hour public int CloseTimeoutSeconds { get; set; } = 30;
[Range(1024, 10 * 1024 * 1024)] // 1KB to 10MB public long MaxMessageSize { get; set; } = 1024 * 1024; // 1MB
public bool EnableCompression { get; set; } = true;
public string[] AllowedOrigins { get; set; } = { "*" };
public bool RequireAuthentication { get; set; } = false;
public string AuthenticationScheme { get; set; } = "Bearer";
public RateLimitOptions RateLimit { get; set; } = new();
public RedisOptions? Redis { get; set; }
public ValidateOptionsResult Validate(string? name, WebSocketOptions options) { var failures = new List<string>();
if (options.MaxConnections <= 0) failures.Add("MaxConnections must be greater than 0");
if (options.ReceiveBufferSize < 1024) failures.Add("ReceiveBufferSize must be at least 1024 bytes");
if (options.MaxMessageSize <= 0) failures.Add("MaxMessageSize must be greater than 0");
if (failures.Count > 0) return ValidateOptionsResult.Fail(failures);
return ValidateOptionsResult.Success; }}
public class RateLimitOptions{ public bool Enabled { get; set; } = true;
[Range(1, 10000)] public int RequestsPerMinute { get; set; } = 60;
[Range(1, 1000)] public int BurstSize { get; set; } = 10;
public TimeSpan WindowSize { get; set; } = TimeSpan.FromMinutes(1);}
public class RedisOptions{ [Required] public string ConnectionString { get; set; } = string.Empty;
public string KeyPrefix { get; set; } = "websocket:";
public int Database { get; set; } = 0;
public TimeSpan CommandTimeout { get; set; } = TimeSpan.FromSeconds(5);
public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(5);}
public class MonitoringOptions{ public bool EnableMetrics { get; set; } = true; public bool EnableHealthChecks { get; set; } = true; public bool EnableTracing { get; set; } = false; public string MetricsEndpoint { get; set; } = "/metrics"; public string HealthEndpoint { get; set; } = "/health";}
This guide covers WebSocket implementation in C# and .NET, including ASP.NET Core, SignalR, and Unity patterns.
.NET Client Implementation
Using ClientWebSocket
The built-in ClientWebSocket
class for .NET applications:
using System;using System.Net.WebSockets;using System.Text;using System.Threading;using System.Threading.Tasks;using System.Text.Json;
public class WebSocketClient{ private ClientWebSocket _webSocket; private CancellationTokenSource _cancellationTokenSource; private readonly Uri _uri; private readonly SemaphoreSlim _sendSemaphore = new(1, 1);
public WebSocketClient(string uri) { _uri = new Uri(uri); }
public async Task ConnectAsync() { _webSocket = new ClientWebSocket(); _cancellationTokenSource = new CancellationTokenSource();
// Configure WebSocket options _webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(20); _webSocket.Options.SetRequestHeader("Authorization", $"Bearer {GetAuthToken()}"); _webSocket.Options.AddSubProtocol("chat");
try { await _webSocket.ConnectAsync(_uri, _cancellationTokenSource.Token); Console.WriteLine($"Connected to {_uri}");
// Start receiving messages _ = Task.Run(ReceiveLoop);
// Send initial message await SendMessageAsync(new { type = "subscribe", channel = "updates" }); } catch (Exception ex) { Console.WriteLine($"Connection failed: {ex.Message}"); throw; } }
private async Task ReceiveLoop() { var buffer = new ArraySegment<byte>(new byte[4096]);
while (_webSocket.State == WebSocketState.Open) { try { var result = await _webSocket.ReceiveAsync( buffer, _cancellationTokenSource.Token );
if (result.MessageType == WebSocketMessageType.Text) { var message = Encoding.UTF8.GetString( buffer.Array, 0, result.Count ); await HandleTextMessage(message); } else if (result.MessageType == WebSocketMessageType.Binary) { await HandleBinaryMessage( buffer.Array.Take(result.Count).ToArray() ); } else if (result.MessageType == WebSocketMessageType.Close) { await HandleClose(result); break; } } catch (WebSocketException ex) { Console.WriteLine($"WebSocket error: {ex.Message}"); await HandleError(ex); break; } } }
public async Task SendMessageAsync(object message) { if (_webSocket.State != WebSocketState.Open) { throw new InvalidOperationException("WebSocket is not connected"); }
var json = JsonSerializer.Serialize(message); var bytes = Encoding.UTF8.GetBytes(json);
await _sendSemaphore.WaitAsync(); try { await _webSocket.SendAsync( new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cancellationTokenSource.Token ); } finally { _sendSemaphore.Release(); } }
public async Task SendBinaryAsync(byte[] data) { if (_webSocket.State != WebSocketState.Open) { throw new InvalidOperationException("WebSocket is not connected"); }
await _sendSemaphore.WaitAsync(); try { await _webSocket.SendAsync( new ArraySegment<byte>(data), WebSocketMessageType.Binary, true, _cancellationTokenSource.Token ); } finally { _sendSemaphore.Release(); } }
public async Task DisconnectAsync() { if (_webSocket.State == WebSocketState.Open) { await _webSocket.CloseAsync( WebSocketCloseStatus.NormalClosure, "Client disconnecting", CancellationToken.None ); }
_cancellationTokenSource?.Cancel(); _webSocket?.Dispose(); }
private async Task HandleTextMessage(string message) { try { var json = JsonDocument.Parse(message); var type = json.RootElement.GetProperty("type").GetString();
switch (type) { case "notification": await HandleNotification(json.RootElement); break; case "data": await HandleData(json.RootElement); break; default: Console.WriteLine($"Unknown message type: {type}"); break; } } catch (JsonException) { Console.WriteLine($"Plain text message: {message}"); } }
private async Task HandleBinaryMessage(byte[] data) { Console.WriteLine($"Received binary data: {data.Length} bytes"); // Process binary data }
private async Task HandleClose(WebSocketReceiveResult result) { Console.WriteLine($"Connection closed: {result.CloseStatus} - {result.CloseStatusDescription}"); }
private async Task HandleError(Exception ex) { Console.WriteLine($"Error: {ex.Message}"); // Implement reconnection logic }
private async Task HandleNotification(JsonElement json) { var message = json.GetProperty("message").GetString(); Console.WriteLine($"Notification: {message}"); }
private async Task HandleData(JsonElement json) { var data = json.GetProperty("data"); Console.WriteLine($"Data received: {data}"); }
private string GetAuthToken() { // Retrieve authentication token return "your-auth-token"; }}
Reconnecting WebSocket Client
Implement automatic reconnection with exponential backoff:
using System;using System.Collections.Concurrent;using System.Net.WebSockets;using System.Threading;using System.Threading.Tasks;using System.Timers;using Timer = System.Timers.Timer;
public class ReconnectingWebSocketClient : IDisposable{ private ClientWebSocket _webSocket; private readonly Uri _uri; private CancellationTokenSource _cancellationTokenSource; private readonly ConcurrentQueue<byte[]> _messageQueue = new(); private Timer _reconnectTimer; private int _reconnectAttempts = 0; private bool _shouldReconnect = true; private bool _isDisposed = false;
// Configuration private readonly int _maxReconnectAttempts = 10; private readonly TimeSpan _minReconnectDelay = TimeSpan.FromSeconds(1); private readonly TimeSpan _maxReconnectDelay = TimeSpan.FromSeconds(30); private readonly double _reconnectDecay = 1.5;
public event EventHandler<string> MessageReceived; public event EventHandler<byte[]> BinaryMessageReceived; public event EventHandler Connected; public event EventHandler<string> Disconnected; public event EventHandler<Exception> ErrorOccurred;
public WebSocketState State => _webSocket?.State ?? WebSocketState.None; public bool IsConnected => State == WebSocketState.Open;
public ReconnectingWebSocketClient(string uri) { _uri = new Uri(uri); }
public async Task ConnectAsync() { try { _webSocket = new ClientWebSocket(); _cancellationTokenSource = new CancellationTokenSource();
ConfigureWebSocket();
await _webSocket.ConnectAsync(_uri, _cancellationTokenSource.Token);
_reconnectAttempts = 0; Connected?.Invoke(this, EventArgs.Empty);
// Process queued messages while (_messageQueue.TryDequeue(out var message)) { await SendInternalAsync(message); }
// Start receive loop _ = Task.Run(ReceiveLoopAsync); } catch (Exception ex) { ErrorOccurred?.Invoke(this, ex);
if (_shouldReconnect && !IsMaxReconnectsReached()) { ScheduleReconnect(); }
throw; } }
private void ConfigureWebSocket() { _webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(20); _webSocket.Options.SetRequestHeader("User-Agent", "ReconnectingWebSocketClient/1.0");
// Add any custom headers or sub-protocols ConfigureWebSocketOptions(_webSocket.Options); }
protected virtual void ConfigureWebSocketOptions(ClientWebSocketOptions options) { // Override in derived class to add custom configuration }
private async Task ReceiveLoopAsync() { var buffer = new ArraySegment<byte>(new byte[4096]); var messageBuilder = new List<byte>();
while (IsConnected && !_cancellationTokenSource.Token.IsCancellationRequested) { try { WebSocketReceiveResult result; do { result = await _webSocket.ReceiveAsync(buffer, _cancellationTokenSource.Token);
if (result.MessageType == WebSocketMessageType.Close) { await HandleCloseAsync(result); return; }
messageBuilder.AddRange(buffer.Array.Take(result.Count));
} while (!result.EndOfMessage);
if (result.MessageType == WebSocketMessageType.Text) { var message = Encoding.UTF8.GetString(messageBuilder.ToArray()); MessageReceived?.Invoke(this, message); } else if (result.MessageType == WebSocketMessageType.Binary) { BinaryMessageReceived?.Invoke(this, messageBuilder.ToArray()); }
messageBuilder.Clear(); } catch (WebSocketException ex) { ErrorOccurred?.Invoke(this, ex); await HandleDisconnectAsync(ex.Message); break; } catch (OperationCanceledException) { break; } } }
public async Task SendAsync(string message) { var bytes = Encoding.UTF8.GetBytes(message); await SendAsync(bytes, WebSocketMessageType.Text); }
public async Task SendBinaryAsync(byte[] data) { await SendAsync(data, WebSocketMessageType.Binary); }
private async Task SendAsync(byte[] data, WebSocketMessageType messageType) { if (IsConnected) { await SendInternalAsync(data, messageType); } else { // Queue message for sending after reconnection _messageQueue.Enqueue(data); } }
private async Task SendInternalAsync(byte[] data, WebSocketMessageType messageType = WebSocketMessageType.Text) { try { await _webSocket.SendAsync( new ArraySegment<byte>(data), messageType, true, _cancellationTokenSource.Token ); } catch (WebSocketException ex) { ErrorOccurred?.Invoke(this, ex); _messageQueue.Enqueue(data); // Re-queue on failure throw; } }
private async Task HandleCloseAsync(WebSocketReceiveResult result) { var reason = result.CloseStatusDescription ?? "No reason provided"; Disconnected?.Invoke(this, reason);
if (_shouldReconnect && result.CloseStatus != WebSocketCloseStatus.NormalClosure) { await HandleDisconnectAsync(reason); } }
private async Task HandleDisconnectAsync(string reason) { Disconnected?.Invoke(this, reason);
if (_shouldReconnect && !IsMaxReconnectsReached()) { ScheduleReconnect(); } }
private void ScheduleReconnect() { var delay = GetReconnectDelay(); _reconnectAttempts++;
Console.WriteLine($"Reconnecting in {delay.TotalSeconds:F1} seconds (attempt #{_reconnectAttempts})");
_reconnectTimer?.Dispose(); _reconnectTimer = new Timer(delay.TotalMilliseconds); _reconnectTimer.Elapsed += async (sender, e) => { _reconnectTimer.Dispose();
if (_shouldReconnect && !_isDisposed) { await ConnectAsync(); } }; _reconnectTimer.Start(); }
private TimeSpan GetReconnectDelay() { var delay = TimeSpan.FromMilliseconds( _minReconnectDelay.TotalMilliseconds * Math.Pow(_reconnectDecay, _reconnectAttempts) );
return delay > _maxReconnectDelay ? _maxReconnectDelay : delay; }
private bool IsMaxReconnectsReached() { return _reconnectAttempts >= _maxReconnectAttempts; }
public async Task DisconnectAsync() { _shouldReconnect = false; _reconnectTimer?.Dispose();
if (IsConnected) { await _webSocket.CloseAsync( WebSocketCloseStatus.NormalClosure, "Client disconnecting", CancellationToken.None ); }
_cancellationTokenSource?.Cancel(); }
public void Dispose() { if (_isDisposed) return;
_isDisposed = true; _shouldReconnect = false;
_reconnectTimer?.Dispose(); _cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Dispose(); _webSocket?.Dispose(); }}
ASP.NET Core WebSocket Server
Basic WebSocket Middleware
Configure WebSocket support in ASP.NET Core:
using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Http;using System.Net.WebSockets;using System.Text;using System.Threading;using System.Threading.Tasks;
public class Startup{ public void ConfigureServices(IServiceCollection services) { services.AddControllers();
// Add WebSocket manager service services.AddSingleton<WebSocketConnectionManager>(); services.AddTransient<WebSocketHandler>(); }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); }
app.UseRouting();
// Enable WebSocket support var webSocketOptions = new WebSocketOptions { KeepAliveInterval = TimeSpan.FromSeconds(120), ReceiveBufferSize = 4 * 1024 }; app.UseWebSockets(webSocketOptions);
// Map WebSocket endpoints app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.Map("/ws", async context => { if (context.WebSockets.IsWebSocketRequest) { using var webSocket = await context.WebSockets.AcceptWebSocketAsync(); var handler = context.RequestServices.GetRequiredService<WebSocketHandler>(); await handler.HandleAsync(context, webSocket); } else { context.Response.StatusCode = 400; } }); }); }}
public class WebSocketHandler{ private readonly WebSocketConnectionManager _connectionManager; private readonly ILogger<WebSocketHandler> _logger;
public WebSocketHandler( WebSocketConnectionManager connectionManager, ILogger<WebSocketHandler> logger) { _connectionManager = connectionManager; _logger = logger; }
public async Task HandleAsync(HttpContext context, WebSocket webSocket) { var connectionId = _connectionManager.AddConnection(webSocket); _logger.LogInformation($"WebSocket connection established: {connectionId}");
try { await ReceiveAsync(connectionId, webSocket); } catch (Exception ex) { _logger.LogError(ex, $"Error in WebSocket connection {connectionId}"); } finally { _connectionManager.RemoveConnection(connectionId); _logger.LogInformation($"WebSocket connection closed: {connectionId}"); } }
private async Task ReceiveAsync(string connectionId, WebSocket webSocket) { var buffer = new ArraySegment<byte>(new byte[4096]);
while (webSocket.State == WebSocketState.Open) { var result = await webSocket.ReceiveAsync(buffer, CancellationToken.None);
switch (result.MessageType) { case WebSocketMessageType.Text: var message = Encoding.UTF8.GetString(buffer.Array, 0, result.Count); await HandleTextMessage(connectionId, message); break;
case WebSocketMessageType.Binary: await HandleBinaryMessage(connectionId, buffer.Array.Take(result.Count).ToArray()); break;
case WebSocketMessageType.Close: await webSocket.CloseAsync( result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None ); break; } } }
private async Task HandleTextMessage(string connectionId, string message) { _logger.LogInformation($"Received from {connectionId}: {message}");
try { var json = JsonDocument.Parse(message); var type = json.RootElement.GetProperty("type").GetString();
switch (type) { case "broadcast": await BroadcastMessage(connectionId, message); break; case "private": await SendPrivateMessage(connectionId, json.RootElement); break; default: await Echo(connectionId, message); break; } } catch (JsonException) { await Echo(connectionId, message); } }
private async Task HandleBinaryMessage(string connectionId, byte[] data) { _logger.LogInformation($"Received binary from {connectionId}: {data.Length} bytes"); // Process binary data }
private async Task Echo(string connectionId, string message) { await _connectionManager.SendAsync(connectionId, $"Echo: {message}"); }
private async Task BroadcastMessage(string senderId, string message) { await _connectionManager.BroadcastAsync(message, senderId); }
private async Task SendPrivateMessage(string senderId, JsonElement json) { var targetId = json.GetProperty("target").GetString(); var content = json.GetProperty("content").GetString();
await _connectionManager.SendAsync(targetId, content); }}
public class WebSocketConnectionManager{ private readonly ConcurrentDictionary<string, WebSocket> _connections = new();
public string AddConnection(WebSocket webSocket) { var connectionId = Guid.NewGuid().ToString(); _connections.TryAdd(connectionId, webSocket); return connectionId; }
public void RemoveConnection(string connectionId) { _connections.TryRemove(connectionId, out _); }
public WebSocket GetConnection(string connectionId) { _connections.TryGetValue(connectionId, out var connection); return connection; }
public IEnumerable<string> GetAllConnectionIds() { return _connections.Keys; }
public async Task SendAsync(string connectionId, string message) { if (_connections.TryGetValue(connectionId, out var webSocket)) { if (webSocket.State == WebSocketState.Open) { var bytes = Encoding.UTF8.GetBytes(message); await webSocket.SendAsync( new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None ); } } }
public async Task BroadcastAsync(string message, string excludeConnectionId = null) { var tasks = new List<Task>();
foreach (var pair in _connections) { if (pair.Key != excludeConnectionId && pair.Value.State == WebSocketState.Open) { tasks.Add(SendAsync(pair.Key, message)); } }
await Task.WhenAll(tasks); }}
SignalR Implementation
SignalR Hub
Implement real-time communication with SignalR:
using Microsoft.AspNetCore.SignalR;using Microsoft.AspNetCore.Authorization;using System.Threading.Tasks;using System.Collections.Concurrent;
public class ChatHub : Hub{ private static readonly ConcurrentDictionary<string, UserConnection> _connections = new(); private readonly ILogger<ChatHub> _logger;
public ChatHub(ILogger<ChatHub> logger) { _logger = logger; }
public override async Task OnConnectedAsync() { var connectionId = Context.ConnectionId; var userId = Context.UserIdentifier ?? "anonymous";
_connections.TryAdd(connectionId, new UserConnection { ConnectionId = connectionId, UserId = userId, ConnectedAt = DateTime.UtcNow });
_logger.LogInformation($"User {userId} connected with ID {connectionId}");
// Notify others await Clients.Others.SendAsync("UserConnected", userId);
// Send welcome message await Clients.Caller.SendAsync("Welcome", new { message = "Welcome to the chat!", connectionId, onlineUsers = GetOnlineUsers() });
await base.OnConnectedAsync(); }
public override async Task OnDisconnectedAsync(Exception exception) { var connectionId = Context.ConnectionId;
if (_connections.TryRemove(connectionId, out var connection)) { _logger.LogInformation($"User {connection.UserId} disconnected"); await Clients.Others.SendAsync("UserDisconnected", connection.UserId); }
await base.OnDisconnectedAsync(exception); }
public async Task SendMessage(string message) { var userId = Context.UserIdentifier ?? "anonymous"; var timestamp = DateTime.UtcNow;
var chatMessage = new ChatMessage { UserId = userId, Message = message, Timestamp = timestamp };
await Clients.All.SendAsync("ReceiveMessage", chatMessage); }
public async Task SendPrivateMessage(string targetUserId, string message) { var senderId = Context.UserIdentifier ?? "anonymous"; var timestamp = DateTime.UtcNow;
var privateMessage = new PrivateMessage { SenderId = senderId, ReceiverId = targetUserId, Message = message, Timestamp = timestamp };
// Send to target user await Clients.User(targetUserId).SendAsync("ReceivePrivateMessage", privateMessage);
// Send confirmation to sender await Clients.Caller.SendAsync("PrivateMessageSent", privateMessage); }
public async Task JoinGroup(string groupName) { await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
var userId = Context.UserIdentifier ?? "anonymous"; await Clients.Group(groupName).SendAsync("UserJoinedGroup", new { groupName, userId, message = $"{userId} joined the group" }); }
public async Task LeaveGroup(string groupName) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
var userId = Context.UserIdentifier ?? "anonymous"; await Clients.Group(groupName).SendAsync("UserLeftGroup", new { groupName, userId, message = $"{userId} left the group" }); }
public async Task SendToGroup(string groupName, string message) { var userId = Context.UserIdentifier ?? "anonymous";
await Clients.Group(groupName).SendAsync("ReceiveGroupMessage", new { groupName, userId, message, timestamp = DateTime.UtcNow }); }
[Authorize] public async Task SendAuthenticatedMessage(string message) { var userId = Context.UserIdentifier; var userName = Context.User?.Identity?.Name;
await Clients.All.SendAsync("ReceiveAuthenticatedMessage", new { userId, userName, message, timestamp = DateTime.UtcNow }); }
public async Task<object> GetConnectionInfo() { return new { connectionId = Context.ConnectionId, userId = Context.UserIdentifier, isAuthenticated = Context.User?.Identity?.IsAuthenticated ?? false, claims = Context.User?.Claims?.Select(c => new { c.Type, c.Value }) }; }
public async Task Typing(bool isTyping) { var userId = Context.UserIdentifier ?? "anonymous"; await Clients.Others.SendAsync("UserTyping", new { userId, isTyping }); }
public async Task<IEnumerable<string>> GetOnlineUsers() { return _connections.Values.Select(c => c.UserId).Distinct(); }
private class UserConnection { public string ConnectionId { get; set; } public string UserId { get; set; } public DateTime ConnectedAt { get; set; } }
private class ChatMessage { public string UserId { get; set; } public string Message { get; set; } public DateTime Timestamp { get; set; } }
private class PrivateMessage : ChatMessage { public string SenderId { get; set; } public string ReceiverId { get; set; } }}
SignalR Client
Connect to SignalR hub from C# client:
using Microsoft.AspNetCore.SignalR.Client;using System;using System.Threading.Tasks;
public class SignalRClient : IAsyncDisposable{ private HubConnection _connection; private readonly string _hubUrl; private readonly ILogger<SignalRClient> _logger;
public event EventHandler<string> MessageReceived; public event EventHandler<string> Connected; public event EventHandler<string> Disconnected;
public HubConnectionState State => _connection?.State ?? HubConnectionState.Disconnected;
public SignalRClient(string hubUrl, ILogger<SignalRClient> logger) { _hubUrl = hubUrl; _logger = logger; }
public async Task ConnectAsync() { _connection = new HubConnectionBuilder() .WithUrl(_hubUrl, options => { options.AccessTokenProvider = () => Task.FromResult(GetAccessToken()); }) .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30) }) .ConfigureLogging(logging => { logging.SetMinimumLevel(LogLevel.Information); }) .Build();
// Register event handlers RegisterHandlers();
// Handle reconnection _connection.Reconnecting += (error) => { _logger.LogWarning($"Connection lost. Reconnecting... Error: {error?.Message}"); return Task.CompletedTask; };
_connection.Reconnected += (connectionId) => { _logger.LogInformation($"Reconnected with ID: {connectionId}"); Connected?.Invoke(this, connectionId); return Task.CompletedTask; };
_connection.Closed += async (error) => { _logger.LogError($"Connection closed. Error: {error?.Message}"); Disconnected?.Invoke(this, error?.Message ?? "Connection closed");
// Manual reconnection if automatic reconnection failed await Task.Delay(new Random().Next(0, 5) * 1000); await ConnectAsync(); };
try { await _connection.StartAsync(); _logger.LogInformation("Connected to SignalR hub"); Connected?.Invoke(this, _connection.ConnectionId); } catch (Exception ex) { _logger.LogError(ex, "Failed to connect to SignalR hub"); throw; } }
private void RegisterHandlers() { _connection.On<ChatMessage>("ReceiveMessage", (message) => { _logger.LogInformation($"Message received: {message.Message}"); MessageReceived?.Invoke(this, message.Message); });
_connection.On<PrivateMessage>("ReceivePrivateMessage", (message) => { _logger.LogInformation($"Private message from {message.SenderId}: {message.Message}"); HandlePrivateMessage(message); });
_connection.On<string>("UserConnected", (userId) => { _logger.LogInformation($"User connected: {userId}"); });
_connection.On<string>("UserDisconnected", (userId) => { _logger.LogInformation($"User disconnected: {userId}"); });
_connection.On<object>("Welcome", (data) => { _logger.LogInformation($"Welcome message received: {data}"); });
_connection.On<dynamic>("UserTyping", (data) => { HandleUserTyping(data.userId, data.isTyping); }); }
public async Task SendMessageAsync(string message) { if (_connection.State != HubConnectionState.Connected) { throw new InvalidOperationException("Not connected to hub"); }
await _connection.InvokeAsync("SendMessage", message); }
public async Task SendPrivateMessageAsync(string targetUserId, string message) { await _connection.InvokeAsync("SendPrivateMessage", targetUserId, message); }
public async Task JoinGroupAsync(string groupName) { await _connection.InvokeAsync("JoinGroup", groupName); }
public async Task LeaveGroupAsync(string groupName) { await _connection.InvokeAsync("LeaveGroup", groupName); }
public async Task SendToGroupAsync(string groupName, string message) { await _connection.InvokeAsync("SendToGroup", groupName, message); }
public async Task<T> InvokeAsync<T>(string methodName, params object[] args) { return await _connection.InvokeAsync<T>(methodName, args); }
public async Task SendAsync(string methodName, params object[] args) { await _connection.SendAsync(methodName, args); }
public IDisposable On<T>(string methodName, Action<T> handler) { return _connection.On(methodName, handler); }
private void HandlePrivateMessage(PrivateMessage message) { // Handle private message }
private void HandleUserTyping(string userId, bool isTyping) { // Handle typing indicator }
private string GetAccessToken() { // Retrieve access token return "your-access-token"; }
public async ValueTask DisposeAsync() { if (_connection != null) { await _connection.DisposeAsync(); } }
private class ChatMessage { public string UserId { get; set; } public string Message { get; set; } public DateTime Timestamp { get; set; } }
private class PrivateMessage : ChatMessage { public string SenderId { get; set; } public string ReceiverId { get; set; } }}
Unity WebSocket Implementation
WebSocket Client for Unity
Using WebSocketSharp for Unity games:
using UnityEngine;using WebSocketSharp;using System;using System.Collections.Generic;using System.Collections.Concurrent;using Newtonsoft.Json;
public class UnityWebSocketClient : MonoBehaviour{ private WebSocket _webSocket; private readonly ConcurrentQueue<Action> _mainThreadActions = new(); private bool _isConnected = false;
[SerializeField] private string serverUrl = "wss://your-server.com/ws"; [SerializeField] private int reconnectDelay = 5;
public event Action OnConnected; public event Action<string> OnMessageReceived; public event Action<string> OnDisconnected; public event Action<string> OnError;
private void Start() { Connect(); }
private void Connect() { try { _webSocket = new WebSocket(serverUrl);
// Configure WebSocket _webSocket.Compression = CompressionMethod.Deflate; _webSocket.WaitTime = TimeSpan.FromSeconds(10);
// Set up event handlers _webSocket.OnOpen += OnWebSocketOpen; _webSocket.OnMessage += OnWebSocketMessage; _webSocket.OnError += OnWebSocketError; _webSocket.OnClose += OnWebSocketClose;
// Add headers _webSocket.SetCookie(new Cookie("sessionId", GetSessionId())); _webSocket.CustomHeaders = new Dictionary<string, string> { { "Authorization", $"Bearer {GetAuthToken()}" }, { "X-Client-Version", Application.version } };
_webSocket.ConnectAsync(); Debug.Log($"Connecting to {serverUrl}..."); } catch (Exception ex) { Debug.LogError($"Failed to create WebSocket: {ex.Message}"); ScheduleReconnect(); } }
private void OnWebSocketOpen(object sender, EventArgs e) { Debug.Log("WebSocket connected!"); _isConnected = true;
EnqueueMainThreadAction(() => { OnConnected?.Invoke();
// Send initial message SendMessage(new { type = "register", playerId = GetPlayerId(), platform = Application.platform.ToString() }); }); }
private void OnWebSocketMessage(object sender, MessageEventArgs e) { if (e.IsText) { Debug.Log($"Received text: {e.Data}"); EnqueueMainThreadAction(() => HandleMessage(e.Data)); } else if (e.IsBinary) { Debug.Log($"Received binary: {e.RawData.Length} bytes"); EnqueueMainThreadAction(() => HandleBinaryMessage(e.RawData)); } }
private void OnWebSocketError(object sender, ErrorEventArgs e) { Debug.LogError($"WebSocket error: {e.Message}"); EnqueueMainThreadAction(() => OnError?.Invoke(e.Message)); }
private void OnWebSocketClose(object sender, CloseEventArgs e) { Debug.Log($"WebSocket closed: {e.Code} - {e.Reason}"); _isConnected = false;
EnqueueMainThreadAction(() => { OnDisconnected?.Invoke(e.Reason);
if (e.Code != 1000) // Not normal closure { ScheduleReconnect(); } }); }
private void HandleMessage(string message) { try { var json = JsonConvert.DeserializeObject<Dictionary<string, object>>(message); var type = json["type"].ToString();
switch (type) { case "game_state": UpdateGameState(json); break; case "player_action": HandlePlayerAction(json); break; case "chat": HandleChatMessage(json); break; default: OnMessageReceived?.Invoke(message); break; } } catch (Exception ex) { Debug.LogError($"Failed to handle message: {ex.Message}"); } }
private void HandleBinaryMessage(byte[] data) { // Handle binary data (e.g., game state snapshots, audio data) }
private void UpdateGameState(Dictionary<string, object> data) { // Update game state based on server data }
private void HandlePlayerAction(Dictionary<string, object> data) { // Handle other player's actions }
private void HandleChatMessage(Dictionary<string, object> data) { var sender = data["sender"].ToString(); var message = data["message"].ToString(); Debug.Log($"Chat from {sender}: {message}"); }
public void SendMessage(object message) { if (_isConnected && _webSocket != null) { var json = JsonConvert.SerializeObject(message); _webSocket.SendAsync(json, (success) => { if (!success) { Debug.LogError("Failed to send message"); } }); } else { Debug.LogWarning("Cannot send message: not connected"); } }
public void SendBinary(byte[] data) { if (_isConnected && _webSocket != null) { _webSocket.SendAsync(data, (success) => { if (!success) { Debug.LogError("Failed to send binary data"); } }); } }
private void ScheduleReconnect() { Debug.Log($"Scheduling reconnect in {reconnectDelay} seconds..."); Invoke(nameof(Connect), reconnectDelay); }
private void EnqueueMainThreadAction(Action action) { _mainThreadActions.Enqueue(action); }
private void Update() { // Execute actions on main thread while (_mainThreadActions.TryDequeue(out var action)) { action?.Invoke(); } }
private void OnDestroy() { Disconnect(); }
private void OnApplicationPause(bool pauseStatus) { if (pauseStatus) { Disconnect(); } else { Connect(); } }
private void OnApplicationFocus(bool hasFocus) { if (!hasFocus && Application.platform == RuntimePlatform.WebGLPlayer) { // Handle WebGL focus loss } }
public void Disconnect() { if (_webSocket != null) { _webSocket.Close(CloseStatusCode.Normal, "Client disconnecting"); _webSocket = null; } _isConnected = false; }
private string GetSessionId() { return PlayerPrefs.GetString("SessionId", Guid.NewGuid().ToString()); }
private string GetAuthToken() { return PlayerPrefs.GetString("AuthToken", ""); }
private string GetPlayerId() { return PlayerPrefs.GetString("PlayerId", SystemInfo.deviceUniqueIdentifier); }}
Comprehensive Testing Strategies
xUnit Integration Tests
Implement comprehensive WebSocket testing with xUnit and ASP.NET Core Test Server:
using Microsoft.AspNetCore.Hosting;using Microsoft.AspNetCore.TestHost;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Logging;using System.Net.WebSockets;using System.Text;using System.Text.Json;using Xunit;using Xunit.Abstractions;
namespace WebSocketGuide.Tests.Integration;
public class WebSocketIntegrationTests : IAsyncLifetime{ private readonly TestServer _server; private readonly HttpClient _httpClient; private readonly ITestOutputHelper _output;
public WebSocketIntegrationTests(ITestOutputHelper output) { _output = output;
var builder = new WebHostBuilder() .ConfigureServices(services => { services.AddLogging(builder => builder.AddXUnit(_output)); services.AddSingleton<WebSocketConnectionManager>(); services.AddTransient<WebSocketHandler>(); }) .Configure(app => { app.UseWebSockets(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.Map("/ws", async context => { if (context.WebSockets.IsWebSocketRequest) { using var webSocket = await context.WebSockets.AcceptWebSocketAsync(); var handler = context.RequestServices.GetRequiredService<WebSocketHandler>(); await handler.HandleAsync(context, webSocket); } else { context.Response.StatusCode = 400; } }); }); });
_server = new TestServer(builder); _httpClient = _server.CreateClient(); }
[Fact] public async Task WebSocket_CanEstablishConnection() { // Arrange var webSocketClient = _server.CreateWebSocketClient(); var uri = new Uri(_server.BaseAddress.Replace("http", "ws") + "ws");
// Act var webSocket = await webSocketClient.ConnectAsync(uri, CancellationToken.None);
// Assert Assert.Equal(WebSocketState.Open, webSocket.State);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test completed", CancellationToken.None); }
[Fact] public async Task WebSocket_CanSendAndReceiveTextMessage() { // Arrange var webSocketClient = _server.CreateWebSocketClient(); var uri = new Uri(_server.BaseAddress.Replace("http", "ws") + "ws"); var webSocket = await webSocketClient.ConnectAsync(uri, CancellationToken.None);
var testMessage = new { type = "test", content = "Hello, WebSocket!" }; var messageJson = JsonSerializer.Serialize(testMessage);
// Act await SendTextMessageAsync(webSocket, messageJson); var receivedMessage = await ReceiveTextMessageAsync(webSocket);
// Assert Assert.Contains("Hello, WebSocket!", receivedMessage);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test completed", CancellationToken.None); }
[Fact] public async Task WebSocket_HandlesBinaryMessages() { // Arrange var webSocketClient = _server.CreateWebSocketClient(); var uri = new Uri(_server.BaseAddress.Replace("http", "ws") + "ws"); var webSocket = await webSocketClient.ConnectAsync(uri, CancellationToken.None);
var binaryData = Encoding.UTF8.GetBytes("Binary test data");
// Act await SendBinaryMessageAsync(webSocket, binaryData); var receivedData = await ReceiveBinaryMessageAsync(webSocket);
// Assert Assert.Equal(binaryData, receivedData);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test completed", CancellationToken.None); }
[Fact] public async Task WebSocket_HandlesMultipleConcurrentConnections() { // Arrange const int connectionCount = 5; var tasks = new List<Task>(); var webSocketClient = _server.CreateWebSocketClient();
// Act for (int i = 0; i < connectionCount; i++) { var connectionId = i; tasks.Add(Task.Run(async () => { var uri = new Uri(_server.BaseAddress.Replace("http", "ws") + "ws"); var webSocket = await webSocketClient.ConnectAsync(uri, CancellationToken.None);
await SendTextMessageAsync(webSocket, $"Message from connection {connectionId}"); var response = await ReceiveTextMessageAsync(webSocket);
Assert.Contains($"{connectionId}", response);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test completed", CancellationToken.None); })); }
await Task.WhenAll(tasks);
// Assert - all tasks completed successfully Assert.All(tasks, task => Assert.True(task.IsCompletedSuccessfully)); }
[Theory] [InlineData(10)] [InlineData(100)] [InlineData(1000)] public async Task WebSocket_HandlesMessageBatches(int messageCount) { // Arrange var webSocketClient = _server.CreateWebSocketClient(); var uri = new Uri(_server.BaseAddress.Replace("http", "ws") + "ws"); var webSocket = await webSocketClient.ConnectAsync(uri, CancellationToken.None);
var messages = Enumerable.Range(0, messageCount) .Select(i => $"Message {i}") .ToList();
// Act var sendTasks = messages.Select(msg => SendTextMessageAsync(webSocket, msg)); await Task.WhenAll(sendTasks);
var receivedMessages = new List<string>(); for (int i = 0; i < messageCount; i++) { var received = await ReceiveTextMessageAsync(webSocket); receivedMessages.Add(received); }
// Assert Assert.Equal(messageCount, receivedMessages.Count);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test completed", CancellationToken.None); }
[Fact] public async Task WebSocket_HandlesConnectionClose() { // Arrange var webSocketClient = _server.CreateWebSocketClient(); var uri = new Uri(_server.BaseAddress.Replace("http", "ws") + "ws"); var webSocket = await webSocketClient.ConnectAsync(uri, CancellationToken.None);
// Act await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Intentional close", CancellationToken.None);
// Assert Assert.Equal(WebSocketState.Closed, webSocket.State); }
private static async Task SendTextMessageAsync(WebSocket webSocket, string message) { var buffer = Encoding.UTF8.GetBytes(message); await webSocket.SendAsync( new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None ); }
private static async Task SendBinaryMessageAsync(WebSocket webSocket, byte[] data) { await webSocket.SendAsync( new ArraySegment<byte>(data), WebSocketMessageType.Binary, true, CancellationToken.None ); }
private static async Task<string> ReceiveTextMessageAsync(WebSocket webSocket) { var buffer = new byte[4096]; var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); return Encoding.UTF8.GetString(buffer, 0, result.Count); }
private static async Task<byte[]> ReceiveBinaryMessageAsync(WebSocket webSocket) { var buffer = new byte[4096]; var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); return buffer.Take(result.Count).ToArray(); }
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync() { _httpClient?.Dispose(); _server?.Dispose(); }}
Performance and Load Testing
Create comprehensive performance tests:
using Microsoft.Extensions.Logging;using System.Collections.Concurrent;using System.Diagnostics;using Xunit;using Xunit.Abstractions;
namespace WebSocketGuide.Tests.Performance;
public class WebSocketPerformanceTests{ private readonly ITestOutputHelper _output; private readonly ILogger<WebSocketPerformanceTests> _logger;
public WebSocketPerformanceTests(ITestOutputHelper output) { _output = output; _logger = new XUnitLogger<WebSocketPerformanceTests>(output); }
[Fact] public async Task WebSocket_PerformanceTest_MultipleConnections() { // Arrange const int connectionCount = 100; const int messagesPerConnection = 10; const int timeoutMs = 30000;
var connectionTasks = new List<Task<ConnectionResult>>(); var stopwatch = Stopwatch.StartNew();
// Act for (int i = 0; i < connectionCount; i++) { var connectionId = i; connectionTasks.Add(CreateConnectionAndSendMessages(connectionId, messagesPerConnection)); }
var results = await Task.WhenAll(connectionTasks); stopwatch.Stop();
// Assert and Report var successfulConnections = results.Count(r => r.Success); var totalMessages = results.Sum(r => r.MessagesSent); var averageLatency = results.Where(r => r.Success).Average(r => r.AverageLatencyMs);
_output.WriteLine($"Performance Test Results:"); _output.WriteLine($" Connections: {successfulConnections}/{connectionCount}"); _output.WriteLine($" Total Messages: {totalMessages}"); _output.WriteLine($" Total Time: {stopwatch.ElapsedMilliseconds}ms"); _output.WriteLine($" Messages/Second: {totalMessages / (stopwatch.ElapsedMilliseconds / 1000.0):F2}"); _output.WriteLine($" Average Latency: {averageLatency:F2}ms");
Assert.True(successfulConnections >= connectionCount * 0.95, "At least 95% of connections should succeed"); Assert.True(averageLatency < 100, "Average latency should be less than 100ms"); }
[Fact] public async Task WebSocket_ThroughputTest_SingleConnection() { // Arrange const int messageCount = 1000; var client = new ReconnectingWebSocketClient("ws://localhost:5000/ws"); var messages = Enumerable.Range(0, messageCount) .Select(i => $"Performance test message {i} - {DateTime.UtcNow:O}") .ToList();
var receivedMessages = new ConcurrentQueue<string>(); var receivedCount = 0;
client.MessageReceived += (sender, message) => { receivedMessages.Enqueue(message); Interlocked.Increment(ref receivedCount); };
// Act await client.ConnectAsync(); var stopwatch = Stopwatch.StartNew();
var sendTasks = messages.Select(async msg => { await client.SendAsync(msg); });
await Task.WhenAll(sendTasks);
// Wait for all messages to be received var timeout = TimeSpan.FromSeconds(30); var start = DateTime.UtcNow; while (receivedCount < messageCount && DateTime.UtcNow - start < timeout) { await Task.Delay(10); }
stopwatch.Stop();
// Assert and Report _output.WriteLine($"Throughput Test Results:"); _output.WriteLine($" Messages Sent: {messageCount}"); _output.WriteLine($" Messages Received: {receivedCount}"); _output.WriteLine($" Time Elapsed: {stopwatch.ElapsedMilliseconds}ms"); _output.WriteLine($" Throughput: {messageCount / (stopwatch.ElapsedMilliseconds / 1000.0):F2} messages/second");
Assert.Equal(messageCount, receivedCount); await client.DisconnectAsync(); }
[Fact] public async Task WebSocket_MemoryUsageTest() { // Arrange const int connectionCount = 50; var initialMemory = GC.GetTotalMemory(true); var clients = new List<ReconnectingWebSocketClient>();
try { // Act - Create connections for (int i = 0; i < connectionCount; i++) { var client = new ReconnectingWebSocketClient("ws://localhost:5000/ws"); clients.Add(client); await client.ConnectAsync(); }
var afterConnectionMemory = GC.GetTotalMemory(false); var memoryPerConnection = (afterConnectionMemory - initialMemory) / connectionCount;
// Send messages var tasks = clients.Select(async (client, index) => { for (int i = 0; i < 10; i++) { await client.SendAsync($"Memory test message {i} from client {index}"); await Task.Delay(10); } });
await Task.WhenAll(tasks);
var afterMessagesMemory = GC.GetTotalMemory(false);
// Assert and Report _output.WriteLine($"Memory Usage Test Results:"); _output.WriteLine($" Initial Memory: {initialMemory / 1024:N0} KB"); _output.WriteLine($" After Connections: {afterConnectionMemory / 1024:N0} KB"); _output.WriteLine($" After Messages: {afterMessagesMemory / 1024:N0} KB"); _output.WriteLine($" Memory per Connection: {memoryPerConnection / 1024:N0} KB");
Assert.True(memoryPerConnection < 50 * 1024, "Memory per connection should be less than 50KB"); } finally { // Cleanup foreach (var client in clients) { await client.DisconnectAsync(); client.Dispose(); }
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } }
private async Task<ConnectionResult> CreateConnectionAndSendMessages(int connectionId, int messageCount) { var result = new ConnectionResult { ConnectionId = connectionId }; var latencies = new List<double>();
try { var client = new ReconnectingWebSocketClient("ws://localhost:5000/ws"); await client.ConnectAsync();
for (int i = 0; i < messageCount; i++) { var stopwatch = Stopwatch.StartNew(); await client.SendAsync($"Message {i} from connection {connectionId}");
// Simple echo response timing (in real scenario, you'd wait for response) await Task.Delay(1); // Simulate network latency stopwatch.Stop();
latencies.Add(stopwatch.Elapsed.TotalMilliseconds); result.MessagesSent++; }
await client.DisconnectAsync(); client.Dispose();
result.Success = true; result.AverageLatencyMs = latencies.Average(); } catch (Exception ex) { _logger.LogError(ex, "Connection {ConnectionId} failed", connectionId); result.Success = false; result.Error = ex.Message; }
return result; }
private class ConnectionResult { public int ConnectionId { get; set; } public bool Success { get; set; } public int MessagesSent { get; set; } public double AverageLatencyMs { get; set; } public string? Error { get; set; } }}
public class XUnitLogger<T> : ILogger<T>{ private readonly ITestOutputHelper _output;
public XUnitLogger(ITestOutputHelper output) { _output = output; }
public IDisposable BeginScope<TState>(TState state) => null!; public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { _output.WriteLine($"[{logLevel}] {formatter(state, exception)}"); }}
Production Deployment and Monitoring
Docker Configuration
Create a production-ready Docker setup:
# Build stageFROM mcr.microsoft.com/dotnet/sdk:8.0 AS buildWORKDIR /src
# Copy project filesCOPY ["WebSocketGuide.Server/WebSocketGuide.Server.csproj", "WebSocketGuide.Server/"]COPY ["WebSocketGuide.Core/WebSocketGuide.Core.csproj", "WebSocketGuide.Core/"]
# Restore dependenciesRUN dotnet restore "WebSocketGuide.Server/WebSocketGuide.Server.csproj"
# Copy source codeCOPY . .
# Build applicationWORKDIR "/src/WebSocketGuide.Server"RUN dotnet build "WebSocketGuide.Server.csproj" -c Release -o /app/build
# Publish stageFROM build AS publishRUN dotnet publish "WebSocketGuide.Server.csproj" -c Release -o /app/publish
# Runtime stageFROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
# Create non-root userRUN groupadd -r appuser && useradd -r -g appuser appuser
# Install curl for health checksRUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy published appCOPY --from=publish /app/publish .
# Set ownershipRUN chown -R appuser:appuser /app
# Switch to non-root userUSER appuser
# Configure ASP.NET CoreENV ASPNETCORE_URLS=http://+:8080ENV ASPNETCORE_ENVIRONMENT=Production
# Health checkHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1
EXPOSE 8080
ENTRYPOINT ["dotnet", "WebSocketGuide.Server.dll"]
Kubernetes Deployment
Deploy with proper scaling and monitoring:
apiVersion: apps/v1kind: Deploymentmetadata: name: websocket-server labels: app: websocket-serverspec: replicas: 3 selector: matchLabels: app: websocket-server template: metadata: labels: app: websocket-server annotations: prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/metrics" spec: containers: - name: websocket-server image: your-registry/websocket-server:latest ports: - containerPort: 8080 name: http protocol: TCP env: - name: ASPNETCORE_ENVIRONMENT value: "Production" - name: WebSocket__MaxConnections value: "10000" - name: WebSocket__Redis__ConnectionString valueFrom: secretKeyRef: name: websocket-secrets key: redis-connection-string - name: Logging__LogLevel__Default value: "Information" resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "1Gi" cpu: "500m" livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 volumeMounts: - name: config mountPath: /app/config readOnly: true volumes: - name: config configMap: name: websocket-config affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - websocket-server topologyKey: kubernetes.io/hostname
---apiVersion: v1kind: Servicemetadata: name: websocket-service labels: app: websocket-serverspec: type: ClusterIP selector: app: websocket-server ports: - port: 80 targetPort: 8080 protocol: TCP name: http sessionAffinity: ClientIP # Important for WebSocket connections sessionAffinityConfig: clientIP: timeoutSeconds: 3600
---apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: name: websocket-hpaspec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: websocket-server minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 - type: Pods pods: metric: name: websocket_active_connections target: type: AverageValue averageValue: "1000"
---apiVersion: v1kind: ConfigMapmetadata: name: websocket-configdata: appsettings.Production.json: | { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "WebSocket": { "MaxConnections": 10000, "ReceiveBufferSize": 4096, "KeepAliveIntervalSeconds": 30, "EnableCompression": true, "RateLimit": { "Enabled": true, "RequestsPerMinute": 120, "BurstSize": 20 } }, "Monitoring": { "EnableMetrics": true, "EnableHealthChecks": true, "MetricsEndpoint": "/metrics", "HealthEndpoint": "/health" } }
Azure Service Fabric Configuration
For enterprise deployments on Azure Service Fabric:
<?xml version="1.0" encoding="utf-8"?><ApplicationManifest ApplicationTypeName="WebSocketGuideType" ApplicationTypeVersion="1.0.0" xmlns="http://schemas.microsoft.com/2011/01/fabric" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Parameters> <Parameter Name="WebSocketServer_InstanceCount" DefaultValue="-1" /> <Parameter Name="WebSocketServer_MaxConnections" DefaultValue="10000" /> <Parameter Name="Redis_ConnectionString" DefaultValue="" /> </Parameters>
<ServiceManifestImport> <ServiceManifestRef ServiceManifestName="WebSocketServerPkg" ServiceManifestVersion="1.0.0" /> <ConfigOverrides> <ConfigOverride Name="Config"> <Settings> <Section Name="WebSocket"> <Parameter Name="MaxConnections" Value="[WebSocketServer_MaxConnections]" /> </Section> <Section Name="Redis"> <Parameter Name="ConnectionString" Value="[Redis_ConnectionString]" /> </Section> </Settings> </ConfigOverride> </ConfigOverrides> <Policies> <EndpointBindingPolicy EndpointRef="ServiceEndpoint" CertificateRef="WebSocketServerCert"/> </Policies> </ServiceManifestImport>
<DefaultServices> <Service Name="WebSocketServer" ServicePackageActivationMode="ExclusiveProcess"> <StatelessService ServiceTypeName="WebSocketServerType" InstanceCount="[WebSocketServer_InstanceCount]"> <SingletonPartition /> </StatelessService> </Service> </DefaultServices>
<Certificates> <EndpointCertificate Name="WebSocketServerCert" X509FindType="FindByThumbprint" X509FindValue="[WebSocketServerCertThumbprint]" /> </Certificates></ApplicationManifest>
Best Practices
Connection Management
- Implement automatic reconnection with exponential backoff
- Handle network state changes appropriately
- Use connection pooling for multiple connections
- Implement proper connection lifecycle management
Security
- Always use WSS (WebSocket Secure) in production
- Implement proper authentication and authorization
- Validate all incoming messages
- Use rate limiting and throttling
- Implement CORS properly
Performance
- Use binary messages for large data transfers
- Implement message compression
- Batch small messages when possible
- Use async/await properly to avoid blocking
- Configure appropriate buffer sizes
Error Handling
- Implement comprehensive error handling
- Log errors with appropriate detail levels
- Provide graceful degradation
- Handle partial message failures
- Implement circuit breaker pattern
Monitoring
- Track connection metrics (open/close/error rates)
- Monitor message throughput and latency
- Log slow message processing
- Implement health checks
- Use distributed tracing for debugging
Troubleshooting Common Issues
Connection Problems
Issue: WebSocket connections are being refused or timing out Solution: Check firewall settings, proxy configurations, and ensure your server is properly configured to accept WebSocket upgrades. Verify the Origin header is in your allowed origins list.
Issue: Connections drop frequently in production Solution: Implement proper heartbeat/ping mechanisms, check load balancer configuration for sticky sessions, and ensure your connection timeout settings are appropriate.
Issue: Memory leaks with long-running connections Solution: Properly implement IDisposable patterns, use weak event subscriptions where appropriate, and monitor memory usage with diagnostic tools like PerfView or Application Insights.
Performance Issues
Issue: High CPU usage with many concurrent connections Solution: Profile your message handling code, consider using channels for message processing, and ensure you’re not blocking async operations.
Issue: Message delivery delays under load Solution: Implement message queuing, consider batching small messages, and optimize your JSON serialization with source generators.
Issue: SignalR group operations are slow Solution: Use Redis backplane for scale-out scenarios, optimize group membership operations, and consider message filtering strategies.
Authentication and Authorization Issues
Issue: JWT tokens expiring during long-lived connections Solution: Implement token refresh mechanisms, use shorter-lived access tokens with refresh tokens, or implement connection re-authentication flows.
Issue: CORS errors in browser clients Solution: Configure CORS properly in your ASP.NET Core application, ensure your allowed origins include the correct protocols and ports.
Conclusion
C# and .NET provide a comprehensive, mature, and powerful platform for WebSocket development. From the low-level ClientWebSocket
API that gives you complete control over the connection lifecycle, to high-level frameworks like SignalR that abstract away the complexity of real-time communication, the .NET ecosystem has solutions for every use case.
Key takeaways from this guide:
-
Choose the Right Approach: Use
ClientWebSocket
for maximum control, ASP.NET Core WebSocket middleware for custom server implementations, and SignalR for rapid development of real-time applications. -
Implement Robust Error Handling: WebSocket connections are inherently unreliable due to network conditions. Implement comprehensive reconnection strategies, circuit breakers, and graceful degradation.
-
Design for Scale: Consider connection pooling, message batching, Redis backplanes for SignalR, and horizontal scaling strategies from the beginning.
-
Prioritize Security: Always use WSS in production, implement proper authentication and authorization, validate all inputs, and follow security best practices.
-
Monitor and Observe: Implement comprehensive monitoring, health checks, and distributed tracing to understand your WebSocket application’s behavior in production.
-
Test Thoroughly: Use the testing patterns demonstrated in this guide to ensure your WebSocket implementation works correctly under load and handles edge cases gracefully.
The .NET ecosystem’s strong typing, excellent tooling, cross-platform capabilities, and enterprise-grade features make it an excellent choice for building robust, scalable, and maintainable WebSocket applications. Whether you’re building a simple chat application, a real-time collaborative tool, or a high-frequency trading platform, .NET has the tools and frameworks you need to succeed.
With the patterns, practices, and code examples provided in this guide, you have everything needed to implement production-ready WebSocket applications that can handle thousands of concurrent connections while maintaining excellent performance, security, and reliability.
Monitoring
- Track connection metrics
- Monitor message throughput
- Log performance metrics
- Implement health checks
- Use distributed tracing for debugging
The .NET Advantage for Enterprise WebSocket Solutions
The .NET ecosystem’s approach to WebSocket development reflects Microsoft’s broader strategy of providing comprehensive, enterprise-ready solutions. This isn’t just about providing WebSocket APIs - it’s about integrating WebSocket capabilities throughout the entire platform. From Azure SignalR Service for managed WebSocket infrastructure to Visual Studio’s sophisticated debugging capabilities for WebSocket connections, the entire toolchain is optimized for productive development.
The evolution from .NET Framework to .NET Core and now .NET 6+ represents a fundamental shift in how Microsoft approaches web development. The platform is now truly cross-platform, high-performance, and cloud-native. For WebSocket applications, this means being able to develop on any operating system, deploy to any cloud provider, and achieve performance that rivals or exceeds traditional compiled languages. The performance improvements in recent .NET versions have been particularly impressive for I/O-bound operations like WebSocket communication.
Integration with the broader Microsoft ecosystem provides unique advantages for enterprise WebSocket applications. Azure Active Directory integration simplifies authentication, Application Insights provides comprehensive monitoring, and Azure DevOps enables sophisticated CI/CD pipelines. For organizations already invested in the Microsoft ecosystem, these integrations reduce development time and operational complexity significantly. The ability to use the same identity provider, monitoring solution, and deployment pipeline for both traditional web applications and WebSocket services simplifies the overall architecture.