signalR, geen pull, maar push!

Ingediend door Dirk Hornstra op 04-mar-2024 22:10

Het is niet best, inmiddels staan er 119 acties op mij te wachten... zaken uit podcasts die ik nog eens wil onderzoeken, andere programmeertalen die ik nog eens wil bekijken, noSQL databases die ik wil onderzoeken, API's en tools van Microsoft. Maar ja, er zitten maar 24 uren in een dag, daarvan werk je 8 uur, heb je een half uur pauze, in totaal (heen- en terugreis) een uurtje reizen, kijk je nog even TV, moet je slapen, eten en de rest van de tijd... ben je bezig om je mailbox op te schonen :)

Maar goed, er zal in de oudheid vast een wijze man/vrouw geweest zijn die gezegd heeft: "als je 1 taak oppakt en uitvoert wordt je lijstje toch weer korter". Daar put ik dan maar hoop uit :)

Zo stond het punt "signalR" al een hele tijd op mijn lijst. In podcast 291 van Scott Hanselman (de Hanselminutes) kwam dit ter sprake. De uitzending is van 3 november 2011. Dertien jaar geleden! Dat het nog steeds een actief product is, dat is een goed teken. Er zijn tenslotte flink wat zaken de afgelopen jaren vervallen, Silverlight van Microsoft, doordat Apple geen flash wilde gebruiken hoef je ook geen Flash meer in je browser te gebruiken. En JavaScript-applets? Die werken volgens mij ook al lang niet meer.

Wat is signalR?

Daarvoor gaan we eerst even naar de andere techniek: AJAX, wat staat voor Asynchronous JavaScript And XML. Vroeger had je websites waarbij je de volledige pagina moest laden om te zien of er wat aangepast was (dus je ziet de tekst, "bezig met inladen gegevens, druk op verversen"). Dat is natuurlijk niet heel gebruikersvriendelijk en bij trage verbindingen zit je dan ook al gauw tegen een wit scherm aan te kijken.

Met AJAX kon je via JavaScript een bericht naar de webserver sturen "is er nog data?". De webserver kon dan reageren: "nog niet", of "ja!". En dan kon er data terug gegeven worden, ook HTML. De JavaScript-code plaatste de HTML dan in je pagina en "ineens" ziet de gebruiker de bijgewerkte gegevens.

Een hele mooie techniek. Alleen... de timing. Want je wilt niet elke seconde een bericht sturen: "is er nog data?", "is er nog data?" omdat je dan een soort DDOS-actie op je eigen site uitvoert. Dus je zult dit misschien instellen op "elke minuut". Alleen, als je data na 20 seconden beschikbaar is, dan zit je eigenlijk 40 seconden voor niks te wachten.

Ik liep hier bij een opdracht tegenaan. De gebruiker kan bepaalde data laten berekenen. De ene keer zijn dat 20 records, de andere keer 365 records. En duurt het de ene keer 10 seconden, de andere keer, als je pech hebt en ook andere processen draaien, kan het wel 30 minuten duren. De eerste opzet heb ik meteen goed uitgewerkt door die verwerking niet in de code van de cliënt te zetten, de persoon die in de browser de opdracht start zit niet 30 minuten naar een wit scherm te kijken, de taak wordt namelijk via Hangfire verwerkt. Hangfire biedt de mogelijkheid om de taken te query-en, is jouw taak al gestart (of staat ie in de wachtrij), of is deze al succesvol verwerkt. Iedereen die op de pagina met de gegevens komt, krijgt dus meteen de melding te zien "dat de data momenteel berekend wordt", zodat je weet dat je mogelijk niet naar de meest up-to-date waardes zit te kijken.

Maar je wilt dus niet elke minuut vragen "ben je klaar", want misschien is het proces na 10 seconden al klaar (en dan wil je meteen door). En je wilt ook niet elke 5 seconden de pagina automatisch opnieuw laden.

Hier komt signalR in beeld. Ik had hier al eerder wat mee willen doen (en dan had ik het nu meteen kunnen implementeren), maar goed, het biedt me de mogelijkheid om er nu meteen een blogpost over te delen en jou misschien verder te helpen!

Eerst naar de homepage van SignalR. Daar staat een grote link naar "Get Started", waarbij je naar een voorbeeld van een chat-applicatie gaat. In dit geval naar .NET Core code, maar ik werk "nog" met een .NET Framework applicatie. Ik ben daarom maar even gaan zoeken en kom onder andere bij deze documentatie uit:


Het lijkt me dat dit voor mijn "probleem" voldoende zou moeten zijn. Ook hier staat nog een chat-applicatie-voorbeeld met een MVC oplossing. Die heb ik lokaal werkend en dat gaat goed.

Maar... het is een "hub". Dus je gaat naar de browser, voert je naam in en typt berichten in: deze worden naar de hub gestuurd en die stuurt het bericht door naar de andere "luisterende partijen" van de hub.
Ik had ook nog even een System.Timers.Timer object aangemaakt in mijn class, maar daar zie je het "vluchtige karakter van het web", zodra je een aanroep naar de hub gedaan hebt, wordt de instantie weer opgeruimd.

