TSM - Aplicații real-time folosind SignalR

Radu Vunvulea - Solution Architect

Trăim într-o lume dinamică, o lume în care datele zboară extrem de rapid. În această lume, aplicațiile web au devenit din ce în ce mai complexe. Zilele când aveam doar pagini web statice au trecut de mult, la fel și perioada în care Ajax și jQuery erau la putere.

Aplicații real-time

Într-o lume în care aplicațiile real time fac parte din viața noastră, avem nevoie de noi mecanisme pentru a putea face apeluri server2client. Aplicațiile web pentru monitorizare, jocurile online, aplicațiile bursiere sau cele în care edităm documente au nevoie de sisteme de acest fel, care să fie robuste și scalabile.

Web Sockets

HTML5 a adus cu el Web Sockets. O soluție perfectă care ne face viața mult mai ușoară. Acesta ne permite să ținem o conexiune deschisă între server și client prin care serverul poate să trimită date la clienți (chiar dacă aplicațiile rulează într-un browser). Un mecanism perfect pentru ceea ce avem nevoie.

Chiar dacă rata de adopție la HTML5 este bună, mai avem câțiva ani de așteptat până când vom avea o adopție de peste 90% la HTML5. Până la apariția WebSockets, pe piață au existat diferite soluții de genul Forever Frame, Server Send Events, Pooling sau SPDY. Până în acest moment niciuna dintre ele nu a fost adoptată în una limitată de către toate browser-ele.

De aceea au apărut diferite framework-uri care ne ajută în această zonă. Commet, Pusher, SockJSm Now.js sunt doar o parte din ele. Un framework care ne ajută să putem notifica clienții web este SignalR.

Ce este SignalR

SignalR este o librărie ce ne oferă o modalitate extrem de simplă pentru a putea avea o comunicare bidirecțională între client și server - real time. Aceasta este o soluție dedicată pentru cei care folosesc .NET. și rezolvă trei mari probleme pe care dezvoltatorii le întâmpinau:

Chiar dacă este susținut de Microsoft, acest framework este open source, putând fi găsit pe GitHub - fiind unul dintre cele mai urmărite proiecte de pe GitHub.

Unde putem să folosim SignalR

Acesta poate să ruleze pe sisteme care au ca backend Windows (.NET) și nu numai. Deoarece acesta poate să ruleze și pe Mono, putem să avem un sistem care rulează sub Linux și să folosească SignalR.

Clienții pe care îi putem avea sunt extrem de variați, începând de la browsere și terminând cu aplicații desktop, Silverlight, Windows Store, Windows Phone și IoS. Acesta poate să ruleze pe diferite browsere, chiar și pe cele pe care Web Sockets nu este suportat. Acest lucru este posibil datorită modului prin care SignalR comunică cu clienții.

Mecanisme de comunicare

Acesta suportă mai multe mecanisme de comunicare, iar în cazul în care observă că unul dintre ele nu este suportat de către browser (client) va face fallback automat la un alt mecanism. Metodele de comunicare suportate sunt:

Dacă un mecanism nu este suportat, se face fallback automat până când se ajunge la Ajax Long Pooling, care este suportat de toate browser-ele de azi. Datele sunt trimise la clienți în format JSON sau Plain Text. Trebuie ținut cont că această soluție nu este gândită să trimită date de dimensiuni mari - fișiere.

Notă: Singurul browser care nu suportă Server Side Event este Internet Explorer.

Base API

SignalR ne oferă două modalități de comunicare: Persistent Connection și Hubs.

Persistent Connection este foarte asemănătoare cu WebSockets, oferindu-ne o conexiune persistentă între client și server. Evenimentele și metodele disponibile în cazul Persistent Connection sunt aceleași pe care le avem la dispoziție și cu WebSockets - "Connect", "Disconnect", "Receive", "Error", "Send", "Broadcast".

Prin intermediul unei conexiuni de acest tip putem să facem broadcast la mesaje la toți clienții sau doar o parte din aceștia. Comparat cu WebSocket, avantajul folosirii Persistent Connection apare în momentul în care avem clienți care nu suportă WebSockets. Noi vom putea comunica cu ei, chiar dacă comunicarea se face prin Forever Frame sau Ajax. Modul în care se face comunicarea între client și server nu trebuie rezolvată de către dezvoltator.

