09 December 2013 Azure, HowTo, SignalR, Websockets Robert Muehsig

SignalR ist ein Open Source Framework für Real Time WebApps. Das größte Problem am Real-Time im Web ist der Kanal zwischen Browser und Server. Wer noch nie was mit SignalR und den Problemstellungen zu tun hatte, hier ein paar Eckpunkte:

Das Problem

Traditionell initiert der Browser den Request zum Server und der Server schickt eine Antwort. Wenn jetzt allerdings der Server Daten zum Browser schicken möchte sind die Möglichkeiten eingeschränkt.

Die “Problemlösungen”

Die älteste Variante ist über “Comet”, darunter versteht man eine Ansammlung von Tricks. So wird zum Beispiel ein Request vom Client initiiert und erst geschlossen falls der Server Daten sendet. Grundsätzlich bleibt die Verbindung hier “ständig” offen. Diese Variante ist aber nicht wirklich elegant.

Moderner geht es über so genannte “Server Sent Events”, welche von fast allen Browsern unterstützt werden – außer dem IE. Ältere IEs müssen auf die alte Variante zurückgreifen.

Die “modernste” Variante sind so genannte WebSockets. Hierbei eine TCP Connection zwischen Server und Client aufgebaut und eine bidirektionale wird aufgebaut.

Das “Problem” bei WebSockets

WebSockets müssen nicht nur im Browser unterstützt werden, sondern auch vom Server. Im IIS selbst sind Websockets erst ab IIS 8.0 offiziell unterstützt. Das heisst nur ab Windows Server 2012. Davor gibt es in der IIS Pipeline keinen Weg das zu aktivieren.

Auf Browser Seite gibt es die Websockets-Unterstützt seit IE 10. Chome und Firefox unterstüten Websockets schon länger und alle modernen Browser haben das Feature implementiert.

SignalR – Real Time für alle

Da es verschiedene “Protokoll-Arten” gibt und die Unterstützung sowohl vom Client als auch Server abhängig ist, ist es nicht ganz trivial Apps zu bauen. Hier kommt nun SignalR zum Zuge. SignalR baut automatisch die “best-mögliche” Verbindung auf und zudem bringt es ein sehr beeindruckendes Programmiermodell mit. SignalR selbst ist Open Source und der Code steht auf GitHub bereit. Trotzdem bekommt man (wenn man darauf angewiesen ist) vollen Support von Microsoft.

SignalR DemoHub

Das Beispiel stammt aus dem GitHub Account:

   1: public class DemoHub : Hub
   2:     {
   3:         public override Task OnConnected()
   4:         {
   5:             return Clients.All.hubMessage("OnConnected " + Context.ConnectionId);
   6:         }
   7:  
   8:         public override Task OnDisconnected()
   9:         {
  10:             return Clients.All.hubMessage("OnDisconnected " + Context.ConnectionId);
  11:         }
  12:  
  13:         public override Task OnReconnected()
  14:         {
  15:             return Clients.Caller.hubMessage("OnReconnected");
  16:         }
  17:  
  18:         public void SendToMe(string value)
  19:         {
  20:             Clients.Caller.hubMessage(value);
  21:         }
  22:  
  23:         public void SendToConnectionId(string connectionId, string value)
  24:         {
  25:             Clients.Client(connectionId).hubMessage(value);
  26:         }
  27:  
  28:         public void SendToAll(string value)
  29:         {
  30:             Clients.All.hubMessage(value);
  31:         }
  32:  
  33:         public void SendToGroup(string groupName, string value)
  34:         {
  35:             Clients.Group(groupName).hubMessage(value);
  36:         }
  37:  
  38:         public void JoinGroup(string groupName, string connectionId)
  39:         {
  40:             if (string.IsNullOrEmpty(connectionId))
  41:             {
  42:                 connectionId = Context.ConnectionId;    
  43:             }
  44:             
  45:             Groups.Add(connectionId, groupName);
  46:             Clients.All.hubMessage(connectionId + " joined group " + groupName);
  47:         }
  48:  
  49:         public void LeaveGroup(string groupName, string connectionId)
  50:         {
  51:             if (string.IsNullOrEmpty(connectionId))
  52:             {
  53:                 connectionId = Context.ConnectionId;
  54:             }
  55:             
  56:             Groups.Remove(connectionId, groupName);
  57:             Clients.All.hubMessage(connectionId + " left group " + groupName);
  58:         }
  59:  
  60:         public void IncrementClientVariable()
  61:         {
  62:             Clients.Caller.counter = Clients.Caller.counter + 1;
  63:             Clients.Caller.hubMessage("Incremented counter to " + Clients.Caller.counter);
  64:         }
  65:  
  66:         public void ThrowOnVoidMethod()
  67:         {
  68:             throw new InvalidOperationException("ThrowOnVoidMethod");
  69:         }
  70:  
  71:         public async Task ThrowOnTaskMethod()
  72:         {
  73:             await Task.Delay(TimeSpan.FromSeconds(1));
  74:             throw new InvalidOperationException("ThrowOnTaskMethod");
  75:         }
  76:  
  77:         public void ThrowHubException()
  78:         {
  79:             throw new HubException("ThrowHubException", new { Detail = "I can provide additional error information here!" });
  80:         }
  81:  
  82:         public void StartBackgroundThread()
  83:         {
  84:             BackgroundThread.Enabled = true;
  85:             BackgroundThread.SendOnPersistentConnection();
  86:             BackgroundThread.SendOnHub();
  87:         }
  88:  
  89:         public void StopBackgroundThread()
  90:         {
  91:             BackgroundThread.Enabled = false;            
  92:         }
  93:     }

