Czym rózni sie aplikacja realtime od typowej, webowej aplikacji?

Rozwiazania realtime dostarczaja odbiorcy informacje w chwili pojawienia sie ich u zródla, w przeciwienstwie do zwyklych stron, które uzytkownik musi recznie odpytac, by sprawdzic, czy nie pojawily sie nowe dane. Przykladami aplikacji realtime lub prawie-realtime moga byc:

  • webowe komunikatory
  • narzedzia do wspólpracy przez web
  • newsfeedy
  • wykresy kursów akcji
  • itp.

Jak zaimplementowac taka usluge?

Niezaleznie czy ma to byc czat, czy wykresy kursów, architektura sieci web i przegladarki internetowe nie ulatwiaja tego zadania. Usluga, która chcemy zaimplementowac, jest typu PUSH (dane ‘wypychane’ do uzytkownika), natomiast narzedzia, które mamy do dyspozycji, oferuja model PULL (uzytkownik sam prosi o dane). Protokól uzywany do przesylania stron internetowych – HTTP – to protokól typu zadanie – odpowiedz, gdzie klient (przegladarka) jest zawsze inicjatorem polaczenia, czyli wysyla zadanie do serwera. Nie ma problemu, gdy to klient chce przeslac informacje w kierunku serwera (np. nadawca wiadomosci na czacie), lecz jak serwer ma poinformowac odbiorce – innego klienta – o nowych danych? Przeciez serwer HTTP nie moze inicjowac polaczen z klientem! Mozna by czekac, az odbiorca sam „odswiezy” strone, ale z pewnoscia nie bedzie to realtime. W historii internetu rozwiazan, kompromisów oraz obejsc tego problemu bylo wiele, m.in.:

  • odpytywanie
  • long polling
  • HTTP streaming
  • pushlet
  • Comet
  • wykorzystanie pluginów (Flash, Java) i niestandardowych polaczen TCP
  • WebSocket

Czesc z nich jedynie udaje model PUSH (czeste odpytywanie serwer o dane, np. co piec sekund, daje wrazenie prawie ‘realtime’), niektóre w sposób niestandardowy wykorzystuja protokól HTTP lub wymagaja niestandardowych rozwiazan po stronie serwera (Comet) czy klienta (Flash, Java). Technologia WebSocket daje nadzieje na prawdziwa dwukierunkowa komunikacje, jednak w tej chwili nie ma jeszcze zatwierdzonej specyfikacji i niewiele przegladarek ja implementuje. Problemem staje sie wybór techniki dzialajacej w jak najwiekszej ilosci srodowisk. W dodatku wiekszosc z nich wymaga pisania duzej ilosci kodu ‘boilerplate’.

Socket.io

Z pomoca przychodzi biblioteka Socket.IO, która po stronie serwera w polaczeniu z node.js oferuje serwer WebSocket, a po stronie klienta warstwe abstrakcji nad technikami HTTP PUSH – automatycznie wybiera najlepsza z nich dzialajaca w przegladarce klienta. Dzieki temu nie musimy martwic sie szczególami implementacji polaczenia klient-serwer, wystarczy kilka linijek JavaScript-u, by calosc zaczela dzialac. Dzialanie Socket.IO najlepiej poznac na przykladzie. Spróbujmy stworzyc prosta wspóldzielona tablice (whiteboard). Pierwszym krokiem bedzie przygotowanie srodowiska – node.js (http://nodejs.org).Node.js jest dostepny w formie instalatora na systemy Windows i Mac OS, a takze w postaci paczek dla popularnych dystrybucji Linuksa. Po zainstalowaniu node.js oprócz samego node’a dostajemy manager pakietów npm, którego wykorzystamy do instalacji Socket.IO:

> npm install socket.io

Warto pamietac: domyslnie npm zachowuje pakiety w aktualnym katalogu, nie sa one dostepne poza nim. Nastepnie zajmiemy sie uruchomieniem najprostszego serwera i klienta Socket.IO w node.js. W tym celu w katalogu, w którym jest zainstalowany socket.io, tworzymy plik .js, np. whiteboard.js:

var app = require('http').createServer(handler)
,fs = require('fs')
,io = require('socket.io').listen(app)

app.listen(8081);

function handler(req, res) {
	fs.readFile('whiteboard.html', function(err, data) {
		res.writeHead(200);
		res.end(data);
	});
}

I startujemy node.js w konsoli:

 node whiteboard.js

To wystarczy, by uruchomic w node.js prosty serwer http z Socket.IO na porcie 8081 i serwowac plik whiteboard.html. Teraz czas na klienta, whiteboard.html:


Otwarcie adresu http://localhost:8081 w przegladarce powinno zaowocowac nawiazaniem polaczenia z serwerem, o czym node.js poinformuje w konsoli:

debug – served static content /socket.io.js debug – client authorized info – handshake authorized 142595329646045365

Skoro polaczenie dziala, spróbujmy wyslac wiadomosc z klienta, odebrac ja na serwerze i przeslac z powrotem do klienta.

Kod whiteboard.html:


Socket.on() rejestruje handler wiadomosci o nazwie ‚testreply’, która spodziewamy sie otrzymac od serwera. Socket.emit() wysyla wiadomosc ‚testmessage’ z zawartoscia okreslona w drugim parametrze.

whiteboard.js:

var app = require('http').createServer(handler)
,fs = require('fs')
,io = require('socket.io').listen(app)

app.listen(8081);

function handler(req, res) {
	fs.readFile('whiteboard.html', function(err, data) {
		res.writeHead(200);
		res.end(data);
	});
}

io.sockets.on('connection', function(socket) {
	socket.on('draw', function(data) {
		socket.broadcast.emit('draw', data);
	});
});

W tym kodzie rejestrowany jest event-handler dla zdarzenia ‘connection’, wywolywany gdy z serwerem polaczy sie nowy klient. W parametrze event-handlera przekazywany jest socket stworzony przy polaczeniu, na którym to z kolei rejestrujemy event-handler obslugujacy otrzymanie wiadomosci o nazwie ‘testmessage’.

Po restarcie node.js i uruchomieniu strony w przegladarce powinien wyswietlic sie prompt, po wypelnieniu którego pokaze sie alert z odpowiedzia przeslana przez serwer.

Po przetestowaniu dwustronnej komunikacji, pozostaje zajac sie tablica. W przykladzie skorzystamy z biblioteki gee.js (http://georgealways.github.com/gee/), która uprosci kwestie obslugi myszy i rysowania w canvasie.

whiteboard.html:



Jak widac, w handlerze ‚mousedrag’ wysylana jest do serwera wiadomosc ‚draw’ zawierajaca wspólrzedne narysowanej linii, natomiast socket.on() obsluguje zdarzenie nadejscia wiadomosci ‚draw’ z serwera – rysuje linie z otrzymanymi wspólrzednymi.

Po stronie serwera wystarczy jedynie odebrac wiadomosc ‚draw’ i przekazac dalej do pozostalych klientów – do czego posluzy socket.broadcast.emit().

var app = require('http').createServer(handler)
,fs = require('fs')
,io = require('socket.io').listen(app)

app.listen(8081);

function handler(req, res) {
    fs.readFile('whiteboard.html', function(err, data) {
        res.writeHead(200);
        res.end(data);
    });
}

io.sockets.on('connection', function(socket) {
    socket.on('draw', function(data) { socket.broadcast.emit('draw', data); });
});

Oto wynik dzialania aplikacji: