Ćwiczenia - Konfiguracja i wykorzystanie SignalR

Ćwiczenie 7

W tym ćwiczeniu ponownie wykorzystamy projekt używany wcześniej.

Doinstalowanie paczki nuget

Skorzystanie z możliwości biblioteki SignalR musisz doinstalować paczkę nugeta. Najprościej to zrobić za pomocą dotnet CLI. Wystarczy, że w konsoli wpiszesz następującą komendę:

$ dotnet add package Microsoft.AspNetCore.SignalR

Możliwe, że w nowszych wersjach templat-u MVC ta paczka jest już częścią składową jednej wielkiej meta-paczki o nazwie Microsoft.AspNetCore.All.

Dodanie Hub-a

Architektura SignalR opiera się na konspekcie hub-a, klasy która stanie się centralnym punktem zarządzania połączeniami RTC w naszej aplikacji. Tak więc zacznij od dodania nowego katalogu, nazwij go Hubs. Następnie stwórz tam klasę. W pierwszej wersji implementacji wystarczy, że to będzie klasa dziedzicząca po klasie Hub znajdującej się w namespace Microsoft.AspNetCore.SignalR. Klasa ta nie musi nawet zawierać metod.

public class ApiHub : Hub
{
}

Konfiguracja SignalR

W następnym kroku przejdź do pliku Startup.cs i metody ConfigureServices, aby skonfigurować bibliotekę SignalR. Jak pewnie już wiesz SignalR udostępnia extensionMethod, który pozwala w prosty sposób na zarejestrowanie tej biblioteki w aplikacji.

services.AddSignalR(hubOptions =>
{
    hubOptions.EnableDetailedErrors = true;
});

W tym przypadku ExtensionMethod przyjmuje wyrażenie lambda, gdzie jako argument dostaniesz obiekt konfiguracyjny. Na tym obiekcie możesz ustawić naprawdę sporo opcji, takich jak czas bezczynności, po którym połączenie zostaje zerwane etc. Jednak, na tych warsztatach, polecam dodać tylko jedną opcję konfiguracji, mianowicie pokazywanie błędów ze wszystkimi informacjami. Może to pomóc Ci zdebugowac, co się stało, jeżeli cos nie zadziała zgodnie z tutorialem.

Następnie, zgodnie z konwencją, przejdź do metody Configure w tym samym pliku i za pomocą extension method dodaj middleware SignalR do pipeline obsługi zadań.

app.UseSignalR(routes => { routes.MapHub<ApiHub>("/hub"); });

SignlaR, podobnie jak MVC, ma swoją tablicę routingu. Nie zapomnij zarejestrowac adresu dla wcześniej stworzonego Hub-a. Jeżeli obecnie nie chcesz, aby połączenie przez webSocket było uwierzytelnione, dodaj ten middleware nad app.UseAuthentication. Natomiast, jeżeli chcesz od razu mieć uwierzytelnione połączenie daj go pod. Oczywiście, żeby uwierzytelnić połączenie, trzeba będzie wykonać kawałek pracy, który zostanie opisany w dalszej części tego ćwiczenia.

Dodanie klienta JavaScript

Musisz na chwilę wyjść ze świata .NET-a i przejść do Frontend-u. Jeżeli zgodnie z tutorialem wygenerowałeś aplikacje MVC, to możesz wykorzystać widok znajdujący się w katalogu Views/Index. Jeżeli nie, to niestety musisz sobie wygenerować kontroler, który będzie serwował widok Razora.

Zacznij od załadowania oficjalnego klienta SinglaR

<script src="https://cdn.jsdelivr.net/npm/@@aspnet/signalr@1.1.2/dist/browser/signalr.min.js"></script>

Następnie, w kolejny tagu script , skonfiguruj klienta SignalR, aby połączyć go z hubem wystawiony pod adresem /hub

var setupConnection = () => {
    var connection = new signalR.HubConnectionBuilder()
        .withUrl("/hub")
        .build();
    connection.on('ReceiveMessage', res => console.log(JSON.parse(res)));
    connection.on("finished", connection.stop);
    connection.start()
        .catch(err => console.error(err.toString()));
}

setupConnection();

Konfiguracja powyższa spowoduje, że wszystkie wiadomości o typie ReceiveMessage zostaną sparsowane do obiektu i wyświetlone na konsoli developerskiej.

Spróbuj teraz uruchomić projekt i zobacz co się wydarzy. Jeżeli dodałeś middleware SignalR-a poniżej app.UseAuthentication to powinieneś mieć teraz czerwono w konsoli. A negocjacja połączenia powinna zwrócić błąd 401 Forbidden.

Uwierzytelnienie połączenia SignalR

Klient

