SignalR - реализация реального времени в .NET
SignalR
SignalR — это библиотека, разработанная компанией Microsoft как часть платформы ASP.NET Core. Она предоставляет высокоуровневую абстракцию над транспортными механизмами реального времени и позволяет создавать приложения с двусторонней связью между клиентом и сервером. Основное назначение SignalR — упрощение реализации взаимодействия в режиме реального времени без необходимости глубокого погружения в низкоуровневые протоколы или сложные архитектурные решения.
Практически SignalR чаще всего используют в трёх сценариях:
- онлайн-обновления интерфейса (чаты, уведомления, статусы);
- синхронизация действий нескольких пользователей;
- доставка событий из фоновых сервисов в веб-клиент.
Если вам нужен общий контекст по веб-стеку, сначала пройдите ASP.NET — веб-платформа Microsoft, затем вернитесь к этой статье как к прикладному углублению. Соседние темы: практикум WebSocket, интеграции в реальном времени.
Быстрый старт
Минимальный сервер: хаб, регистрация и маршрут.
// Hubs/ChatHub.cs
public class ChatHub : Hub
{
public Task SendMessage(string user, string message) =>
Clients.All.SendAsync("ReceiveMessage", user, message);
}
// Program.cs
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<ChatHub>("/chathub");
app.Run();
Клиент в браузере (@microsoft/signalr или CDN):
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chathub")
.withAutomaticReconnect()
.build();
connection.on("ReceiveMessage", (user, message) => {
console.log(`${user}: ${message}`);
});
await connection.start();
await connection.invoke("SendMessage", "alice", "Привет");
Порядок на клиенте: зарегистрировать обработчики connection.on, затем start(), затем invoke. Имена методов на клиенте и сервере должны совпадать (регистр важен для JavaScript).
Модель взаимодействия и архитектурные основы
В основе SignalR лежит модель Publisher–Subscriber, которая является одной из ключевых парадигм реактивного программирования. Сервер выступает в роли издателя, способного отправлять сообщения всем подписанным клиентам или их подмножеству. Клиенты, в свою очередь, могут не только получать данные, но и инициировать события на сервере, что делает связь полностью двунаправленной.
Такой подход кардинально отличается от классической модели HTTP-запросов, где клиент всегда инициирует взаимодействие, а сервер лишь отвечает. В случае с SignalR сервер может инициировать передачу данных в любой момент, не дожидаясь запроса. Это открывает возможности для создания интерактивных систем, таких как чаты, игровые приложения, панели мониторинга, системы голосования, торговые площадки и картографические сервисы.
SignalR инкапсулирует сложность выбора и управления транспортным протоколом. Он автоматически определяет наиболее подходящий механизм связи на основе возможностей клиента и сервера. Поддерживаемые транспорты включают:
- WebSocket — современный протокол, обеспечивающий полнодуплексную связь поверх TCP. Он обеспечивает минимальную задержку и максимальную эффективность.
- Server-Sent Events (SSE) — односторонний поток данных от сервера к клиенту через HTTP. Подходит для сценариев, где клиенту не требуется часто отправлять данные.
- Long Polling — эмуляция постоянного соединения через повторяющиеся HTTP-запросы. Используется как fallback-механизм, когда другие протоколы недоступны.
SignalR выбирает WebSocket в первую очередь, если он поддерживается обеими сторонами. При его отсутствии применяется SSE, а затем — long polling. Этот процесс происходит прозрачно для разработчика, что позволяет сосредоточиться на бизнес-логике, а не на деталях сетевого взаимодействия.
| Критерий | SignalR | Чистый WebSocket в ASP.NET Core | SSE / Long Polling вручную |
|---|---|---|---|
| Двусторонние вызовы по имени метода | Да, хаб + invoke | Свой протокол поверх фреймов | Обычно только сервер → клиент |
| Fallback транспорта | Автоматически | Нет | Настраиваете сами |
Группы, Clients.User | Встроено | Реализуете сами | Реализуете сами |
| Масштабирование кластера | Redis / Azure SignalR Service | Свой fan-out (Redis, Kafka…) | Аналогично |
| Когда выбирать | Чаты, дашборды, .NET-экосистема | Полный контроль протокола, не-.NET клиенты | Односторонний push, простые ленты |
Хаб как центральная абстракция
Основной строительный блок любого приложения на SignalR — это хаб (hub). Хаб представляет собой класс, унаследованный от базового класса Hub, предоставляемого фреймворком. Он служит точкой входа для клиентских вызовов и центром управления рассылкой сообщений.
Каждый публичный метод хаба становится доступным для вызова со стороны клиента. Когда клиент вызывает такой метод, выполнение происходит на сервере в контексте текущего подключения. Внутри метода хаба разработчик может обращаться к свойству Clients, которое предоставляет доступ к различным группам подключённых клиентов:
Clients.All— все подключённые клиенты.Clients.Caller— клиент, инициировавший вызов.Clients.Others— все клиенты, кроме инициатора.Clients.Group("groupName")— клиенты, принадлежащие определённой группе.Clients.User("userId")— клиенты, аутентифицированные под указанным идентификатором.
Эти возможности позволяют гибко управлять доставкой сообщений, организовывать чаты по комнатам, персонализировать уведомления и реализовывать сложные сценарии взаимодействия.
Конфигурация и интеграция в ASP.NET Core
Для использования SignalR в приложении ASP.NET Core необходимо зарегистрировать соответствующие сервисы в контейнере зависимостей. Это делается вызовом метода AddSignalR() в конфигурации сервисов. После этого хаб регистрируется в маршрутизации с помощью метода MapHub<THub>("/path"), который связывает URL-путь с конкретным типом хаба.
Такая интеграция позволяет SignalR работать в рамках стандартной экосистемы ASP.NET Core, включая middleware, зависимости, логирование и обработку ошибок. Хабы участвуют в жизненном цикле приложения и могут использовать внедрение зависимостей для получения сервисов, таких как репозитории, кэши или внешние API.
Клиентская сторона — универсальность и кроссплатформенность
SignalR поддерживает широкий спектр клиентских платформ. Наиболее распространённый вариант — JavaScript-клиент, работающий в современных браузерах. Он предоставляет простой API для установки соединения, вызова методов на сервере и регистрации обработчиков событий.
Помимо браузерного JavaScript, существуют официальные клиентские библиотеки для:
- .NET — включая WPF, Windows Forms, MAUI, консольные приложения и службы.
- Java — для Android-приложений и серверных решений на JVM.
- Node.js — для создания серверных компонентов, взаимодействующих с SignalR-сервером.
Экспериментальная поддержка также доступна для C++ и Swift, что расширяет возможности интеграции с мобильными и встраиваемыми системами.
Клиентская библиотека управляет состоянием соединения, автоматически восстанавливает его при обрыве и обеспечивает согласованность данных. Разработчик получает единый интерфейс для работы с SignalR независимо от платформы.
Аутентификация и авторизация
SignalR интегрируется с системой аутентификации ASP.NET Core. Это означает, что можно использовать любые механизмы, поддерживаемые фреймворком — cookie-based аутентификацию, JWT-токены, OpenID Connect и другие. Контекст подключения содержит информацию о текущем пользователе, включая его идентификатор и роли.
Это позволяет применять политики авторизации к методам хаба с помощью атрибутов [Authorize], точно так же, как в контроллерах MVC или страницах Razor. Можно ограничить доступ к определённым методам только для аутентифицированных пользователей или пользователей с конкретными ролями.
Кроме того, SignalR предоставляет события подключения и отключения клиента (OnConnectedAsync и OnDisconnectedAsync), которые можно использовать для логирования, очистки ресурсов или обновления состояния системы.
По умолчанию Clients.User опирается на Context.User.Identity.Name. Для JWT и кастомных claim задайте свой IUserIdProvider:
public class NameIdentifierUserIdProvider : IUserIdProvider
{
public string? GetUserId(HubConnectionContext connection) =>
connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
builder.Services.AddSingleton<IUserIdProvider, NameIdentifierUserIdProvider>();
Масштабирование и отказоустойчивость
Одно из важнейших преимуществ SignalR — возможность масштабирования на несколько серверов. По умолчанию каждое подключение существует только на одном сервере, что затрудняет рассылку сообщений всем клиентам в распределённой среде. Для решения этой задачи используется механизм backplane — внешнее хранилище, которое синхронизирует сообщения между всеми экземплярами приложения.
Поддерживаемые backplane-решения включают:
- SQL Server — использует Service Broker для передачи сообщений между серверами.
- Redis — высокопроизводительное in-memory хранилище, идеально подходящее для временных сообщений.
- Azure SignalR Service — управляемый облачный сервис, полностью абстрагирующий инфраструктурную сложность.
Выбор backplane зависит от требований к производительности, стоимости и инфраструктурных предпочтений. Все они обеспечивают прозрачную работу приложения при горизонтальном масштабировании.
Практические сценарии применения
SignalR находит применение в самых разных доменах:
- Чаты и мессенджеры — мгновенная доставка сообщений, индикаторы набора текста, онлайн-статусы.
- Совместное редактирование — синхронизация изменений в документах в реальном времени.
- Игровые приложения — передача состояния игры, позиций игроков, событий.
- Финансовые системы — обновление котировок, торговых сигналов, портфелей.
- Мониторинг и аналитика — отображение метрик, логов, событий безопасности.
- Образовательные платформы — интерактивные тесты, опросы, совместные доски.
В каждом из этих случаев SignalR устраняет необходимость в постоянных опросах сервера, снижает нагрузку на сеть и обеспечивает мгновенную реакцию на изменения.
SignalR как часть современной архитектуры приложений
SignalR не существует изолированно — он органично встраивается в современные архитектурные подходы, такие как микросервисы, event-driven systems и реактивные приложения. Его использование особенно уместно в тех случаях, когда система должна реагировать на события немедленно, без задержек, связанных с опросом или периодической синхронизацией.
В контексте микросервисной архитектуры SignalR может выступать как отдельный сервис реального времени, который получает события от других микросервисов через шину сообщений (например, RabbitMQ, Kafka или Azure Service Bus) и транслирует их подключённым клиентам. Такой подход позволяет отделить логику обработки данных от логики доставки уведомлений, соблюдая принцип единственной ответственности.
В монолитных приложениях SignalR часто используется напрямую внутри основного веб-процесса, что упрощает разработку и отладку. Однако при росте нагрузки такая модель требует перехода к выделенному сервису или использованию облачного решения, такого как Azure SignalR Service, которое полностью берёт на себя управление соединениями и масштабированием.
Группы и пользовательские подключения
Одной из ключевых возможностей SignalR является работа с группами подключений. Группа — это динамический набор клиентских соединений, которым можно отправлять сообщения одновременно. Группы создаются и управляются программно: разработчик вызывает метод AddToGroupAsync для добавления текущего подключения в группу и RemoveFromGroupAsync для удаления.
Группы не сохраняются между перезапусками сервера. Это означает, что если приложение завершится, все группы будут потеряны, и клиенты должны повторно присоединиться к ним после восстановления соединения. Такое поведение упрощает управление состоянием и делает систему более отказоустойчивой, но требует от клиента реализации логики повторного подписания.
Помимо групп, SignalR поддерживает адресацию по идентификатору пользователя. Если клиент прошёл аутентификацию, его подключения автоматически ассоциируются с ClaimsPrincipal.Identity.Name (или другим идентификатором, если используется кастомная схема). Это позволяет отправлять сообщения конкретному пользователю, даже если он подключён с нескольких устройств одновременно:
await Clients.User("alice@example.com").SendAsync("NewMessage", content);
Такой механизм особенно полезен в персонализированных системах — уведомления, оповещения о платежах, обновления статуса заказа и т.п.
Жизненный цикл подключения и обработка событий
Каждое клиентское подключение проходит через чётко определённый жизненный цикл, управляемый хабом:
- Подключение — вызывается метод
OnConnectedAsync. Здесь можно выполнить инициализацию — загрузить данные пользователя, добавить его в нужные группы, зафиксировать факт входа в систему. - Активное взаимодействие — клиент вызывает методы хаба, сервер отправляет сообщения.
- Отключение — вызывается метод
OnDisconnectedAsync. Причина отключения передаётся в параметре (Exception), что позволяет различать плановое завершение соединения и аварийный разрыв.
Эти точки расширения позволяют реализовать сложную логику: от онлайн-статусов до распределённых блокировок ресурсов. Например, при входе пользователя в чат можно увеличить счётчик онлайн-участников, а при выходе — уменьшить и уведомить остальных.
Обработка ошибок и устойчивость к сбоям
SignalR предоставляет механизмы для обработки исключений как на стороне сервера, так и на клиенте. На сервере любое необработанное исключение в методе хаба приведёт к завершению вызова, но не к падению всего приложения. Для централизованной обработки ошибок можно переопределить метод OnDisconnectedAsync или использовать middleware ASP.NET Core.
На клиентской стороне JavaScript-библиотека SignalR автоматически пытается восстановить соединение при потере связи. Разработчик может подписаться на события:
onreconnecting— начало попытки переподключения.onreconnected— успешное восстановление соединения.onclose— окончательное закрытие соединения.
Это позволяет реализовать UX-паттерны — показывать индикатор временной недоступности, кэшировать сообщения для отправки после восстановления, предлагать пользователю обновить страницу.
Производительность и ограничения
SignalR оптимизирован для высокой частоты сообщений. В тестовых сценариях один сервер способен обрабатывать десятки тысяч одновременных подключений при умеренной нагрузке. Однако производительность зависит от множества факторов:
- Тип транспорта (WebSocket значительно эффективнее long polling).
- Размер и частота сообщений.
- Сложность логики в методах хаба.
- Наличие backplane (Redis обычно быстрее SQL Server).
Для высоконагруженных систем рекомендуется:
- Минимизировать объём передаваемых данных — пакет
Microsoft.AspNetCore.SignalR.Protocols.MessagePackи.AddMessagePackProtocol()на сервере и клиенте. - Избегать синхронных операций в методах хаба.
- Использовать пул потоков и асинхронные вызовы.
- Выносить тяжёлые вычисления за пределы хаба.
SignalR не предназначен для передачи больших файлов или потокового видео. Его сила — в передаче структурированных событий малого размера с минимальной задержкой.
Интеграция с другими технологиями .NET
SignalR тесно интегрирован с экосистемой .NET:
- Entity Framework Core — можно отправлять уведомления при изменении данных в базе.
- MediatR — использовать SignalR как обработчик событий в паттерне "публикация-подписка" на уровне приложения.
- Background Services — фоновые службы могут получать доступ к контексту хаба через
IHubContext<T>и инициировать рассылку без входящего запроса от клиента. - Blazor Server — использует SignalR как транспорт для двусторонней связи между браузером и сервером.
Пример получения IHubContext в фоновой службе:
Код ITЗагрузка примера кода…
Такой подход позволяет создавать гибридные системы, где бизнес-логика отделена от канала доставки.
Транспортные механизмы — внутренняя архитектура и выбор протокола
SignalR не является самостоятельным сетевым протоколом. Он представляет собой абстракцию над существующими механизмами передачи данных в реальном времени. Эта абстракция позволяет разработчику писать код один раз, не задумываясь о том, как именно данные будут доставлены до клиента. Выбор конкретного транспорта происходит автоматически на этапе установления соединения.
Процесс согласования (negotiation) начинается с того, что клиент отправляет HTTP-запрос к специальному эндпоинту /negotiate. Сервер отвечает JSON-объектом, содержащим информацию о поддерживаемых транспортах и, в случае использования Azure SignalR Service, временный URL для подключения. Клиент анализирует этот ответ и выбирает наиболее предпочтительный транспорт из доступных.
Порядок предпочтения фиксирован и оптимизирован для производительности:
- WebSocket — это идеальный транспорт для SignalR. Он обеспечивает постоянное, полнодуплексное соединение поверх одного TCP-соединения. Данные передаются в виде фреймов без накладных расходов HTTP-заголовков. WebSocket требует поддержки как на стороне сервера (ASP.NET Core полностью его поддерживает), так и на стороне клиента (все современные браузеры, .NET-клиенты, Node.js). Если обе стороны поддерживают WebSocket, он будет выбран.
- Server-Sent Events (SSE) — это односторонний поток данных от сервера к клиенту через обычное HTTP-соединение. Клиент открывает соединение и держит его открытым, а сервер по мере необходимости отправляет данные в формате
text/event-stream. SSE проще в реализации, чем WebSocket, и хорошо работает через большинство прокси и балансировщиков нагрузки. Однако он не позволяет клиенту отправлять данные серверу через тот же канал — для этого используются обычные HTTP-запросы. SSE поддерживается всеми основными браузерами, кроме Internet Explorer. - Long Polling — это самый старый и универсальный метод. Клиент отправляет HTTP-запрос к серверу и не получает немедленного ответа. Сервер удерживает запрос открытым, пока не появится сообщение для отправки или не истечёт таймаут. После получения ответа клиент немедленно отправляет новый запрос. Этот цикл создаёт иллюзию постоянного соединения. Long Polling работает везде, где есть HTTP, но имеет высокие накладные расходы (новые заголовки для каждого запроса) и большую задержку по сравнению с другими методами.
Такой многоуровневый подход гарантирует, что приложение на SignalR будет работать в максимально широком спектре сетевых условий, от современных корпоративных сетей до ограничений мобильных операторов.
За обратным прокси и балансировщиком
В продакшене хаб почти всегда стоит за Nginx, Traefik, IIS ARR или Ingress в Kubernetes. Для WebSocket прокси должен пробрасывать upgrade-заголовки и не обрывать долгие соединения слишком коротким proxy_read_timeout.
Пример фрагмента для Nginx:
Код ITЗагрузка примера кода…
На уровне Kestrel включите WebSockets: app.UseWebSockets() до MapHub. Sticky sessions на балансировщике помогают только без backplane; при Redis или Azure SignalR Service привязка к узлу не обязательна.
JWT: браузерный клиент передаёт токен через accessTokenFactory; за прокси убедитесь, что query access_token или заголовок Authorization не отрезаются правилами WAF.
Потоковая передача (streaming)
Помимо разовых SendAsync, хаб может отдавать поток с сервера (IAsyncEnumerable<T>) или принимать поток от клиента — удобно для прогресса, логов, чанков файла.
// Сервер → клиент
public async IAsyncEnumerable<int> StreamCounter(
int count,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
for (var i = 0; i < count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(200, cancellationToken);
yield return i;
}
}
JavaScript-клиент: connection.stream("StreamCounter", 10) и подписка на элементы. Для .NET-клиента — connection.StreamAsync<int>("StreamCounter", 10).
Безопасность и защита соединений
Безопасность является критически важным аспектом любого приложения реального времени. SignalR наследует все механизмы безопасности ASP.NET Core, что обеспечивает единый и надёжный подход к защите.
Аутентификация
Аутентификация в SignalR происходит на этапе установления соединения. Для JavaScript-клиентов в браузере это обычно означает использование cookie, установленных при входе в систему. Поскольку SignalR-соединение использует те же домен и порт, что и основное веб-приложение, браузер автоматически отправляет аутентификационные cookie вместе с запросом на согласование.
Для других клиентов (например, .NET или Java) чаще используется токен-based аутентификация. Клиент получает JWT-токен от сервера авторизации и передаёт его в качестве параметра строки запроса или в заголовке Authorization при вызове withUrl.
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chat", {
accessTokenFactory: () => {
return getAccessToken(); // функция, возвращающая токен
}
})
.build();
Сервер проверяет токен с помощью стандартного middleware JWT Bearer, и если он действителен, контекст соединения заполняется информацией о пользователе.
Авторизация
После успешной аутентификации применяются правила авторизации. К методам хаба можно применять атрибут [Authorize], точно так же, как к контроллерам MVC. Можно указывать роли или политики:
[Authorize(Roles = "Admin")]
public async Task BroadcastAdminMessage(string message)
{
await Clients.All.SendAsync("AdminMessage", message);
}
Это гарантирует, что только пользователи с ролью Admin смогут вызвать этот метод.
Защита от атак
SignalR также предоставляет средства для защиты от распространённых атак:
- Cross-Site WebSocket Hijacking (CSWSH): поскольку WebSocket-соединение не отправляет Origin-заголовок, важно проверять источник запроса на этапе согласования. Это делается с помощью CORS-политик в ASP.NET Core.
- Отказ в обслуживании (DoS): можно ограничить количество одновременных подключений от одного IP-адреса с помощью middleware или инфраструктурных средств (например, Application Gateway в Azure).
- Подделка запросов: все вызовы методов хаба проходят через строгую типизацию и проверку параметров, что снижает риск инъекций.
Масштабирование — переход от одного сервера к кластеру
Приложение, работающее на одном сервере, может эффективно использовать встроенную память для хранения информации о подключениях. Однако при горизонтальном масштабировании возникает проблема: клиент A подключён к серверу 1, а клиент B — к серверу 2. Если сервер 1 захочет отправить сообщение всем клиентам, он не знает о существовании клиента B.
Для решения этой проблемы используется шина сообщений — backplane. Backplane — это внешнее хранилище, которое выступает в роли посредника между всеми экземплярами приложения. Когда любой сервер хочет отправить сообщение, он публикует его в backplane. Все остальные серверы подписываются на эту шину и, получив сообщение, пересылают его своим локальным клиентам.
Microsoft официально поддерживает три типа backplane для SignalR:
- Azure SignalR Service — это управляемый облачный сервис. Разработчик не управляет инфраструктурой, а просто добавляет одну строку кода
AddAzureSignalR(). Сервис берёт на себя всё — согласование, управление соединениями, масштабирование и отказоустойчивость. Это самый простой и надёжный способ для приложений, размещённых в Azure. - Redis — популярное in-memory хранилище, которое отлично подходит для передачи временных сообщений. Для его использования требуется настроить Redis-сервер и добавить пакет
Microsoft.AspNetCore.SignalR.StackExchangeRedis. SignalR использует Redis Pub/Sub для обмена сообщениями между серверами. - SQL Server — использует Service Broker SQL Server для передачи сообщений. Этот вариант подходит для организаций, уже имеющих развитую инфраструктуру на базе SQL Server, но он менее производителен, чем Redis.
Выбор backplane — это компромисс между простотой управления, производительностью и стоимостью. Для большинства новых проектов рекомендуется начинать с Azure SignalR Service или Redis.
Продвинутые сценарии — IHubContext и фоновые службы
Одна из самых мощных возможностей SignalR — это возможность инициировать рассылку сообщений не из хаба, а из любого места в приложении. Это достигается через интерфейс IHubContext<T>.
IHubContext<T> — это служба, которую можно внедрить через DI-контейнер. Она предоставляет тот же API, что и свойство Clients внутри хаба, но без привязки к конкретному клиентскому вызову.
Это открывает множество сценариев:
- Фоновые задачи — служба, выполняющаяся по расписанию, может уведомлять пользователей о завершении длительной операции.
- Обработка событий: при получении события из внешней системы (например, через очередь сообщений) можно мгновенно оповестить всех заинтересованных клиентов.
- Интеграция с бизнес-логикой: репозиторий или сервисный слой могут напрямую отправлять уведомления, не вовлекая контроллеры или хабы.
Пример использования в фоновой службе уже был приведён ранее. Важно понимать, что IHubContext — это "окно" в мир подключённых клиентов из любого уголка приложения.
SignalR в контексте слоистой архитектуры приложения
SignalR не должен становиться центром бизнес-логики. Его роль — транспортный слой для доставки событий. В правильно спроектированном приложении хаб выступает исключительно как адаптер между клиентским соединением и внутренними сервисами системы.
Типичная архитектура выглядит следующим образом:
- Клиент вызывает метод хаба, например,
SendMessage(message). - Хаб получает вызов, но не обрабатывает его самостоятельно. Он делегирует управление зарегистрированному сервису через внедрение зависимостей.
- Сервисный слой выполняет всю необходимую логику — валидацию, сохранение в базу данных, применение бизнес-правил. Подробнее о проверках входа — Проверка и валидация.
- После завершения операции сервис может опубликовать событие (например, через MediatR или простой делегат).
- Хаб или фоновая служба, подписавшись на это событие, использует
IHubContextдля отправки уведомления всем заинтересованным клиентам.
Такой подход обеспечивает чёткое разделение ответственности:
- Хаб отвечает только за коммуникацию.
- Сервисы отвечают за логику.
- Модели данных и репозитории остаются независимыми от деталей передачи сообщений.
Пример реализации:
Код ITЗагрузка примера кода…
Эта модель позволяет легко тестировать бизнес-логику без запуска SignalR-соединений и упрощает поддержку кода.
Группы — динамическая организация клиентов
Группы в SignalR — это временные, неустойчивые коллекции подключений. Они не имеют встроенной персистентности и не связаны с понятием "пользователь" или "роль". Группа существует только до тех пор, пока в ней есть хотя бы одно активное подключение.
Управление группами полностью лежит на разработчике. Типичный сценарий — добавление пользователя в группу при входе в чат-комнату:
public async Task JoinRoom(string roomName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
await Clients.Group(roomName).SendAsync("UserJoined", Context.UserIdentifier);
}
И удаление при выходе:
public async Task LeaveRoom(string roomName)
{
await Clients.Group(roomName).SendAsync("UserLeft", Context.UserIdentifier);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
}
Важно помнить — если клиент теряет соединение (например, из-за потери сети), он автоматически удаляется из всех групп. При восстановлении соединения он должен повторно присоединиться к нужным группам. Это требует от клиента реализации логики повторного подписания, обычно в обработчике события onreconnected.
Группы идеально подходят для:
- Чат-комнат
- Спортивных трансляций (группа по матчу)
- Аукционов (группа по лоту)
- Совместного редактирования документа
Они не подходят для долгосрочных подписок, таких как "получать все уведомления о заказах". Для таких случаев лучше использовать адресацию по UserId.
Обработка состояния подключения и жизненного цикла
Каждое подключение имеет уникальный идентификатор ConnectionId, который действителен только в течение одного сеанса. Этот идентификатор не следует путать с UserId — последний привязан к аутентифицированной личности и может быть связан с несколькими ConnectionId одновременно (например, пользователь зашёл с телефона и компьютера).
Хаб предоставляет два ключевых метода для управления жизненным циклом:
-
OnConnectedAsync()— вызывается сразу после установки соединения. Здесь можно:- Загрузить профиль пользователя
- Добавить его в глобальную группу "все пользователи"
- Отправить приветственное сообщение
- Зафиксировать факт входа в систему мониторинга
-
OnDisconnectedAsync(Exception exception)— вызывается при любом разрыве соединения. Параметрexceptionсодержит информацию о причине:null— клиент корректно закрыл соединение.- Объект исключения — произошла ошибка (таймаут, сетевой сбой и т.д.).
Эти методы позволяют поддерживать актуальное состояние онлайн-пользователей, управлять лицензиями (например, ограничение числа одновременных сессий) и выполнять очистку ресурсов.
Интеграция с фоновыми службами и внешними системами
Одна из самых сильных сторон SignalR — возможность интеграции с системами, которые не инициируют взаимодействие с пользователем. Например, служба обработки платежей, работающая в фоне, может уведомить веб-интерфейс о успешном завершении транзакции.
Для этого используется IHubContext<T>, внедряемый в фоновую службу:
Код ITЗагрузка примера кода…
Такой подход превращает веб-приложение в реактивную систему, способную мгновенно реагировать на события из любого источника — очередей, баз данных, внешних API.
SignalR и реактивные паттерны — от событий к потокам
SignalR естественным образом ложится в парадигму реактивного программирования. Каждое входящее сообщение от клиента — это событие, на которое система может реагировать. Каждое исходящее сообщение — это реакция на внутреннее или внешнее изменение состояния.
В современных .NET-приложениях эту модель можно усилить с помощью библиотеки System.Reactive (Rx.NET). Хотя SignalR сам по себе не является реактивной библиотекой, его можно легко интегрировать с Rx-паттернами для создания сложных цепочек обработки событий.
Например, можно представить входящие сообщения от клиента как IObservable<T>, применить к ним операторы фильтрации, дебаунсинга, объединения с другими потоками, а затем отправить результат обратно через IHubContext. Это особенно полезно в сценариях высокой частоты событий, таких как совместное редактирование текста или отслеживание перемещений курсора.
Такой подход позволяет отделить:
- Источник событий (хаб, очередь, таймер)
- Логику преобразования (реактивные операторы)
- Потребителя (рассылка через SignalR)
Это повышает тестируемость и читаемость кода, особенно в сложных доменах.
Типизация и контракты — безопасность на уровне компиляции
Одна из сильных сторон SignalR в экосистеме .NET — это строгая типизация. Методы хаба определяются как обычные C#-методы с чёткими сигнатурами. Клиентские вызовы проверяются на соответствие этим сигнатурам.
Для JavaScript-клиента эта безопасность обеспечивается на уровне соглашений и документации. Однако для .NET-клиентов (включая Blazor и MAUI) можно использовать строго типизированные хабы.
Строго типизированный хаб определяется через интерфейс:
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
}
public class ChatHub : Hub<IChatClient>
{
public async Task SendMessage(string message)
{
await Clients.All.ReceiveMessage(Context.UserIdentifier, message);
}
}
Теперь свойство Clients.All имеет тип IChatClient, и любой вызов метода, не определённого в интерфейсе, приведёт к ошибке компиляции. Это предотвращает опечатки в именах методов и гарантирует согласованность между сервером и клиентом.
На стороне .NET-клиента вызовы серверных методов идут через InvokeAsync (имена совпадают с методами хаба):
await using var connection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/chathub")
.WithAutomaticReconnect()
.Build();
connection.On<string, string>("ReceiveMessage", (user, message) => { /* UI */ });
await connection.StartAsync();
await connection.InvokeAsync("SendMessage", "alice", "Привет");
Такой подход с Hub<T> на сервере и согласованными именами методов снижает число опечаток в крупных проектах.
Отладка и диагностика — инструменты для разработчика
Разработка приложений реального времени сопряжена с уникальными вызовами — подключения могут обрываться, сообщения теряться, клиенты — восстанавливать соединение в неожиданный момент. SignalR предоставляет несколько уровней диагностики.
Логирование
SignalR интегрирован с системой логирования ASP.NET Core. Включение подробного логирования позволяет отслеживать:
- Этапы согласования (
Negotiate) - Установление соединения (
Connected) - Вызовы методов (
Invoking hub method) - Отправку сообщений (
Sending message) - Разрывы соединения (
Disconnected)
Для включения достаточно настроить уровень логирования в appsettings.json:
{
"Logging": {
"LogLevel": {
"Microsoft.AspNetCore.SignalR": "Debug",
"Microsoft.AspNetCore.Http.Connections": "Debug"
}
}
}
Инструменты разработчика в браузере
JavaScript-клиент SignalR использует стандартный console для вывода диагностических сообщений. При включении логирования в конструкторе можно увидеть все этапы жизненного цикла соединения прямо в DevTools:
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chat")
.configureLogging(signalR.LogLevel.Information)
.build();
Мониторинг в production
В рабочей среде важно отслеживать метрики:
- Количество активных подключений
- Частота сообщений
- Время отклика
- Ошибки подключения
Если используется Azure SignalR Service, все эти метрики доступны в портале Azure и могут быть интегрированы с Application Insights. Для self-hosted решений можно экспортировать метрики через IHubContext или использовать middleware для сбора статистики.
Практическое руководство — многопользовательский чат на SignalR
Ниже представлено пошаговое руководство по созданию полнофункционального чата с поддержкой комнат, истории сообщений, приватных сообщений и онлайн-статусов. Архитектура следует принципам разделения ответственности, масштабируемости и безопасности.
Шаг 1. Создание проекта и базовой структуры
Создайте новый проект ASP.NET Core:
dotnet new web -n ChatApp
cd ChatApp
Добавьте необходимые пакеты:
dotnet add package Microsoft.AspNetCore.SignalR
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.InMemory # для упрощённой демонстрации
Шаг 2. Модели данных
Определите основные сущности:
Код ITЗагрузка примера кода…
Шаг 3. Хранилище (упрощённое)
Для демонстрации используем in-memory хранилище. В production замените на EF Core + SQL Server.
Код ITЗагрузка примера кода…
Шаг 4. Хаб чата
Код ITЗагрузка примера кода…
Шаг 5. Конфигурация приложения
Код ITЗагрузка примера кода…
Шаг 6. Клиентская часть (JavaScript)
Создайте wwwroot/index.html с интерфейсом для комнат, истории, приватных сообщений и списка онлайн-пользователей. Реализация включает:
- Подключение к
/chathub - Вызов
JoinRoom("General")при старте - Обработка
ReceiveHistory,ReceiveMessage,ReceivePrivateMessage - Отображение
UserStatusChangedв списке пользователей - Форма отправки с выбором: публичное / приватное
Полный HTML/JS-код опущен для сохранения фокуса на архитектуре, но он напрямую следует из API хаба.
Интеграция SignalR с Blazor
Blazor предоставляет два режима: Server и WebAssembly. SignalR интегрируется с ними по-разному.
Blazor Server — SignalR как транспорт
Blazor Server уже использует SignalR для двусторонней связи между браузером и сервером. Каждый компонент работает на сервере, а UI-изменения передаются через SignalR-соединение.
Важно: встроенный канал Blazor Server (circuit) — это отдельное SignalR-соединение для UI. Для своего чата или уведомлений подключайте клиент HubConnection к отдельному хабу или пробрасывайте события через singleton-сервис, который хаб вызывает после SendAsync.
Код ITЗагрузка примера кода…
IHubContext<T> в Blazor Server используйте на сервере для отправки (из фоновой службы или API), а не для подписки в Razor-компоненте.
Blazor WebAssembly — клиентский SignalR
Blazor WebAssembly работает в браузере. Для взаимодействия с SignalR-сервером используется JavaScript-клиент или нативный .NET-клиент.
Вариант A — Использование HubConnection напрямую
Код ITЗагрузка примера кода…
Вариант B — Инкапсуляция в сервис
Создайте ChatService, внедрите его через DI, и используйте в компонентах. Это предпочтительный подход для сложных приложений.
См. также
| Тема | Материал |
|---|---|
| Обзор ASP.NET и кратко про SignalR | ASP.NET — веб-платформа |
| SignalR в справочнике C# / ASP.NET Core | C# — ASP.NET Core, раздел SignalR |
| Сырые WebSocket, JWT на handshake | Практикум REST и WebSocket |
| Real-time в микросервисах | Интеграции — WebSocket и SignalR |
| Вопросы на собеседование | ASP.NET Core — SignalR |
| Термин в глоссарии | SignalR |
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.