Введение: Почему WebSocket на C# — выбор профессионалов
В современной веб-разработке real-time взаимодействие стало стандартом. Торговые платформы, системы мониторинга, онлайн-игры и чаты — все они требуют мгновенной передачи данных. Технология WebSocket на C# (.NET) предоставляет мощный и гибкий инструмент для создания таких приложений. В отличие от опросов HTTP, WebSocket устанавливает постоянное двунаправленное соединение, что радикально снижает задержки и нагрузку на сервер.
В этой статье мы рассмотрим продвинутые аспекты работы с WebSocket в C#. Вы узнаете, как построить отказоустойчивый сервер, управлять тысячами одновременных подключений, обрабатывать ошибки и интегрировать WebSocket с популярными фреймворками. Материал рассчитан на разработчиков, уже знакомых с основами .NET и асинхронного программирования.
Архитектура продвинутого WebSocket-сервера на C#
Базовый пример WebSocket-сервера в C# часто ограничивается одним обработчиком. Для production-решений требуется продуманная архитектура. Рассмотрим ключевые компоненты.
Управление подключениями и идентификация клиентов
Вместо простого списка подключений используйте ConcurrentDictionary для потокобезопасного хранения. Каждому клиенту присваивается уникальный идентификатор (GUID) и хранятся метаданные (например, имя пользователя, группа).
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);
}
Такой подход позволяет легко отправлять сообщения конкретным пользователям, группам или всем сразу.
Обработка фрагментированных сообщений
В реальных приложениях данные могут приходить фрагментами. Стандартный метод ReceiveAsync возвращает один фрагмент. Для сборки полного сообщения используйте цикл:
public async Task<byte[]> ReceiveFullMessageAsync(WebSocket socket, CancellationToken ct)
{
using var ms = new MemoryStream();
WebSocketReceiveResult result;
var buffer = new byte[1024 * 4]; // 4KB буфер
do
{
result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), ct);
ms.Write(buffer, 0, result.Count);
} while (!result.EndOfMessage);
return ms.ToArray();
}
Это критично для передачи больших JSON-объектов или бинарных данных (изображения, файлы).
Масштабирование: SignalR vs Raw WebSocket
Для сложных приложений встает вопрос выбора между низкоуровневым WebSocket и SignalR — библиотекой от Microsoft, абстрагирующей WebSocket. Рассмотрим сценарии.
Когда использовать Raw WebSocket
- Максимальная производительность и минимальный overhead.
- Собственный протокол передачи (бинарные данные, кастомные фреймы).
- Интеграция с не-.NET клиентами (например, Go или Rust).
- Полный контроль над управлением соединениями.
Когда выбирать SignalR
- Автоматическое масштабирование через Redis Backplane или Azure SignalR Service.
- Поддержка fallback-транспортов (Long Polling, Server-Sent Events).
- Встроенные механизмы групп, Hub-методов и повторного подключения.
- Быстрая разработка типовых real-time функций (чат, лента уведомлений).
Рекомендация: Для продвинутых приложений часто используют гибридный подход — SignalR для клиент-серверного общения и raw WebSocket для внутренних микросервисов.
Обработка ошибок и отказоустойчивость
WebSocket-соединения нестабильны по своей природе: клиенты отключаются, сеть падает, сервер перегружается. Продвинутое приложение должно корректно обрабатывать эти сценарии.
Механизм Heartbeat (Ping/Pong)
Для обнаружения "мертвых" соединений реализуйте периодическую проверку:
public async Task HeartbeatCheck(WebSocket socket, string connectionId, CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(30), ct);
if (socket.State != WebSocketState.Open) break;
try
{
var pingBuffer = Encoding.UTF8.GetBytes("ping");
await socket.SendAsync(new ArraySegment<byte>(pingBuffer),
WebSocketMessageType.Text, true, ct);
}
catch (WebSocketException)
{
// Соединение разорвано – удаляем
await RemoveConnection(connectionId);
break;
}
}
}
Graceful Shutdown и повторное подключение
При остановке сервера отправляйте всем клиентам сообщение о закрытии с кодом ServiceRestart. На стороне клиента реализуйте экспоненциальную задержку перед повторным подключением:
// Клиентский код (пример на C#)
int retryDelay = 1000; // 1 секунда
while (!cancelled)
{
try
{
await client.ConnectAsync(uri, ct);
retryDelay = 1000; // Сброс при успехе
await ReceiveLoop(client);
}
catch (Exception)
{
await Task.Delay(retryDelay, ct);
retryDelay = Math.Min(retryDelay * 2, 30000); // Max 30 секунд
}
}
Продвинутая маршрутизация и middleware
Для сложных приложений (например, мульти-комнатный чат) используйте паттерн Middleware, знакомый по ASP.NET Core. Реализуйте цепочку обработчиков для WebSocket:
public class WebSocketRouter
{
private readonly List<Func<WebSocketContext, Func<Task>, Task>> _middlewares = new();
public void Use(Func<WebSocketContext, Func<Task>, Task> middleware) => _middlewares.Add