WebSockets mit ASP.NET: SignalR

written by devangelist on Dezember 14, 2012 in Allgemein and ASP.NET and Browser and HTML5 and Javascript with 2 comments

Was bei Node sogut wie von Anfang an dabei war kommt für ASP.NET mit SignalR: Echtzeitkommunikation zwischen Client und Server.

In Vergangenheit war es relativ schwierig vom Server den Client direkt anzusprechen. Das weit verbreitete Chat-Beispiel, wo genau dieses Szenario eintretet, sprich der Server bei Eintreffen einer neuen Nachricht alle Clients benachrichtigen muss – wurde meist mit Umwegen gelöst:

  • xhr-polling: Der Client sendet alle 5 Sekunden einen (xhr-)Request zum Server um eventuelle neue Nachrichten zu erhalten.
  • long-polling: Der Client sendet einen Request zum Server und anstelle einer leeren Antwort wird die Verbindung gehalten und gewartet, bis neue Daten verfügbar sind. Erst wenn der Request abgeschlossen ist, wird ein neuer gesendet. Vorteil bei dieser (long-poll) Variante sind die geringere Anzahl an Verbindungen und die damit verbundene Entlastung des Servers im Öffnen und Schließen von Verbindungen.
  • forever-frame (hidden iframe): Ist im Prinzip nichts anderes als ein verstecktes iFrame, welches  als chunked-Block gesendet wird. Das dabei verwendete Feature Chunked Encoding ist Teil der HTTP 1.1 Spezifikation und wurde ursprünglich für das Senden und inkrementelle Rendern von großen Dokumenten gedacht. Der Server kann also mit dem Senden der Antwort sofort beginnen, noch bevor die Länge überhaupt bekannt ist. Dieses stückweise-Zurücksenden der chunks kann für das Aufrechterhalten einer lang lebende Verbindung verwendet werden und somit ein Push vom Server zum Client ermöglichen. Beim forever-frame werden die Antworten stückweise in Script-Tags gerendert, um somit JavaScript Code im Browser auszuführen.

Weksockets

Das WebSocket-Protokoll ist ein auf TCP basierendes Netzwerkprotokoll für bidirektionale (full-duplex) Verbindungen zwischen Web- und Serveranwendungen.

Der Große Vorteil im WebSocket-Protokoll liegt im einmaligen Öffnen der Verbindung. Hat der Client somit die Verbindung geöffnet, kann diese vom Server verwendet werden um ein richtiges Push zu ermöglichen. Zudem entfällt der ganze HTTP-Overhead, welcher bei jedem Request entstehen würde.

Bevor die Verbindung geöffnet wird, führen Server und Client einen sogenannten Handshake durch. Dabei werden Informationen wie der verwendete Host (und Ressourcen-Name) sowie ein zufällig generierter Key (Sec-WebSocket-Key) zur Überprüfung, ob der Server die Anfrage interpretiert hat, mitgegeben.

Beispiel-Handshake (Wikipedia)

GET /mychat HTTP/1.1
 Host: server.example.com
 Upgrade: websocket
 Connection: Upgrade
 Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
 Sec-WebSocket-Protocol: chat
 Sec-WebSocket-Version: 13
 Origin: http://example.com

Der Server antwortet ebenfalls mit einem Schlüssel (Sec-WebSocket-Accept) welcher als Verifikation dient. Dieser wird aus dem vom Client gesendeten Base64-kodierten Sec-WebSocket-Key und der statischen GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 zusammengesetzt, als SHA1-Hash erstellt und Base64-kodiert.

HTTP/1.1 101 Switching Protocols
 Upgrade: websocket
 Connection: Upgrade
 Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
 Sec-WebSocket-Protocol: chat

Es gibt also jede Menge Vorteile, aber einen (noch) großen Nachteil: Die Browser-Kompatibilität, denn nur Moderne Browser unterstützen das neue WebSockets Protokoll. Auch auf der Serverseite sieht es nicht viel anders aus: erst seit IIS 8 gibt es eine Unterstützung für Websockets.

Who cares?