Pentru a putea folosi un Persistent Connection este nevoie să extindem clasa "PersistentConnection" și să facem override la metodele de care noi avem nevoie. Două din cele mai importante metode din această clasă sunt "OnConnected" (care se apelează când un nou client se conectează) și "OnReceived" (apelată când un client trimite un mesaj). Mesajele se pot trimite la unul sau mai mulți clienți apelând metodele pe care proprietatea "Connection" ni le pune la dispoziție.

public class FooConnection:PersistentConnection 
{
 protected override Task OnConnected(IRequest request, string connectionId)
  {
   return Connection.Broadcast("We have a new 
    user: " + request.QueryString["nickname"]);
  }
  protected override Task OnReceived(IRequest request, string connectionId, string data)
  {
    return Connection.Broadcast(request.QueryString["nickname"] + ":" + data);
  }
}

Pe partea de client, odată ce clientul obține o referință la connection, poate să facă override la metode precum "received" sau "error". Este foarte important de reținut că nu este de ajuns acest lucru. Odată ce am făcut override la ele, este necesar să deschidem o conexiune cu serverul, apelând metoda "start".

var connection = $.connection("/echo", "name=" + nickname, true);;
connection.received(function (data) {
	...
});
connection.error(function (err) {
	...
});
connection.start(function () {
	...
});

Hub-urile pot să fie privite ca o abstractizare a Persistent Connection. API-ul care ne este pus la dispoziție prin hub-uri este mult mai simplu și mai ușor de folosit. Această abstractizare ne aduce o funcționalitate care nu este disponibilă dacă folosim Persistent Connection - RPC (Remote Procedure Calls). Da, ați auzit bine, hub-urile ne permit să facem acest lucru extrem de ușor.

Putem foarte ușor să apelăm metode client de pe server (chiar dacă sunt definite în JavaScript) sau clientul poate la fel de bine să apeleze metode care sunt pe server. Cred că acest lucru este unul din cele mai mari avantaje pe care hub-urile le aduc.

Pe server, dacă dorim să adăugăm un nou hub este nevoie să extindem clasa Hub. De această dată nu mai avem nevoie să facem override la nici o metodă. Trebuie doar să ne definim metodele pe care dorim să le expunem.

La hub-uri este foarte important să adăugăm atributul "HubName" pe clasa care definește hub-ul nostru. Acesta va specifica ce nume de clienți hub trebuie să folosească pentru a apela hub-ul nostru. Pentru a apela metodele pe care clientul le expune este nevoie să ne folosim de proprietatea "Client" care ne pune la dispoziție diferite metode să apelăm metodele expuse de client. Putem să facem atât apeluri la un anumit client cât și apeluri de tip broadcast. În aceste cazuri datele de tip dynamic ne sunt de mare ajutor.

[HubName("footballScore")]
public class ScoreHub : Hub
{
    public void Start(string matchId, string team1Name, string team2Name)
    {
        DateTime when = DateTime.Now;
        Clients.All.matchStart(matchId, team1Name, team2Name, when.ToShortTimeString());
    }

    public void Stop(string matchId)
    {
        DateTime when = DateTime.Now;
        Clients.All.matchEnded(matchId, when.ToShortTimeString());
    }<

