WebSocket на C#: real-time приложения продвинутого уровня

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему 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                

Блоги

Рекомендаций по книгам