Web vNext mit SignalR

Die rasante Entwicklung von neuen Technologien gibt einem Softwareentwickler selbst im Urlaub kaum Zeit zur Erholung. Es wäre bestimmt einfacher und sicher erholsamer, einfach den Sommer zu genießen, als schon wieder irgendeine neue Technologie kennen zu lernen. So gesehen ist dieser Artikel eine schlechte Nachricht.
Wenn Sie aber in die Welt von neuartigen Anwendungen einsteigen möchten, haben Sie sowieso keine Wahl und sind hier genau richtig. Denn in diesem Artikel geht es darum, eine neue und auf GitHub sehr populäre Open-Source Bibliothek namens SignalR vorzustellen. Wenn Sie Anwendungen entwickeln, oder entwickeln möchten, die als "Connected Distributed Web Systems" bezeichnet werden, ist SignalR eine valide Option.

In welcher Zeit leben wir?

Das Web scheint mit einer immer größeren Anzahl an mobilen Geräten am Markt nicht an Bedeutung zu verlieren. Die Apps können nicht unbedingt viel, aber kommen gut an. Man könnte fast behaupten, die Apps könnten die klassischen Webanwendungen ein wenig verdrängen. Es kling fast paradox, aber es steht fest: Das Web war nie wichtiger. Es ist nicht die Absicht dieses Artikels, die Trends bezüglich Apps, Desktop-, oder Webanwendungen zu beleuchten. Vielmehr versuchen wir uns davon zu befreien und treffen eine mutige, aber sinnvolle Annahme: "Das Web ist der Bus". Nun, zumindest in diesem Artikel, betrachten wir das Internet als Software-Bus-System. Dies gibt uns die Möglichkeit, einigen aktuellen Herausforderungen in der Anwendungskommunikation mit einem neuen Lösungsansatz zu begegnen.

Grundsätzlich kann eine Anwendung (Partizipant im System) eine private oder eine öffentliche I PAdresse besitzen. Unter Partizipant verstehe ich eine beliebige Anwendung, die in einem Kommunikationssystem interagiert. Das könnte zum Beispiel ein Browser, eine App oder ein Service sein.

In der Regel muss man aber davon ausgehen, dass alle "Partizipanten" eine private, nicht im Internet veröffentlichte Adresse besitzen und dadurch in der Praxis von außen nicht erreichbar sind. Oder noch ungünstiger: Sie sind hinter einer Firewall "versteckt" und damit ebenfalls nicht zugänglich.

Was ist die Anforderung?

Unser Ziel in diesem Artikel ist, solche Partizipanten vom Server aus ansprechen zu können, auch dann, wenn sie nicht direkt erreichbar sind.
Diese Anforderung ist gemeinhin unter dem Namen "Push Notification" bekannt und stellt eine große Herausforderung dar. Es gibt zahlreichen Szenarien, die häufig entweder gar nicht oder nur schwierig umgesetzt werden können, wenn es darum geht, die Anwendungen in ihrer "geschützten" Umgebung zu erreichen.

Vor etwa 15 Jahren, als C++ meine Welt dominierte, hätte ich persönlich die Kommunikation über WebSocket [1] mit C++ realisiert. Das wäre auch heute noch möglich, aber leider bedeutet die Vielfalt der verschieden Plattformen, dass WebSocket nicht überall verfügbar ist. Wenn Sie bereits Windows Server 2012 im Einsatz haben und Internet Explorer 10 oder Google Chrome unter Ihren Nutzern bereitstellen können, könnten Sie diese Technologie in Betracht ziehen.

Ich kenne leider niemanden, der diese Anforderungen erfüllen kann. Im Moment ist technologisch so viel im Umbruch, dass meiner Meinung nach sehr schwer ist, die richtigen Entscheidungen zu treffen, um auch nur die nächste halbe Dekade überbrücken zu können.

Zumindest für das Problem der Push Notifications könnte SignalR helfen und eine Lösung für die Zukunft sein.

