SignalR und ein Timer

Hitzesommer 2020, Dürren, Waldbrände, Weil das alles nicht eingetroffen ist, wir soft Lockdown erleben, es am Sonntag mal wieder regnet und man bei 16° den Langärmligen auspacken muss- ein Blazor Blog Beitrag.

image

Die Beziehung Server und Browser waren von Geburt an auf kurzfristig angelegt. Ein Request folgt ein Response und das wars mit der Liebe. Später hat man wieder die HTTP Natur – per Status – eine längerfristige Verbindung angestrebt. Dabei hat immer nur eine Seite das Anspracherecht, der andere muss einfach nur liefern. Im Zuge einer Genderwissenschaftsstudie, wurde festgestellt- auch irgendwie blöd. Man braucht mehr Gerechtigkeit, beide Seite sollen unaufgefordert mit der anderen sprechen können und so hat das W3C (keine chinesisch unterwanderte UNO Teilorganisation) WebSockets per Dekret eingeführt. Microsoft wiederrum (Amis halt) mussten daraus ihr eigenes Ding machen und abstrahieren, wie wir Softwareentwickler das immer so tun, wenn es um fremdes Werk geht.

SignalR

Mit SignalR haben die Redmonder eine Bibliothek geschaffen, die es erlaubt per Pusch vom Webserver eine Nachricht zum Klienten zu senden. Das beschränkt sich nicht auf den Browser, da die Client Library auch .NET und JavaScript vorliegt. Über den Zeitstrahl gibt es 2 oder auch 3 Varianten. Die klassische .NET Variante (2.4) ist weder Code noch Protokoll kompatibel zur .NET core (1 oder 3). Wer mit Blazor oder auch nur mit asp.net core ab Version 3.x arbeitet braucht _kein_ Nuget Paket extra installieren.

Mein erster Gedanke war eine Blazor WebAssembly App die mit Daten vom Server versorgt wird, z.b. Börsenkurse. Nun bin ich keine Börse was also? Alles nur nicht noch ein Chat, Also irgendwelche CPU Werte die in kurzen Intervall an alle verbunden Clients ausgeliefert werden. Wir brauchen also einen Timer.

Um das Beispiel nachvollziehen zu können:

-erstelle eine UWP Anwendung

-füge der Solution eine ASP.NET Core Webanwendung hinzu

image