Dass man von einer vollen Unterstützung von WebSockets ausgehen kann ist somit ausgeschlossen; es bleibt also nur eine Fallback Möglichkeit. Mithilfe von SignalR wird dies zum Kinderspiel: Werden WebSockets nicht unterstützt, wird automatisch auf Long polling und Forever Frame zurückgegriffen.

Als Erstes das folgende NuGet Package über die Console installieren:

Install-Package Microsoft.AspNet.SignalR -pre

In SignalrR gibt es zwei Konzepte: persistent connections (Dauerhafte Verbindungen) und hubs.
Erstere sind im Prinzip eine lower-level-API welche auf HTTP aufbauen. In den meisten Fällen, besonders in Webanwendungen, werden Hubs verwendet, welche im Prinzip das publish-subscribe Pattern verfolgen.
Wir folgen dem weit verbreiteten Beispiel der Chat Anwendung, welches auf der Serverseite jedenfalls, in einer handvoll Zeilen Code geschrieben ist:

public class Chat : Hub
{
    public void Send(string message)
    {
        Clients.All.addMessage(message);
     }
}

Clientseitig reicht eine einfache HTML Seite mit folgendem Code:

<html>
    <head>
        <script src="http://code.jquery.com/jquery-1.8.2.min.js" type="text/javascript"></script>
        <script src="Scripts/jquery.signalR-1.0.0-alpha2.min.js" type="text/javascript"></script>
        <script src="~/signalr/hubs" type="text/javascript"></script>
        <script type="text/javascript">
            $(function () {
                // Proxy
                var chat = $.connection.chat;

                // Funktion welche der Server aufrufen kann.
                chat.client.addMessage = function (message) {
                    $('#messages').append('<li>' + message + '</li>');
                };

                // Verbindung aufbauen.
                $.connection.hub.start().done(function () {
                    $("#broadcast").click(function () {
                        // Die Methode auf dem Server aufrufen.
                        chat.server.send($('#msg').val());
                    });
                });
            });
        </script>
    </head>
    <body>
        <div>
            <input type="text" id="msg" />
            <input type="button" id="broadcast" value="broadcast" />
            <ul id="messages"></ul>
        </div>
    </body>
</html>

Der ~/signalr/hubs Endpunkt bietet die Möglichkeit auf allen öffentlichen Methoden der Hubs zuzugreifen. Ein Auszug aus der generierten Datei aus der Demo:

signalR.chat = signalR.hub.createHubProxy('chat');

Doch bevor diese Methoden verwendet werden können, muss die Verbindung aufgebaut werden:

$.connection.hub.start()
    .done(function () {
        // connected!
    })
    .fail(function () {
        // connection failed!
    });

Wenn die Verbindung aufgebaut ist, kann der Client die Methoden des Hubs (Kurze Erinnerung: public void Send(string messagee)) aufrufen…

chat.server.send($('#msg').val());

…und auf deren Callbacks (Kurze Erinnerung: Methodenkontext Clients.All.addMessage(message)) subscriben:

chat.client.addMessage = function (message) {
    $('#messages').append('<li>' + message + '</li>');
};

Das ausgeführte Beispiel, in zwei Browser-Fenster geöffnet, sollte die eingegebenen Nachrichten von einem auf das andere Fenster transportieren. Zugegeben, das war wirklich sehr einfach, aber hinter den Kulissen bietet das Hub eine Menge Möglichkeiten:

Um messages an den Clients zu übertragen kann das Clients dynamic object verwendet werden.
Jegliche Methode welche im Hub aufgerufen wird, ruft das dementsprechende Callback auf dem Client auf.

Um Mitteilungen nur an einem bzw. bestimmten Teilnehmern zu versenden gibt es die Möglichkeit dies direkt über die ConnectionId oder einer group zu machen:

public void Send(string data)
{
     // Nur an eine spezifische ConnectionId (In dem Falle dem Caller selbst) senden
     Clients.Client(Context.ConnectionId).addMessage(data);
}
public Task Join()
{
    return Groups.Add(Context.ConnectionId, "foo");
}
public Task Send(string message)
{
    return Clients.Group("foo").addMessage(message);
}

Hinweis: Wenn sich Verbindungen beenden (disonnect), werden diese automatisch der von der Gruppe entfernt.

Zusammenfassung:

public void Send(string message)
{
    // An alle senden.
    Clients.All.send(message);

    // An alle außer dem Caller selbst.
    Clients.Others.send(message);

    // An alle außer einer ConnectionId.
    Clients.AllExcept(Context.ConnectionId).send(message);

    // An den Caller sebst.
    Clients.Caller.send(message);

    // An die Gruppe "foo".
    Clients.Group("foo").send(message);

    // An alle in der Gruppe "foo" außer dem Caller selbst.
    Clients.OthersInGroup("foo").send(message);

    // An alle in der Gruppe "foo" außer der gegebenen ConnectionId.
    Clients.Group("foo", Context.ConnectionId).send(message);

    // An eine bestimmte ConnectionId.
    Clients.Client(Context.ConnectionId).send(message);
}

Hinweis: jegliche Rückgabe vom Hub wird als JSON serialisiert.

A step deeper:

Das Erkennen von neuen Verbindungen bzw das Trennen und Wiederverbinden funktioniert vom Prinzip ähnlich:

public class Chat : Hub
{
    public override Task OnConnected()
    {
        return Clients.All.joined(Context.ConnectionId, DateTime.Now.ToString());
    }

    public override Task OnDisconnected()
    {
        return Clients.All.leave(Context.ConnectionId, DateTime.Now.ToString());
    }

    public override Task OnReconnected()
    {
        return Clients.All.rejoined(Context.ConnectionId, DateTime.Now.ToString());
    }
}

Das Hub, außerhalb des Hubs

Manchmal kann es durchaus Sinn machen, die Clients bei Eintreten eines bestimmten Events zu benachrichtigen. Typisches Beispiel dafür könnte ein background-task sein, welcher irgendwann entscheidet ein Event auf dem Client aufzurufen. Auch dafür bietet das SignalR-Framework eine vorgefertigte Möglichkeit: Der dependency-resolver für den aktuellen Host löst das IConnectionManager interface auf und gibt somit Zugriff auf das Kontext-Objekt für das Hub:

public class Notifier
{
    public static void Say(string message)
    {
        var context = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
        context.Clients.All.addMessage(message);
    }
}

public class MemberManager
{
    public static void AddConnection(string connectionId, string groupName)
    {
        var context = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
        context.Groups.Add(connectionId, groupName);
    }

    public static void RemoveConnection(string connectionId, string groupName)
    {
        var context = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
        context.Groups.Remove(connectionId, groupName);
    }
}

Fazit:

Ziemlich coole Sache: Angefangen daran, dass das Projekt auf GitHub steht bis hin zur Verwendung, welche wirklich einfach und unkompliziert ist. Dies ist man von Microsoft in der Art und Weise nicht gewohnt, aber sicherlich von vielen Entwicklern sehr willkommen. Ich habe bereits einige Projekte mit NodeJS und damit Socket.IO bzw. SockJS erstellt und finde, dass die Richtung in welche sich Microsoft mit SignalR bewegt, definitiv die richtige ist. *thumbsup*

Update: Ein fertiges Beispiel kann direkt über Nuget in das Visual Studio Projekt geladen werden:

Install-Package Microsoft.AspNet.SignalR.Sample
WebSockets mit ASP.NET: SignalR: 1 Stern2 Sterne3 Sterne4 Sterne5 Sterne 4,73 von 5 Punkte, 11 abgegebene Stimmen.
Loading ... Loading ...

About the Author

Roberto Bez ist passionierter Webentwickler und TechLead bei der HolidayCheck AG. Für Roberto bedeutet das Entwickeln nicht nur Arbeit, sondern auch Freude, Motivation und täglich neue, aufregende Herausforderungen. Besonders gerne setzt er sich mit neuen Webtechnologien sowie Datenbanken aller Art auseinander und versucht diese in die tägliche Anwendungsentwicklung miteinzubringen. Neben dem Entwickeln trifft man ihn gerne Abends beim Laufen oder im Sommer bei Mountainbike-Touren durch die schönen Berge Südtirols.