Dat doe ik dus even anders... en dat lijkt wel te werken! Het is een stukje POC (proof of concept) code, dus het moet vast anders en beter, maar dit werkt.
Wat ik doe is dat ik de code redelijk in stand hou, maar ook in de StartUp een static class aanmaak. Daar zit een timer op en die stuurt nu elke 10 seconden een bericht naar elke aangesloten cliënt.



Startup.cs


using Microsoft.Owin;
using Owin;
using TestSignalR.Helpers;

[assembly: OwinStartup(typeof(TestSignalR.StartUp))]
namespace TestSignalR
{
    public partial class StartUp
    {
        public void Configuration(IAppBuilder app)
        {
            // Any connection or hub wire up and configuration should go here
            app.MapSignalR();
            HangfireDummy.StartPolling();
        }
    }
}

Helpers/HangfireDummy.cs

using Microsoft.AspNet.SignalR;

namespace TestSignalR.Helpers
{
    public static class HangfireDummy
    {
        private static System.Timers.Timer timer;

        public static void StartPolling()
        {
            timer = new System.Timers.Timer() { Interval = 10000 };
            timer.Elapsed += Timer_Elapsed;
            timer.Start();
        }

        private static void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            var context = GlobalHost.ConnectionManager.GetHubContext<HangfireHub>();
            context.Clients.All.addNewMessageToPage("Hangfire calling!", "all oké over there?");
        }
    }
}

HangfireHub.cs

using Microsoft.AspNet.SignalR;

namespace TestSignalR
{
    public class HangfireHub : Hub
    {
    }
}

Chat.cshtml

@{
    ViewBag.Title = "Chat";
}
<h2>Chat</h2>
<div class="container">
    <ul id="discussion">
    </ul>
</div>
@section scripts {
    <!--Script references. -->
    <!--The jQuery library is required and is referenced by default in _Layout.cshtml. -->
    <!--Reference the SignalR library. -->
    <script src="~/Scripts/jquery.signalR-2.2.2.min.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="~/signalr/hubs"></script>
    <!--SignalR script to update the chat page and send messages.-->
    <script>$(function () {
            // Reference the auto-generated proxy for the hub.
           var chat = $.connection.hangfireHub;
            // Create a function that the hub can call back to display messages.
            chat.client.addNewMessageToPage = function (name, message) {
                // Add the message to the page.
                $('#discussion').append('<li><strong>' + htmlEncode(name)
                    + '</strong>: ' + htmlEncode(message) + '</li>');
            };
           $.connection.hub.start().done(function () {});

        });
        // This optional function html-encodes messages for display in the page.
        function htmlEncode(value) {
            var encodedValue = $('<div />').text(value).html();
            return encodedValue;
        }</script>
}

 

Je ziet dat ik de code van de chat behoorlijk gestript heb. Het is alleen bedoeld om te ontvangen, niet meer om iets te versturen.
Misschien ga ik nog wel initieel data versturen, maar dat zal in de Javascript-code uitgevoerd worden en is dus niet zichtbaar voor de gebruiker.
Waar je ook even op moet letten, in het voorbeeld heet de class ChatHub. Ik heb die van mij HangfireHub genoemd. Maar dan moet je ook de aanroep in het Javascript aanpassen! Het is dan geen $.connection.chatHub; maar $.connection.hangfireHub;

Dat je nu een timer hebt die regelmatig controleert of er taken afgerond zijn, dat is natuurlijk beter dan dat je 100 aanroepen binnen krijgt om die controle uit te voeren (en dat die elk "los" een aanroep uitvoeren).
Een soort callback-methode zou nog mooier zijn. Maar omdat taken via de database uitgevoerd worden zal er een query-actie uitgevoerd moeten worden.  Ik doe zelf al acties met JobStorage.Current.GetMonitoringApi() en waarschijnlijk zal ik die ook hier gebruiken.

In de code als de Timer getriggerd wordt, wordt nu "all oké over there" gestuurd, maar dit zou een stuk JSON kunnen zijn met de ID's van de taken, met hun status. Dus of ze staan te wachten of uitgevoerd worden. De aanroepende cliënt kan dan valideren of het ID van de eigen taak hier tussen staat. Zo ja, dan kan in het scherm bijgewerkt worden wat de huidige status is ("taak staat te wachten", "momenteel wordt de taak verwerkt") en kan, als de taak niet meer in de lijst voorkomt er vanuit gegaan worden dat de taak afgerond is. Mogelijk wil je nog wel iets met failed tasks doen o.i.d., maar dat is leuk voor versie 2 :)

En misschien nog even kijken of er ook een soort "cleane versie" is, ik zie hier namelijk jQuery-afhankelijkheden. Daar willen we eigenlijk zoveel mogelijk vanaf....