Eine Hub Klasse dient als Kommunikationszentrale für alle verbundenen Clients.  Diese erbt von Hub und definiert eine Methode die vom Client aufgerufen werden kann. Hier SendMessage genannt. Eigentlich brauchen wir die aber nicht, weil den Job der Timer übernimmt.  Die Hilfsmethode GetCpublabla liefert irgendeinen Counter- Feel free- erfinde was eigenes.

 

   1:  public class CPUHub:Hub
   2:  {
   3:    double tickcount;
   4:    private  readonly System.Timers.Timer _timer =
new System.Timers.Timer();
   5:  ......
   6:   
   7:  public async Task SendMessage(string user, string message)
   8:  {
   9:      string msg = (await GetCpuUsageForProcess()).ToString();
  10:      await Clients.All.SendAsync("ReceiveMessage", user, msg);
  11:  }
  12:  private async Task<double> GetCpuUsageForProcess()
  13:  {
  14:     var proc = Process.GetCurrentProcess();
  15:     var cpu = proc.TotalProcessorTime.TotalSeconds;
  16:     return cpu;
  17:  }

Als nächstes der C# Code, der benötigt wird um den Timer ticken zu lassen. Damit wird die in SendMessage enthalten Logik zeitgesteuert ausgeführt. Allerdings ergeben sich in der Praxis Probleme. Eine Hubklasse, die irgendwie statisch ausgeführt werden muss, sonst wäre da der Status der verbundenen Clients futsch. Der Timer wiederrum läuft in welchem Context. Bei UI Code würde man ggf ein UI Thread Dispatch benötigen. Meine Lösung: der Hub Context findet sich im Dependency Container. Folglich brauchen wir die Logik um per Konstruktor DI die Referenz darauf zu erhalten.

   1:   private readonly IHubContext<CPUHub> _hubContext;
   2:   public CPUHub(IHubContext<CPUHub> hubContext)
   3:    {
   4:      _timer.Interval = 1000;
   5:      _timer.Elapsed += timer_Tick;
   6:      _timer.Start();
   7:      _hubContext = hubContext;
   8:   }

Nun kurze Stipvisite in der startup.cs um SignalR richtig anzumelden und zu konfigurieren.

   1:  public void ConfigureServices(IServiceCollection services)
   2:  {
   3:   services.AddRazorPages();
   4:   services.AddSignalR();
   5:   .....
   6:  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
   7:  {
   8:  ...
   9:   app.UseEndpoints(endpoints =>
  10:   {
  11:   endpoints.MapRazorPages();
  12:   endpoints.MapHub<CPUHub>("/CPUHub");
  13:  });
  14:    
  15:   

Der Tick des Timers ruft dann auf dem Hub Context für alle verbundenen Clients (.All) die dortige Methode “ReceiveMessage” auf und übergibt 2 Parameter.

   1:   private async void timer_Tick(
   2:   object sender, System.Timers.ElapsedEventArgs e)
   3:   {
   4:     tickcount++;
   5:     string msg = (await GetCpuUsageForProcess()).ToString();
   6:     await _hubContext.Clients.All.SendAsync(
"ReceiveMessage", tickcount.ToString(), msg);
   7:   }

 

Der Tickcount dient mir nur dazu um bei Systemtests festzustellen, ob Nachrichten verloren gehen. Kann man natürlich weglassen und mit einem Parameter bei ReceiveMessage arbeiten.

Man hat das idente Problem, wenn man aus z.B. REST Controller Aufrufen eine SignalR Hub Methode anstoßen möchte. Der Hub Context muss in die API Controller Klasse injiziert werden.

SignalR Client

So nun zur UWP Anwendung (ja echt, keine App). Es fehlt das Paket, installieren per Nuget und gf checken in der csproj.

<PackageReference Include="Microsoft.AspNetCore.SignalR.Client">
     <Version>3.1.6</Version>
   </PackageReference>

Mein UI wird UWP nicht gerecht und ist über alle Maßen einfach

   1:   <StackPanel Orientation="Horizontal">
   2:      <TextBlock Text="TextBlock" TextWrapping="Wrap" x:Name="text1"/>
   3:      <TextBlock Text="TextBlock" TextWrapping="Wrap" x:Name="txtTicks"/>
   4:      <TextBlock Text="TextBlock" TextWrapping="Wrap" x:Name="txtRec"/>

Die Logik der XAML Codebhind Datei könnte genauso in einer Blazor Page genutzt werden. Das Connection Objekt wird basierend auf dem Fluent Interface Pattern von Martin Fowler. Ich mag diese Builder – Build() Notation hier nicht. In LINQ hilft es lesbaren Code in eine Zeile zu pressen.

Ab Zeile 14 wird das Event per .On registriert. Auch etwas was im Vergleich zum C# += unhandlich wirkt, aber eben die Flexibilität eine Strings voraussetzt.

   1:  private HubConnection hubConnection;
   2:  double rec;
   3:   
   4:  public  MainPage()
   5:   {
   6:     this.InitializeComponent();
   7:     hubConnection = new HubConnectionBuilder()
   8:     .WithUrl(new Uri(@"https://localhost:44354/CPUHub"))
   9:     .Build();
  10:   }
  11:   
  12:  protected async override void OnNavigatedTo(NavigationEventArgs e)
  13:    {
  14:      hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
  15:       {
  16:         text1.Text = message;
  17:         rec++;
  18:         txtRec.Text = rec.ToString();
  19:         txtTicks.Text = user;
  20:        });
  21:   
  22:        await hubConnection.StartAsync();
  23:        base.OnNavigatedTo(e);
  24:    }

Ein Tipp noch: das klappt bei mir auf meiner Windows 10 Maschine nur, wenn ich einen Http Debugging Proxy wie Fiddlertool dazwischen schalte. Das bedeutet Fiddler muss gestartet sein, dann die Webiste und dann noch das UWP Projekt.

Kommentare sind geschlossen