Server-Seitig wird ein Hub definiert und per API kann man “Client-Funktionen” aufrufen – so z.B. “hubMessage”.

Diese Methode sind im Javascript definiert und SignalR sort für dessen Aufruf:

   1: function writeError(line) {
   2:     var messages = $("#messages");
   3:     messages.append("<li style='color:red;'>" + getTimeString() + ' ' + line + "</li>");
   4: }
   5:  
   6: function writeEvent(line) {
   7:     var messages = $("#messages");
   8:     messages.append("<li style='color:blue;'>" + getTimeString() + ' ' + line + "</li>");
   9: }
  10:  
  11: function writeLine(line) {
  12:     var messages = $("#messages");
  13:     messages.append("<li style='color:black;'>" + getTimeString() + ' ' + line + "</li>");
  14: }
  15:  
  16: function getTimeString() {
  17:     var currentTime = new Date();
  18:     return currentTime.toTimeString();
  19: }
  20:  
  21: function printState(state) {
  22:     var messages = $("#Messages");
  23:     return ["connecting", "connected", "reconnecting", state, "disconnected"][state];
  24: }
  25:  
  26: function getQueryVariable(variable) {
  27:     var query = window.location.search.substring(1),
  28:         vars = query.split("&"),
  29:         pair;
  30:     for (var i = 0; i < vars.length; i++) {
  31:         pair = vars[i].split("=");
  32:         if (pair[0] == variable) {
  33:             return unescape(pair[1]);
  34:         }
  35:     }
  36: }
  37:  
  38: $(function () {
  39:     var connection = $.connection.hub,
  40:         hub = $.connection.demoHub;
  41:  
  42:     connection.logging = true;
  43:  
  44:     connection.connectionSlow(function () {
  45:         writeEvent("connectionSlow");
  46:     });
  47:  
  48:     connection.disconnected(function () {
  49:         writeEvent("disconnected");
  50:     });
  51:  
  52:     connection.error(function (error) {
  53:         writeError(error);
  54:     });
  55:  
  56:     connection.reconnected(function () {
  57:         writeEvent("reconnected");
  58:     });
  59:  
  60:     connection.reconnecting(function () {
  61:         writeEvent("reconnecting");
  62:     });
  63:  
  64:     connection.starting(function () {
  65:         writeEvent("starting");
  66:     });
  67:  
  68:     connection.stateChanged(function (state) {
  69:         writeEvent("stateChanged " + printState(state.oldState) + " => " + printState(state.newState));
  70:         var buttonIcon = $("#startStopIcon");
  71:         var buttonText = $("#startStopText");
  72:         if (printState(state.newState) == "connected") {
  73:             buttonIcon.removeClass("glyphicon glyphicon-play");
  74:             buttonIcon.addClass("glyphicon glyphicon-stop");
  75:             buttonText.text("Stop Connection");
  76:         } else if (printState(state.newState) == "disconnected") {
  77:             buttonIcon.removeClass("glyphicon glyphicon-stop");
  78:             buttonIcon.addClass("glyphicon glyphicon-play");
  79:             buttonText.text("Start Connection");
  80:         }
  81:     });
  82:  
  83:     hub.client.hubMessage = function (data) {
  84:         writeLine("hubMessage: " + data);
  85:     }
  86:  
  87:     $("#startStop").click(function () {
  88:         if (printState(connection.state) == "connected") {
  89:             connection.stop();
  90:         } else if (printState(connection.state) == "disconnected") {
  91:             var activeTransport = getQueryVariable("transport") || "auto";
  92:             connection.start({ transport: activeTransport })
  93:             .done(function () {
  94:                 writeLine("connection started. Id=" + connection.id + ". Transport=" + connection.transport.name);
  95:             })
  96:             .fail(function (error) {
  97:                 writeError(error);
  98:             });
  99:         }
 100:     });
 101:  
 102:     $("#sendToMe").click(function () {
 103:         hub.server.sendToMe($("#message").val());
 104:     });
 105:  
 106:     $("#sendToConnectionId").click(function () {
 107:         hub.server.sendToConnectionId($("#connectionId").val(), $("#message").val());
 108:     });
 109:  
 110:     $("#sendBroadcast").click(function () {
 111:         hub.server.sendToAll($("#message").val());
 112:     });
 113:  
 114:     $("#sendToGroup").click(function () {
 115:         hub.server.sendToGroup($("#groupName").val(), $("#message").val());
 116:     });
 117:  
 118:     $("#joinGroup").click(function () {
 119:         hub.server.joinGroup($("#groupName").val(), $("#connectionId").val());
 120:     });
 121:  
 122:     $("#leaveGroup").click(function () {
 123:         hub.server.leaveGroup($("#groupName").val(), $("#connectionId").val());
 124:     });
 125:  
 126:     $("#clientVariable").click(function () {
 127:         if (!hub.state.counter) {
 128:             hub.state.counter = 0;
 129:         }
 130:         hub.server.incrementClientVariable();
 131:     });
 132:  
 133:     $("#throwOnVoidMethod").click(function () {
 134:         hub.server.throwOnVoidMethod()
 135:         .done(function (value) {
 136:             writeLine(result);
 137:         })
 138:         .fail(function (error) {
 139:             writeError(error);
 140:         });
 141:     });
 142:  
 143:     $("#throwOnTaskMethod").click(function () {
 144:         hub.server.throwOnTaskMethod()
 145:         .done(function (value) {
 146:             writeLine(result);
 147:         })
 148:         .fail(function (error) {
 149:             writeError(error);
 150:         });
 151:     });
 152:  
 153:     $("#throwHubException").click(function () {
 154:         hub.server.throwHubException()
 155:         .done(function (value) {
 156:             writeLine(result);
 157:         })
 158:         .fail(function (error) {
 159:             writeError(error.message + "<pre>" + connection.json.stringify(error.data) + "</pre>");
 160:         });
 161:     });
 162: });

Was ist neu in SignalR 2.0?

Damian Edwards hat auf seinem GitHub Account ein kleines Demoprojekt angelegt:

image

Azure Websites & Websockets

Seit gut einem Monat gibt es die Websockets Unterstütztung in Azure Websites. Im Standardfall ist die Websocketsunterstützung deaktiviert. Die Einstellung kann man direkt im Azure Managementportal machen:

image

Wenn man da die SignalR Demoanwendung laufen lässt (ohne Websockets Support) sieht der Traffic so aus:

image

Mit Websocket Support:

image

Das tollen an SignalR: Der “Transportweg” ist erst einmal egal. Diese Sache übernimmt SignalR und man kann sich selbst auf die eigentliche Funktionalität konzentrieren.

SignalR Ressourcen

Wer sich weiter informieren möchte, hier einige Links die ich ganz interessant fand:

- ASP.NET SignalR Tutorial
- SignalR “JabbR” Raum – Chat (gebaut mit SignalR) wo meist die Entwickler selbst anzutreffen sind.
- SignalR Account auf GitHub

Das Video von den beiden SignalR-“Hauptentwicklern”  ist ziemlich beeindruckend:

David Fowler, Damian Edwards: Under the covers with ASP.NET SignalR

The real-time web is here. You’ve seen the demos before; synchronized moving shapes across browsers and Windows apps, but now you want to *really* understand what’s going on behind the curtain. What better way than to watch one of the SignalR co-creators build a SignalR-like framework from scratch on stage. Knowing how it works will help you use it better and might just prevent you making mistakes based on incorrect assumptions. Know your tools and learn the magic behind SignalR.


Written by Robert Muehsig

Software Developer - from Dresden, Germany, now living & working in Switzerland. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de

If you like the content and want to support me you could buy me a beer or a coffee via Litecoin or Bitcoin - thanks for reading!