Uwierzytelnianie połączenia po stornie klienta wymaga przekazania mu JWT tokena podczas budowania obiektu connection. Do wywołania metody withUrl przekazujesz obiekt konfiguracyjny.

var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxYzE3ZDY1MC0xNWE5LTRhNjEtODZjYi1mYTRkOGUxMWRhZTgiLCJ1bmlxdWVfbmFtZSI6IjFjMTdkNjUwLTE1YTktNGE2MS04NmNiLWZhNGQ4ZTExZGFlOCIsImp0aSI6ImUwZDBmY2M5LTQ0MzktNDBhYS05M2Q1LWM5YjE2MGM0ODIwNiIsImlhdCI6IjE1NTAzNjA4OTg1NDUiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJhZG1pbiIsIm5iZiI6MTU1MDM2MDg5OCwiZXhwIjoxNTUwMzY4MDk4LCJpc3MiOiJXZWJBcGkifQ.rJYIw1LGqMIoesUgBcLGrXZ4JGYoGu5obIWg_FOz5W4";
var setupConnection = () => {
    var connection = new signalR.HubConnectionBuilder()
        .withUrl("/hub",
            {
                accessTokenFactory: () => token
            })
        .build();
    // ....    
}
// ...

Wtedy SignalR automatycznie zacznie przesyłać token w zapytaniu po negocjację połączenia. Token będzie się znajdować w QueryString-u pod kluczem acces_token.

Serwer

Aby dodać uwierzytelnienie połączenia po stronie serwera, pierwsze co musisz zrobić to dodać atrybut [Authorize] do wcześniej stworzonego Hub-a. Przejdź do pliku Startup.cs i w metodzie ConfigureServices odnajdź konfiguracje sprawdzania JwtBearerToken. Wewnątrz metody znajdującej się w .AddJwtBearer musisz zapisać się na event OnMessageReceived, gdzie dla próby negocjacji połączenia webSocket musisz przepisać token znajdujący się w QueryString-u do instancji obiektu MessageRecivedContext.

.AddJwtBearer(cfg =>
{
    // ...    
    cfg.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            var accessToken = context.Request.Query["access_token"];

            // If the request is for our hub...
            var path = context.HttpContext.Request.Path;
            if (!string.IsNullOrEmpty(accessToken) &&
                (path.StartsWithSegments("/hub")))
            {
                // Read the token out of the query string
                context.Token = accessToken;
            }

            return Task.CompletedTask;
        }
    };
});

Inaczej middleware sprawdzający JWT tokeny nie będzie wiedział skąd ma go otrzymać.

Wysyłanie wiadomości z poziomu serwera

Teraz, kiedy masz uwierzytelnione połączenie, zajmij się wysyłaniem wiadomości z poziomu serwera. Stwórz nowy kontroler. Nazwij go jak chcesz. Następnie, za pomocą konstruktora wstrzyknij do niego obiekt IHubContext<ApiHub>. Potem stwórz oddzielny endpoint, gdzie wykorzystasz ten context. Wystarczy, że na tym kontekście wywołasz metodę _context.Clients.All.SendAsync. Pamiętaj, że jest to metoda asynchronicza. Jako przykładu wykorzystania możesz użyć kodu poniżej

await _hub.Clients.All.SendAsync("ReceiveMessage", JsonConvert.SerializeObject(new { type = "Message", content = content }));

Jeśli wykonasz wszystko poprawnie, to podczas wysyłania, za pomocą postmana, uderzysz w ten endpoint, w drugiej zakładce, automatycznie, w konsoli powinna pojawić się wiadomość.

Wysyłanie wiadomości z poziomu klienta

Wysłanie wiadomości z poziomu klienta wymaga zdefiniowania jej obsługi na poziomie Hub-a (W końcu ta klasa przestanie być pusta). Dodaj tam metodę SendMessage, która zadziała jako broadcast i roześle przekazane wiadomości do wszystkich połączonych klientów.

public async Task SendMessage(string message)
{
    await Clients.All.SendAsync("ReceiveMessage", JsonConvert.SerializeObject(new { message }));
}

Następnie, z poziomu widoku razor-a, dodaj formularz, który pozwoli na wysłanie wiadomości:

<div class="text-center" id="send">
    <h2>Send Message:</h2>
    <input type="text"/>
    <button>Send</button>
</div>

Zapisz się na event click na obiekcie button. Jako callback dodaj wywołanie pobrania wartości inputa i wysłanie wiadomości przez obiekt connetion

var input = document.querySelector("#send");

input.querySelector("button").addEventListener("click", evt => {
    evt.preventDefault();
    var message = input.querySelector("input").value;
    connection.invoke("SendMessage", message)
        .catch(error => console.error(error));
});

Ważne jest, aby pierwszy argument metody invoke pokrywał się z nazwą metody zdefiniowanej w hub-ie.