SignalR WebSockets

SignalR: Echtzeit-Kommunikation im Web

Stephan Pfeiffer

Das klassische HTTP-Protokoll ist zustandslos: Der Client fragt an, der Server antwortet. Doch was passiert, wenn der Server den Client aktiv über neue Ereignisse informieren muss? Hier kommen WebSockets und ASP.NET Core SignalR ins Spiel.

Das Problem mit Long-Polling

Früher hat man sich mit "Long-Polling" beholfen: Der Browser schickt alle paar Sekunden per JavaScript einen Request an den Server, um zu fragen: "Gibt es etwas Neues?". Das erzeugt enormen Overhead und skaliert schlecht. WebSockets halten stattdessen eine einzige, permanente TCP-Verbindung offen, über die bidirektional Daten fließen können.

Die Magie von SignalR

SignalR abstrahiert diese komplexe Verbindungslogik. Es nutzt standardmäßig WebSockets, fällt aber bei älteren Browsern automatisch auf Server-Sent Events oder Long-Polling zurück. Über SignalR können Clients in Echtzeit Nachrichten austauschen und Systemereignisse synchron empfangen.

Das Herzstück im Backend ist der Hub:

CanvasHub.cs
using Microsoft.AspNetCore.SignalR;
using Homepage.Services;

namespace Homepage.Hubs
{
    public class CanvasHub : Hub
    {
        private readonly PixelCanvasService _canvasService;

        public CanvasHub(PixelCanvasService canvasService)
        {
            _canvasService = canvasService;
        }

        public async Task DrawPixel(int x, int y, string color)
        {
            // Speichere den Pixel im Singleton-Service
            _canvasService.SetPixel(x, y, color);

            // Informiere ALLE verbundenen Clients außer dem Sender
            await Clients.Others.SendAsync("ReceivePixel", x, y, color);
        }
    }
}

Integration im Frontend

Um diesen Hub anzusprechen, reicht im Frontend ein kurzes JavaScript. Wir verbinden uns mit dem Endpunkt und definieren, was passieren soll, wenn der Server die Methode ReceivePixel aufruft:

JavaScript (Frontend)
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/canvashub")
    .build();

// Was passiert, wenn der Server ruft?
connection.on("ReceivePixel", (x, y, color) => {
    ctx.fillStyle = color;
    ctx.fillRect(x, y, 10, 10);
});

// Verbindung starten
connection.start().catch(err => console.error(err));

// Selber zeichnen und an den Server senden
canvas.addEventListener("click", (e) => {
    const x = e.offsetX;
    const y = e.offsetY;
    connection.invoke("DrawPixel", x, y, "#FF0000");
});

Skalierung in der Praxis

Bei tausenden gleichzeitigen Nutzern stößt ein einzelner Server an sein Limit (Stichwort: Socket Exhaustion). In Azure lässt sich das über den Azure SignalR Service auslagern, bei On-Premise-Umgebungen (wie meiner) nutzt man meist ein Redis Backplane, um Nachrichten zwischen mehreren Web-Nodes zu synchronisieren.