Was ist SignalR?

SignalR ist eine Bibliothekensammlung, die Real-Time-Messaging zwischen Partizipanten ermöglicht und dabei verschiedene Protokolle mit einer WebSocket-ähnlichen API unterstützt.

Es handelt sich hierbei um ein ursprünglich privates Projekt der beiden Microsoft-Mitarbeiter Damian Edwards und David Fowler. Das Projekt ist auf GitHub unter [2] zu finden.

Bevor wir uns den Protokollen widmen, betrachten wir, wie eine Push-Lösung allgemein realisiert werden kann.

In Abb. 1 sind die verschiedenen Szenarien dargestellt. Das erste Szenario zeigt eine gewöhnliche Web-Anwendung. Ein Browser schickt einen Request zum Server, aber der Server kann nur eine Response-Message zurückschicken. Wie erwartet, ist ein Request von Server zum Browser nicht möglich. Dieses Szenario ist unsere Ausgangsituation.

Abb1-WebInfrastructure

Im zweiten Szenario realisiert der Browser das sogenannte Polling-Verfahren. Das heißt, der Browser schickt in regelmäßigen Intervallen einen Request zum Server, um ein Ergebnis abzufragen. Das ist eine sehr einfache Realisierung, entspricht aber keinem Real-Time-Messaging und belastet das Netzwerk zu stark. Abgesehen davon ist es sehr schwer, das richtige Intervall zu wählen. Wenn das Intervall zu kurz ist, ist die Belastung des Netzwerkes zu groß. Außerdem könnte dieses Verfahren für eine hohe Anzahl an Partizipanten vermutlich ungeeignet sein.

Um dieses Problem zu lösen, bietet SignalR eine Lösung an, die immer funktionieren kann: Das sogenannte Long-Polling. Dabei werden weniger Requests zum Server geschickt, weil die Intervalle zwischen jedem Requests größer sind (z.B. 2 Minuten). In der Zwischenzeit bleibt eine Verbindung offen. Über diese kann der Server den Client erreichen. Dies scheint eine intelligente Lösung zu sein.

Allerdings könnte sie neue Probleme mit sich bringen: Die Web-Infrastruktur ist auf Request/Response ausgelegt. Das heißt, Load-Balancer, Proxies usw. routen in der Regel die Requests nach verschiedenen Verfahren zum Empfänger, beispielsweise um die Last besser zu verteilen. Im Falle von SignalR und ebenso WebSockets hält der Server eine Verbindung jedoch auch dann offen, wenn keine Kommunikation erforderlich ist, und "torpediert" so die Funktionsweise der konventionellen WebInfrastruktur.

Bei einem Short Polling wäre dies nicht der Fall. Momentan kann man sogar davon ausgehen, dass für eine solche Art der Kommunikation dedizierte Server verwendet werden sollten. Hier gibt es jedoch bisher nur wenige Erfahrungen.

Server Komponenten von SignalR

Die Low-Level-Komponente ist SignalR Core und implementiert in der Assembly SignalR.Server alle nötigen Server-Funktionalitäten. Darüber hinaus gibt es drei Host-Komponenten:

SignalR.​Hosting.​AspNet

Sie bietet die Klassen an, die für das Hosting eines SignalR Server in ASP.NET gebraucht werden.

SignalR.​Hosting.​Owin

Host-Implementierung des Open-Webserver-Interface für .NET. Hierbei handelt es sich um den Versuch einen Standard für die Kommunikation zwischen .NET, Webserver und Web-Anwendung zu etablieren. Dieser Standard soll helfen, die Anwendung vom Server zu entkoppeln. Mehr dazu unter [3].

SignalR.​Hosting.​Self

Dieser Host wird verwendet, wenn der SignalR Server in einer Anwendung gehostet werden soll, die kein Web-Server ist. Das könnte beispielsweise ein Office Add-In oder ein Windows Service sein.

SignalR.​Hosting.​Self:

Dieser Host wird verwendet, wenn der SignalR Server in einer Anwendung gehostet werden soll, die kein Web-Server ist. Das könnte beispielsweise ein Office Add-In oder ein Windows Service sein.

class Program {
	static void Main(string[] args) {
		string url = "http://localhost:8081";
		using (WebApplication.Start<Startup>(url)) {
			Console.WriteLine("SignalR Host started on {0}", url);
			Console.ReadLine();
		}
	}
}

In der Regel bietet SignalR zwei Modelle an: Peristed Connections und Hubs. Damit befassen wir uns später etwas ausführlicher.

Client Komponenten von SignalR

Für die Implementierung der Client-Funktionalität bietet die SignalR- Bibliothek folgende Komponenten an:

SignalRJs

Die JavaScript-Bibliotheken

SignalR.Client

Die .NET Client- Bibliotheken, inklusive WinRT

SignalR.​Client.​WP7

Die SignalR Client-Bibliotheken für Windows Phone

SignalR.​Client.​Silverlight und SignalR.​Client.​Silverlight5

Die SignalR Client-Bibliotheken für Silverlight

Persistent Connection Programmier​modell

Client

Soweit Ihre Anwendung nur einfache Kommunikation benötigt, wird Ihnen die Implementierung eigener Connection-Klassen wahrscheinlich genügen. Das Schwierige dabei ist zu wissen, was "einfach" ist. In diesem Kontext scheint einfache Kommunikation vorzuliegen, wenn die Anwendung eine oder zwei (wenige) semantisch verschiedene, einfache Messages benötigt, um eine Funktionalität abzubilden.

Folgendes Beispiel zeigt clientseitig, wie die Connection im JavaScript erzeugt wird, wie die Messages empfangen werden und wie sie versendet werden.