    public void NewScore(string matchId, string team1Score, string team2Score, string playerName)
    {
        Clients.All.goal(matchId, team1Score, team2Score, playerName);
    }        
}

Fiecare client se identifică unic printr-un connection token. Managementul acestuia este făcut în întregime de server și semnat cu o semnătură digitală. Connection token există până la finalul conexiunii și este format din connection id și username. Dacă username-ul există doar în cazul în care clientul este autentificat, connection id-ul există din primul moment când o conexiune este stabilită între client și server.

Pe partea de client, lucrurile se simplifică. Odată ce avem o referința la hub-ul nostru, putem să definim metodele client pe care serverul le poate apela sau să scriem cod care apelează metodele expuse de către server. Înainte să începem să trimitem date sau să fim apelați este nevoie să apelăm $.connection.hub.start().

// server2client example
var footballScore = $.connection.footballScore;
$.extend(footballScore.client, {
    matchStart: function (matchId, team1Name, team2Name, when) {
	...
    },
    
    matchEnded: function (matchId, when) {
	...
    },
    
    goal: function (matchId, team1Score, team2Score, playerName) {
	...
    },
});
// client2Server example
var footballScore = $.connection.footballScore;
$.connection.hub.start()
    .done(function() {
        $("#Start").click(function() {
            footballScore.server.start($("#TeamId").val(), $("#Team1Name").val(), $("#Team2Name").val());
        });
        $("#Stop").click(function () {
            footballScore.server.stop($("#TeamId").val());
        });
        
        $("#Goal").click(function () {
            footballScore.server.newScore($("#TeamIdScore").val(), $("#Team1Score").val(), $("#Team2Score").val(), $("#Player").val());
        });
    });

De remarcat este faptul că dacă avem mai multe mai multe hub-uri, pe care serverul le expune, comunicare între clienți și server se va face pe câte un connection pentru fiecare client. Un client va folosi aceeași conexiune pentru a comunica cu două sau mai multe hub-uri de pe același server. Acest lucru este făcut deoarece se încearcă limitarea numărului de conexiune deschise între server și clienți. Folosirea unei singure conexiuni nu are nici o repercusiune din punct de vedere a performanței.

Hubs vs Persistent Connection

O întrebare destul de firească care poate să apară în acest moment este:

Când să folosesc un hub și când să folosesc persistent connection?

Răspunsul este destul de simplu. Hubs se recomandă să fie folosit în momentul în care avem nevoie de RPC. Pentru cazurile în care formatul mesajului trebuie specificat sau vrem să folosim un model de tipul messaging and dispatching atunci Persistent Connection este soluția recomandată. Totodată în cazul în care integrăm într-o aplicație deja existentă SignalR, atunci se recomandă să folosim Persistent Connection, migrarea spre SignalR fiind mult mai ușoară.

Securitate

Apelurile de tip CSRF (Cross-Site Request Forgery) sunt evitate prin următoarele mecanisme:

Din cauza ultimului punct menționat, dacă avem doua tab-uri deschise vom avea două connection token-uri diferite și automat doi clienți separați. Cu puțin custom code putem să trecem peste această limitare.

Performanță

Performanțele pe care SignalR le are sunt bune. Putem să avem peste 450.000 de mesaje manipulate de către un singur server, iar numărul de conexiuni pe o singură mașină pe care le putem avea este de 15.000-20.000. Numărul de conexiuni este limitat de un singur factor - numărul de porturi pe care le avem disponibile.

Scalabilitate

Scalabilitatea într-un astfel de sistem nu este foarte ușoară. Acest lucru se datorează problemei pe care SignalR o rezolvă. Deoarece face handling la mesaje, dacă scalăm cu încă o instanță și pune un load balancer în față la server, un apel de tip broadcast nu o să ajungă la toți clienți.

Pentru a putea rezolva această problemă trebuie să folosim un mecanism prin care un mesaj poate să fie trimis la toate nodurile din cluster. Acest lucru se face destul de ușor prin intermediul a trei soluții aplicabile în acest moment:

Folosirea unui astfel de serviciu este simplă, singurul lucru pe care trebuie să îl facem este să specificăm string-ul de conexiune. De exemplu integrarea cu Service Bus și sincronizarea între noduri se reduce la o singura linie de cod:

GlobalHost.DependencyResolver.UseServiceBus(sbConnectStrion, "codecampcluj");

Totodată nimic nu ne oprește să extindem modul în care nodurile se pot sincroniza, doar că este nevoie să scriem noi modalitatea de sincronizare. Dacă folosim mai multe mașini cu SignalR care se sincronizează între ele este bine de știu că latența în cazul unui broadcast crește ușor. Acest lucru se întâmplă din cauza că un mesaj odată ce ajunge la server trebuie să fie trimis și la restul serverelor.

O fermă formată din noduri cu SignalR poate să fie folosită cu succes dacă dorim să facem broadcast la mesaje, iar o latență de câteva milisecunde nu ne afectează. Aceasta nu este recomandă pentru comunicări de tip client2client sau high-frequency realtime deoarece latența poate să fie destul de mare, nefiind cea mai bună soluție de care noi avem nevoie.

Concluzie

Am văzut că SignalR este un framework care ne ajută să avem aplicații web care pot să comunice în ambele sensuri, fiind perfect pentru aplicații bursiere sau aplicații de monitorizare. Numărul de mesaje pe care un server cu SignalR le poate procesa este extrem de mare fiind o soluție ideală când avem nevoie să facem față la zeci de milioane de mesaje pe oră.

Vă invit să incercați să folosiți SignalR și să vedeți cât de simplu este.