Gdy aplikacja ma pokazywać zmiany bez odświeżania strony, zwykłe żądania HTTP szybko zaczynają być niewygodne. Za krótkim zapisem signal r kryje się zwykle SignalR, biblioteka dla .NET, która pozwala przesyłać dane między serwerem i klientem niemal natychmiast. Pokażę, jak działa, kiedy warto jej użyć, jak zbudować prosty przykład w ASP.NET Core oraz na co uważać przy skalowaniu i utracie połączenia.
SignalR upraszcza komunikację czasu rzeczywistego w aplikacjach webowych
- SignalR umożliwia serwerowi wysyłanie danych do przeglądarki bez odświeżania strony.
- Najczęściej korzysta z WebSocketów, ale może przełączyć się na Server-Sent Events lub long polling.
- Centralnym elementem rozwiązania jest hub, czyli punkt komunikacji między klientami a serwerem.
- Biblioteka dobrze pasuje do czatów, powiadomień, dashboardów i monitoringu.
- Przy wielu instancjach aplikacji trzeba zaplanować sticky sessions, Redis backplane albo Azure SignalR Service.
Czym jest SignalR i kiedy naprawdę go potrzebujesz
SignalR to biblioteka ASP.NET Core do budowania funkcji czasu rzeczywistego. Najważniejsza różnica względem typowego API REST polega na tym, że komunikacja może być inicjowana nie tylko przez klienta. Serwer sam wysyła zdarzenie do przeglądarki, gdy zmieni się stan danych.
W praktyce oznacza to, że użytkownik nie musi odświeżać strony, aby zobaczyć nową wiadomość, zmianę statusu zamówienia albo postęp generowania raportu. Ja najczęściej widzę największą wartość SignalR tam, gdzie opóźnienie rzędu kilku sekund pogarsza doświadczenie użytkownika.
| Przypadek | Dlaczego SignalR pasuje |
|---|---|
| Czat i komentarze | Nowe wiadomości pojawiają się natychmiast u wybranych odbiorców. |
| Dashboard operacyjny | Metryki i alerty mogą być aktualizowane bez cyklicznego odpytywania API. |
| Status zamówienia | Klient otrzymuje informację o zmianie etapu obsługi. |
| Postęp zadania w tle | Serwer przekazuje procent wykonania długiego procesu. |
| Aplikacja współdzielona | Zmiana wykonana przez jednego użytkownika może być rozgłoszona pozostałym. |
Nie każda aplikacja potrzebuje stałego połączenia. Jeżeli dane zmieniają się raz na kilka minut, zwykłe żądanie HTTP będzie prostsze i tańsze w utrzymaniu. SignalR ma sens wtedy, gdy aktualność danych jest częścią funkcji, a nie tylko miłym dodatkiem.
Przeczytaj również: ng command not found? Jak naprawić Angular CLI
Jak SignalR wybiera sposób komunikacji
Biblioteka próbuje użyć WebSocketów, ponieważ zapewniają dwukierunkową komunikację przy niewielkim narzucie. Jeżeli środowisko ich nie obsługuje, może skorzystać z Server-Sent Events, czyli strumienia danych tylko z serwera do klienta, albo z long pollingu.
To jedna z praktycznych zalet tego rozwiązania. Nie muszę od razu rozwiązywać wszystkich problemów związanych z różnymi przeglądarkami, proxy i ograniczeniami sieci. Jednocześnie nie oznacza to, że infrastruktura może być dowolna. Reverse proxy musi poprawnie przepuszczać połączenia WebSocket, a limity czasu i liczby połączeń trzeba sprawdzić przed wdrożeniem.
Jak działa hub i przepływ wiadomości
Centralnym punktem SignalR jest hub. To klasa po stronie serwera, w której definiuje się metody wywoływane przez klienta oraz komunikaty wysyłane do klientów. Hub nie jest trwałym magazynem stanu. Traktuję go raczej jako bramę komunikacyjną, a dane biznesowe przechowuję w bazie lub osobnej usłudze.
using Microsoft.AspNetCore.SignalR;
public sealed class NotificationsHub : Hub
{
public Task SendMessage(string message)
{
return Clients.All.SendAsync("messageReceived", message);
}
}Wywołanie Clients.All wysyła komunikat do wszystkich aktywnych połączeń. W realnej aplikacji częściej używa się Clients.User, Clients.Group albo Clients.Caller. Dzięki temu można wysłać informację konkretnej osobie, grupie użytkowników lub tylko klientowi, który wykonał operację.
Grupy są szczególnie przydatne w aplikacjach wieloorganizacyjnych. Użytkownicy pracujący nad jednym projektem mogą trafić do grupy o nazwie identyfikatora projektu, a zmiana dokumentu zostanie przekazana wyłącznie właściwym osobom.
public async Task JoinProject(string projectId)
{
await Groups.AddToGroupAsync(
Context.ConnectionId,
$"project:{projectId}");
}Ważne ograniczenie polega na tym, że instancja huba jest tworzona dla obsługi wywołania. Nie należy więc zapisywać w jej polach informacji, które muszą przetrwać kolejne komunikaty. Stan połączenia i dane biznesowe to dwie różne rzeczy. Pomieszanie tych ról prowadzi do trudnych do odtworzenia błędów.
Minimalny przykład w ASP.NET Core
Po stronie serwera konfiguracja jest krótka. W aplikacji ASP.NET Core dodaję obsługę SignalR i mapuję konkretną ścieżkę huba.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub("/hubs/notifications");
app.Run(); Biblioteka serwerowa jest częścią współdzielonego frameworka ASP.NET Core. Dla klienta JavaScript trzeba dodać osobny pakiet @microsoft/signalr. W prostym projekcie można też skorzystać z gotowego bundlera, ale mechanizm pozostaje taki sam.
import {
HubConnectionBuilder,
LogLevel
} from "@microsoft/signalr";
const connection = new HubConnectionBuilder()
.withUrl("/hubs/notifications")
.withAutomaticReconnect()
.configureLogging(LogLevel.Information)
.build();
connection.on("messageReceived", message => {
console.log("Nowa wiadomość:", message);
});
async function startConnection() {
try {
await connection.start();
console.log("Połączenie aktywne");
} catch (error) {
console.error("Nie udało się połączyć", error);
}
}
startConnection();W tym przykładzie klient nasłuchuje zdarzenia o nazwie messageReceived. Nazwa musi być identyczna po obu stronach. To drobny szczegół, który często powoduje pozornie tajemniczy problem: połączenie działa, ale interfejs niczego nie odbiera.
withAutomaticReconnect() pomaga po chwilowej utracie sieci. Domyślna sekwencja prób obejmuje zwykle opóźnienia 0, 2, 10 i 30 sekund. Nie rozwiązuje jednak problemu pierwszego uruchomienia połączenia. Błąd podczas początkowego start() trzeba obsłużyć samodzielnie, na przykład przez ponowienie próby z własnym opóźnieniem.
Po stronie klienta można również wywołać metodę huba:
await connection.invoke("SendMessage", "Raport jest gotowy");W aplikacji produkcyjnej nie pozwalam, aby dowolny klient bez kontroli wywoływał operacje biznesowe. Metoda huba powinna sprawdzać uprawnienia, poprawność danych i kontekst użytkownika, podobnie jak zwykły endpoint API.
SignalR, WebSockety, SSE czy polling
SignalR nie jest konkurencją dla WebSocketów w sensie technologicznym. Najczęściej używa WebSocketów jako transportu, ale dodaje warstwę abstrakcji, serializację wiadomości, obsługę hubów, klientów i ponownego łączenia. Wybór zależy więc od tego, czy potrzebuję gotowego modelu komunikacji, czy pełnej kontroli nad protokołem.
| Rozwiązanie | Najlepsze zastosowanie | Ograniczenie |
|---|---|---|
| SignalR | Aplikacje .NET z komunikacją zdarzeniową i wieloma typami klientów | Dodatkowa warstwa oraz konieczność zarządzania trwałymi połączeniami |
| Surowe WebSockety | Własny protokół, bardzo niskie opóźnienia, gry i specjalistyczne strumienie | Więcej kodu do napisania i utrzymania |
| Server-Sent Events | Jednokierunkowe aktualizacje z serwera do przeglądarki | Klient nie wysyła danych tym samym kanałem |
| Polling | Proste aplikacje z rzadko zmieniającymi się danymi | Opóźnienie, zbędne żądania i większe zużycie zasobów |
Moja praktyczna reguła jest prosta. Jeżeli aplikacja działa w ekosystemie ASP.NET Core i potrzebuje kilku rodzajów powiadomień, zaczynam od SignalR. Jeżeli buduję transport dla strumienia audio, gry czasu rzeczywistego albo urządzenia z bardzo konkretnym protokołem, rozważam WebSockety bez dodatkowej abstrakcji.
Nie używałbym SignalR tylko dlatego, że brzmi nowocześnie. Stałe połączenie zużywa pamięć i połączenia TCP nawet wtedy, gdy użytkownik chwilowo nic nie robi. Przy małej liczbie zdarzeń zwykłe API z odświeżaniem co kilka minut może być rozsądniejszym wyborem.
Skalowanie, bezpieczeństwo i utrata połączenia
Na jednej instancji aplikacji wdrożenie jest proste. Problem pojawia się wtedy, gdy ruch trafia do kilku serwerów. Serwer A może znać połączenie klienta, a serwer B nie będzie wiedział, że powinien dostarczyć mu komunikat.
W takim środowisku trzeba zaplanować sticky sessions, czyli kierowanie kolejnych żądań tego samego połączenia do jednej instancji, albo użyć mechanizmu rozsyłającego komunikaty między serwerami. Własna infrastruktura często korzysta z Redis backplane. W Azure wygodnym rozwiązaniem jest Azure SignalR Service, który przejmuje obsługę połączeń i ułatwia skalowanie.
Przed wdrożeniem sprawdzam przede wszystkim cztery rzeczy:
- Autoryzację huba, szczególnie przy komunikatach kierowanych do użytkowników lub grup.
- CORS i HTTPS, jeśli frontend oraz backend działają pod różnymi adresami.
- Konfigurację proxy, w tym obsługę WebSocketów i limity czasu.
- Rozmiar wiadomości, aby nie przesyłać całych obiektów, gdy wystarczy identyfikator i kilka pól.
Automatyczne ponowne połączenie nie gwarantuje dostarczenia każdej wiadomości. Jeżeli klient był offline przez dłuższy czas, może przegapić zdarzenia. Dlatego po ponownym połączeniu często wykonuję synchronizację stanu przez zwykłe API. SignalR powinien informować o zmianach, ale nie musi być jedynym źródłem prawdy.
W nowszych wersjach .NET można włączyć stateful reconnect, czyli mechanizm buforowania wiadomości podczas krótkiej przerwy w połączeniu. Pomaga przy chwilowym przełączeniu sieci Wi-Fi na komórkową, ale nie zastępuje trwałego magazynu zdarzeń ani pełnej synchronizacji po długiej awarii.
Osobną kwestią jest obserwowalność. Loguję moment połączenia, rozłączenia, ponownego łączenia oraz liczbę aktywnych klientów. Bez tych danych trudno odróżnić błąd huba od problemu z proxy, limitem połączeń albo konfiguracją load balancera.
Dobry moment na wdrożenie SignalR w projekcie
SignalR jest dobrym wyborem, gdy aplikacja ma reagować na zdarzenia niemal natychmiast, a backend powstaje w ASP.NET Core. Największą korzyść daje nie sam fakt użycia WebSocketów, lecz gotowy model hubów, grup, klientów i komunikacji w obie strony.
Na start polecam zbudować jeden mały scenariusz, na przykład powiadomienie o zakończeniu zadania. Dopiero gdy wiadomo, jakie zdarzenia są potrzebne, warto dodawać grupy, autoryzację, automatyczne ponowne łączenie i obsługę wielu instancji.
Najczęstszy błąd polega na traktowaniu połączenia czasu rzeczywistego jak niezawodnej kolejki komunikatów. Gdy rozdzielimy powiadamianie od trwałego stanu aplikacji, rozwiązanie pozostaje prostsze, łatwiejsze do skalowania i znacznie odporniejsze na problemy sieciowe.