$(function () {
	var connection = $.connection('/echo');
	connection.received(function (data) {
		$('#results).append('<li>' + data + '</li>');
	});

	connection.start().done(function() {
		$("#btnBroadcast").click(function () {
			connection.send($('#msg').val());//Bsp. 1. connection.send ({"prop1": ‚value of prop1‘});//Bsp. 2.
		});
	});
});

Der Befehl Send ist keine Message, sondern zeigt, dass Eingabeparameter als Message zu verstehen sind. Dieser Befehl sorgt dafür, dass die Instanz eines anonymen Typs entsprechend serialisiert wird.

Um anonyme Typen mit den Eigenschaften Prop1 und Prop2 zu verarbeiten, benötigt man in SignalR nicht mehr als eine sehr einfache Implementierung der Basisklasse PersistentConnection. Dazu gleich etwas mehr.

Sollte Ihre Anwendung viele Typen solcher Messages verwenden, wird es schwierig, diese in einem Handler auseinander zu halten. In solchen Fällen sind die sogenannten Hubs besser geeignet. Dies ist die andere Art, einen Server zu implementieren, die im nächsten Kapitel detailliert beschrieben wird.

Server

Die wichtigsten Teile der Klasse PersistentConnection sind in folgendem Listing zu sehen.

using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;

public class MyConnection : PersistentConnection {
	protected override Task OnReceived(IRequest request, string connectionId, string data) {
		// Broadcast Daten zu alle clients return Connection.Broadcast(data);
	}

	protected override Task OnConnected(IRequest request, string connectionId) {
		return Connection.Broadcast("Connection " + connectionId + " connected");
	}

	protected override Task OnReconnected(IRequest request, string connectionId) {
		return Connection.Broadcast("Client " + connectionId + " re-connected");
	}

	protected override Task OnDisconnected(IRequest request, string connectionId) {
		return Connection.Broadcast("Disconnected " + connectionId );
	}
}

Im Allgemeinen bietet die Klasse virtuelle Methoden wie OnReceived, OnConnected und OnReconnected an. Allein damit ist es möglich festzustellen, ob ein neuer Participant sich mit dem Server verbunden hat (OnConnected), oder wenn eine Message empfangen wurde (OnReceived).

Wichtig ist, dass jede Art von Message eine Verbindung mit dem Server darstellt. Das heißt, wenn OnReconnected aufgerufen wird, bedeutet das nicht, dass ein neuer Client das System betreten hat. Diese Methode wird bei jedem Request aufgerufen. Sie wird beispielsweise auch dann aufgerufen, wenn Polling-Requests von Clients im Hintergrund geschickt werden, um die Verbindung zu erneuern.

Manchmal möchten Sie die Messages außerhalb der Connection-Klasse versenden. Um dies zu bewerkstelligen gibt es die Klasse GlobalHost.

var context = GlobalHost.ConnectionManager.GetConnectionContext<MyEndPoint>();
context.Connection.Broadcast(message);

Darüber hinaus ist es ebenso möglich bestimmte Aufrufer einer Gruppe zuzuordnen. Folgendes Listing zeigt wie das geht.

protected override IList<string> OnRejoiningGroups(IRequest request, IList<string> groups, string connectionId) {
	return groups;
}

protected override Task OnReceived(IRequest request, string connectionId, string data) {
	...
	// Daten an eine Gruppe senden return Groups.Send(groupName, message);
}

protected override Task OnDisconnect(string connectionId) {
	return Groups.Remove(connectionId, "...");
}

Es ist zu beachten, dass die Send-Methode ein Argument vom Typ Objekt erwartet:

public static class ConnectionExtensions {
	public static Task Broadcast(this IConnection connection, object value, params string[] excludeConnectionIds);
	public static Task Send(this IConnection connection, string connectionId, object value, params string[] excludeConnectionIds);
}

Das gibt die Möglichkeit komplexere Messages (im XML-Jargon complex-type) ohne viel Mühe auszugeben:

Send({Prop1:‚xyz‘, Prop2:123});

Wenn Sie die Messages an die Gruppen verschicken möchten, können Sie ebenso GlobalHost verwenden.

var context = GlobalHost.ConnectionManager.GetConnectionContext<MyEndPoint>();
context.Groups.Send(group, message);

JavaScript-Clients werden diese Objekte als JSON empfangen.
Das ist in der Tat sehr einfach und irgendwie cool. Allerdings gilt solche Vorgehensweise in der Welt von SOA als "Communication Anti-Pattern". Wenn Sie eine App entwickeln, die kommunikationstechnisch eine oder zwei Messages bearbeitet, ist diese Technik in Ordnung. Wenn Sie aber viele Messages bearbeiten, sind Sie der eigentliche Service-Anbieter (private oder public). In diesem Falle sollten die Contracts klar definiert werden.

Bitte grundsätzlich keine anonymen Typen "auf Teufel komm raus" verwenden, nur weil sie schneller zu schreiben sind. Denken Sie daran, dass eine Anwendung nicht nur implementiert, sondern auch gewartet werden muss!

Last but not least: Ihre Implementierung der PersistentConnection ist eine gewöhnliche Klasse, die z.B. im Falle von ASP.NET Hosting irgendwo im Projekt existieren sollte. Darüber hinaus müssen Sie dem Host (z.B. IIS) die Route mitteilen, an der das Listening stattfinden wird. Dies geschieht üblicherweise in der Application_Start-Routine Ihrer Web-Anwendung, die SignalR anbietet:

public class Global : System.Web.HttpApplication {
	void Application_Start(object sender, EventArgs e) {
		RouteTable.Routes.MapConnection<MyEndPoint>("echo", "/echo");
	}
}

Mehr über PersistentConnection erfahren Sie unter [4].
Wenn Sie mehr über die JavaScript-Bibliothek und PersistentConnections erfahren möchten, folgen Sie dem Verweis[5].

Hubs

Wenn Ihre Anwendung hauptsächlich serviceorientiert arbeiten soll, sind Hubs die bessere Wahl gegenüber PersistentConnections. Ein Hub ist eine Klasse, die von der Basisklasse Hub ableitet. Das Interessante an diesem Konzept ist, dass die Basis-Klasse Hub keine virtuellen Methoden oder Schnittstellen vorsieht, die man überschreiben bzw. implementieren soll. Das wird deshalb betont, weil diese Vorgehensweise in fast allen APIs, die vergleichbare Aufgaben lösen, üblich ist.
Eine vollständige Implementierung eines Hub ist in diesem Listing zu sehen:

public class MyHub : Hub {
	public void DoWork(MyEntity input) {
		Clients.All.onMessage (new {...});
	}
}

Die Methode doWork in der Klasse MyHub ist quasi die Funktion die remote aufgerufen wird. In der WCF-Welt wäre dies eine gewöhnliche SOAP Operation.

Die Idee hinter diesem Konzept ist einem Client zu ermöglichen eine beliebige public-Methode an einem Hub aufzurufen. Das Beispiel oben zeigt wie die JavaScript Funktion onMessage auf allen Clients aufzurufen ist.
Folgender JavaScript Code zeigt wie dies in JavaScript zu realisieren ist:

var myHubProxy = $.connection.myHub;
contosoChatHubProxy.client.onMessage = function (msg) {
	console.log(msg);
};

$.connection.hub.start().done(function () {
	$('#newContosoChatMessage').click(function () {
		contosoChatHubProxy.server.doWork(msg).done(function(result) {
			console.log(result);
		}).fail(function(e1,e2,e3){ })
	});
});

Wenn JavaScript die Funktion doWork aufruft, wird im Service die Methode MyHub.DoWork (MyEntity input) aufgerufen. Die Message wird, soweit kompatibel definiert, automatisch in die Instanz von MyEntity umgewandelt.

Zugegebenermaßen ist dies für Entwickler sehr einfach zu implementieren, dank einer Proxy-Klasse. Beim Laden der Web-Anwendung sollte folgender Skriptverweis eingebunden werden:

<script src="/Daenet.SignalR/signalr/hubs" type="text/javascript"></script>

Durch diesen Request erzeugt SignalR im Hintergrund dynamisch eine JavaScript-Datei, die einige Hilfsfunktionen bietet.

Nachdem JavaScript den Aufruf zu doWork abgesetzt hat, blockt dieser Call, solange die Ausführung in MyHub.DoWork andauert, nicht. Das heißt, die Funktion doWork ist clientseitig asynchron, unabhängig davon, ob sie Rückgabeparameter hat oder nicht.

Unter der Haube

SignalR unterstützt verschiedene Protokolle (Transports), die den meisten Entwicklern kaum bekannt sind: WebSockets, LongPolling, ForeverFrames und ServerSentEvents.

Wenn clientseitig beispielsweise

connection.start();
aufgerufen wird, versucht SignalR das beste Protokoll zu finden. Im Idealfall wäre dies der WebSocketTransport. Sollte dies nicht möglich sein, wird SignalR für den Falle, dass der Internet Explorer verwendet wird, versuchen die Verbindung mit dem Server über ForeverFrame-Transport aufzubauen. Im Falle von Firefox, Chrome, Opera und Safari wird SignalR sein Glück mit dem ServerSentEvents Transport versuchen. Wenn keine der Varianten in Frage kommt, gibt es eine Fallback-Lösung namens LongPolling. Bitte beachten Sie, dass LongPolling die einzige Variante ist, die eine Annäherung an Real-Time-Messaging ermöglicht. Selbst im Falle von LongPolling sind extrem gute Echtzeitwerte möglich, weil die Kommunikation eigentlich durch die geöffneten Streams stattfindet.

Unabhängig vom Transport, wird eine Message vom Client immer einen Request/Response-Aufruf initiieren. Darüber hinaus besteht zusätzlich eine offene Verbindung zwischen dem Client und dem SignalR-Service (Persistent Connection bzw. Hub). Je nach Transport ist die Verbindung unterschiedlich aufgebaut. Auch das Format der Daten, die zwischen Client und Service hin und her fließen, ist logischerweise protokollabhängig.

Abb. 2 zeigt ein Long-Polling. Dies passiert in der Regel, wenn die Infrastruktur Proxies enthält, die kein Streaming unterstützen. Zum Beispiel unterstützt der Debugging-Proxy Fiddler auch kein Streaming in der Default-Konfiguration.

Abb. 2 Long-Polling

Eine andere Möglichkeit einen Transport zu erzwingen, ist beim Aufrufen der Startmethode die Property transport zu setzen. Folgender Aufruf erzwingt den LongPolling-Transport:

connection.start({ transport: ‘longPolling' });

Man kann auch mehrere Transports kombinieren:

connection.start({ transport: 'foreverFrame longPolling');

Wenn LongPolling aktiv ist, hält die Verbindung ca. 2 Minuten, insoweit keine Messages von Service zum Client geschickt wurden. Wenn eine Message zum Client geschickt wurde, erneuert der Client die Verbindung.

Wenn ein Client eine Message zum Service sendet, wird immer ein neuer Request zum Service geschickt.

Ähnlich wie beim LongPolling zeigen die Abbildungen 3 und 4 wie dieses Szenario mit den Transportarten ForeverFrames und ServerSentEvent aussieht.

Abb. 3 Forever Frames

Im Falle von ForeverFrames und ServerSentEvents wird eine Verbindung aufgebaut, die alle 2 Minuten erneuert wird, wenn keine Messages zwischen Client und Service fließen. Messages vom Service zum Client fließen durch eine geöffnete Verbindung und verursachen keinen neuen HTTP-Requests. Wenn ein Client eine Message zum Service schickt, wird ein neuer Request erzeugt.

Abb. 4 ServerSentEvents

Fazit

In diesem Artikel haben wir gesehen, wie man asynchrones Real-Time- Messaging über HTTP mit SignalR aufbauen kann. Gegenüber WebSockets hat SignalR den Vorteil, dass man nicht auf den Support von WebSockets in HTML5 warten muss.

Auf der anderen Seite ist die Entwicklung von WebSockets tief in der Microsoft-Plattform verzahnt und hat schon heute einen festen Platz in der Windows Communication Foundation Strategie. Dies ist der Vorteil von WebSockets gegenüber SignalR.

SignalR wird vermutlich mittelfristig seinen berechtigten Platz im ASP.NET Open-Source finden.

Die neuen Push-Konzepte, egal ob SignalR oder WebSockets, sind sehr innovativ und bieten zweifellos eine Plattform für bisher schwer denkbare Szenarien an. Allerdings endet diese Reise hier bestimmt nicht. Heute wissen wir noch nicht, wie die herkömmliche Infrastruktur (Netzwerke, Router, LoadBalancer und Server) auf diese neue Art von Anwendungen wirklich reagieren wird.

Bitte vergessen Sie nicht, dass alle aufgezählten Komponenten für Request/Response konzipiert sind. Auch in diesem Anwendungsbereich gibt es noch ungelöste Probleme. Wie verschickt man beispielsweise einen Broadcast, wenn die Services auf mehreren Nodes im Cluster laufen?

Diesbezüglich gibt es schon Versuche mit dem Einsatz von ServiceBus Topics. Das ist jedoch ein Thema, dass den Rahmen dieses Artikels sprengen würde.

[1] WebSocket Spezifikation
http://websocket.org

[2] SignalR auf GitHub
https://github.com/​SignalR/​SignalR/​wiki/​Getting-Started

[3] OWIN Spezifikation
http://owin.org/spec.html

[4] PersistentConnections
https://github.com/SignalR/​SignalR/​wiki/​PersistentConnection

[5] JavaScript-Client und PersistentConnections
https://github.com/​SignalR/​SignalR/​wiki/​SignalR-JS-Client

Damir Dobric
  • Blog

Mehr zum Thema