ugrás a tartalomhoz

Felületfrissítés Node.js-sel websocketen keresztül

tihi · 2012. Feb. 24. (P), 17.31

Szeretném veletek megosztani a Node.js-sel és websockettel szerzett tapasztalataimat. Olyan rendszert szerettem volna készíteni, ahol a felület interaktívan frissül, mint a Facebooké üzenet érkezésekor.

Elkészítettem hát a saját megoldásomat, ami kezdetben egy setInterval()-lal meghívott AJAX hívással valósult meg. Sajnos hamar be kellett lássam, hogy ha a webszervert és az adatbázist nem szeretném feleslegesen halálra terhelni, akkor ki kell találjak valami más megoldást. Az első és egyben legegyszerűbb a setInterval() kellően nagy értékre való állítása lett volna, de így a felhasználói élmény romlott volna.

Az elmúlt hetekben ismerkedtem meg a Node.js-sel. Webfejlesztőként viszonylag kis energia befektetés volt szükséges a használatának elsajátításához, hiszen a JavaScript szintakszisa és logikája már ismert számomra. (Gondolom sokunk van még így ezzel.) Alacsony memóriaigénye van, gyors, pont megfelel a célra. Először arra gondoltam, az AJAX kéréseket Node-dal szolgálom ki, így spórolok egy kis erőforrást, de végül arra jutottam, jobb ha a kapcsolódási idő és a setInerval() miatti késleltetést is elkerülöm.

A long polling és a websocket közül én az utóbbi megoldást választottam, ez volt a szimpatikusabb, egyszerűbb, azonban egy probléma felmerült, mégpedig a támogatottság kérdése, ugyanis nem mindegyik böngésző implementálta még a websocketet. Hosszas mérlegelés után arra jutottam, hogy ha egy böngésző nem támogatja, akkor nem lesz azonnali a frissítés, csak következő oldal betöltéskor változnak az adatok. Mivel egy asztali programot vált ki az alkalmazás, ezért úgy gondoltam, tehetek kivételt.

A kliens oldali kód:

var connection = null;

window.WebSocket = window.WebSocket || window.MozWebSocket;

if (!window.WebSocket) {
    return false;
}

connection = new WebSocket("ws://127.0.0.1:8000");

connection.onopen = function () {
    console.log("Connection established");
};

connection.onerror = function (error) {
    console.log("An error occured");
    console.log(error);
};

connection.onmessage = function (message) {
    try {
        console.log(message.data);
        var json = JSON.parse(message.data);
        
        …
        
    } catch (e) {
        console.log("This does not look like valid JSON: ", message.data);
        return;
    }
};

var errorIntervalCh = setInterval(function () {
    if (connection.readyState !== 1) {
        console.log("Unable to communicate to websocket.");
        clearInterval(errorIntervalCh);
    }
}, 3000);

A szerveroldali kód:

var http = require("http").createServer(function (request, response) {});
var ws   = new (require("websocket").server)({httpServer: http});

var port = 8000;
var clients = new Array();

http.listen(port, function () {});
console.log("Websocket server is running on " + port);

process.on("SIGHUP", function () {
    console.log("Sighup event triggered");
    
    …
    
    for (var i in clients) {
        clients[i].sendUTF(JSON.stringify({ … }));
    }
});

ws.on("request", function (request) {
    var connection = request.accept(null, request.origin);
    
    console.log("User connected");
    
    …
    
    clients.push(connection);
    
    connection.on("message", function(message) { … });
    
    connection.on("close", function (connection) {
        clients.splice(clients.indexOf(connection), 1);
        console.log("User disconnected");
    });
});

A következő probléma az adatbázis-terhelés volt: jó lenne csak akkor lekérdezni, ha történt az adatbázisba írás, ilyenkor kellene valami, ami arra készteti a Node-ot, hogy nézze meg, van-e mit küldeni. Ezekre MySQL-ben remek triggereket lehet írni, de a Node ettől még nem tudja, hogy ellenőrizni kellene.

Az egyik nagy terheléses cikkben olvastam, hogy szignállal fel lehet kelteni egy démont. Pont ez kell nekem, egy SIGHUP segítségével megkérjük a Node-ot, hogy ébredjen fel, és tegye a dolgát. Már csak valahogy meg kellene szólaltatni a szignált. A MySQL ad lehetőséget saját, C-ben írt függvény definiálására, de én nem szerettem volna ilyet írni, szerintem más sem. Egy egyszerű exec() kellett, amire találtam is egy kész megoldást. A plugin jól működik, azonban biztonsági kockázatot rejt magában: bárki számára elérhető lesz az operációs rendszer! Ha többen használják a MySQL szervert, érdemes saját plugint írni erre a célra.

delimiter $$
CREATE TRIGGER nodetrigger
AFTER INSERT ON message FOR EACH ROW
BEGIN
SET @S = (SELECT sys_exec('kill -HUP `pidof node`'));
END$$
delimiter ;

Már csak egy probléma maradt, a jogosultság. Ahhoz hogy mysql felhasználó SIGHUP-ot küldhessen a Node-nak, a Node szerverünket a mysql felhasználóval kell futtassuk:

sudo -u mysql node websocket.salvus.node.js

A dolog még teszt fázisban van, de szépen teszi a dolgát. Sokat gondolkoztam, hogy van-e egyszerűbb, jobb megoldás, esetleg van-e valamilyen rejtett biztonsági kockázat a dologban, de eddig nem sikerült találnom.

 
1

socket.io

Poetro · 2012. Feb. 24. (P), 20.02
Miért nem Socket.IO alapokon valósítottad meg? Akkor egy lenne az interfész mindkét oldalon, és működne long polling és websocket esetén is, vagy éppen amit a böngésző támogat.
3

Nem ismertem.

tihi · 2012. Feb. 24. (P), 20.34
Socket.IO -t nem ismertem, de jónak tűnik. Köszi a tippet. :)
2

Server sent events

presidento · 2012. Feb. 24. (P), 20.27
Miért nem server-sent DOM events-et (Eventsource) használtál? Ezt jelenleg több böngésző támogatja, gyakorlatilag a long polling van megvalósítva a böngésző által. Ráadásul te nem is küldtél adatot a szervernek, csak fogadtál, pont erre találták ki. :)
4

Nem találkoztam még ezzel.

tihi · 2012. Feb. 24. (P), 20.38
Sajnos nem találkoztam, még ezzel a megoldással. Köszi, megnézem. :)

Igazából még nem tudom, hogy milyen irányba megy el a későbbiekben a fejlesztés, így előfordulhat, hogy szükség lesz a két irányú kommunikációra a jövőben.
5

Érdekes

Bártházi András · 2012. Feb. 24. (P), 21.44
Érdekes irányból közelítetted meg a dolgot, tetszik, már régóta tervezem valami ilyesmi kipróbálását. A MySQL-es megoldásod nem rossz, de valami csak beszúrja azt az üzenetet, az is küldhetné az értesítést a Node.JS script felé, akár HTTP interfészen, akár más megoldáson keresztül.

Itt van még egyébként egy friss bejegyzés a témakörben: http://www.rabbitmq.com/blog/2012/02/23/how-to-compose-apps-using-websockets/
10

Php -val a SIGHUP küldése

tihi · 2012. Feb. 25. (Szo), 10.15
Php -val a SIGHUP küldése jóval egyszerűbb lett volna valóban, ez nem jutott eszembe. Mysql táblánként csak 1 triggert támogat, így azokat is át kellett írjam. Talán azt tartottam szem előtt, hogy minél gyorsabb legyen az alkalmazás.
6

Tetszik a cikk, szépen

inf · 2012. Feb. 24. (P), 22.38
Tetszik a cikk, szépen kódolsz, érthető, hogy mi történik, bár egyik technológiát (nodejs, websocket) sem ismertem előtte...
7

Mi van akkor, ha a triggert

carstepPCE · 2012. Feb. 24. (P), 23.44
Mi van akkor, ha a triggert felhasználóra korlátozod, így kevésbé rejt kockázatot, csak aki jogosult használni a triggert az futtatja a SIGHUP-t.
11

A biztonsági rést a

tihi · 2012. Feb. 25. (Szo), 10.19
A biztonsági rést a sys_exec() függvény okozza. Sajnos a függvény nem korlátozható felhasználóra, mindenki számára elérhető.
select sys_exec('');
8

rossz link!

Arnold Layne · 2012. Feb. 25. (Szo), 00.46
A második link (long polling és a websocket) végén ott maradt egy ] karakter.
9

Javítottam, kösz.

Joó Ádám · 2012. Feb. 25. (Szo), 04.54
Javítottam, kösz.
12

NowJS?

megant · 2012. Feb. 28. (K), 13.17
Ezt láttátok már?

http://nowjs.com/
13

Ilyesmi kellene nekem is

zzrek · 2012. Feb. 29. (Sze), 18.37
Én is gondolkodom egy ilyesmiben, szívesen fogadnék tanácsot.
Egy egyszerű shared szerverrel, php-ben szeretném megoldani a dolgot.
A websocket általában megoldható ilyen tárhelyeken, vagy ez valamilyen speciális beállítást igényel, amit általában nem adnak meg?
A long polling is ezért nem jó az én esetemben, mert az ilyen egyszerű tárhelyeken korlátozva van az egyszerre megnyitott kapcsolatok száma.

Szóval szerintetek van esélyem rá, hogy JS-PHP-vel websocketezzek olcsó tárhelyen?
14

Nem tudom

Poetro · 2012. Feb. 29. (Sze), 18.48
Hát nem tudom... A PHP-t nem igazán ilyenfajta használatra találták ki (mármint websocket), de azért, ahogy látom meg lehet benne valósítani. Bár ebben az esetben semmiben nem különbözik a long-polling-tól, elvégre mindkettő pontosan egy kapcsolatot tart nyitva. És ebben az esetben szerintem inkább a terhelés miatt fognak rádszólni, amit a PHP fog generálni (mondjuk nem csináltam teszteket), mivel a websocket használat valahogy így néz ki:
while(true){
  $changed = $sockets;
  socket_select($changed,$write=NULL,$except=NULL,NULL);
  foreach($changed as $socket){
    if($socket==$master){
      $client=socket_accept($master);
      if($client<0){ console("socket_accept() failed"); continue; }
      else{ connect($client); }
    }
    else{
      $bytes = @socket_recv($socket,$buffer,2048,0);
      if($bytes==0){ disconnect($socket); }
      else{
        $user = getuserbysocket($socket);
        if(!$user->handshake){ dohandshake($user,$buffer); }
        else{ process($user,$buffer); }
      }
    }
  }
}
15

Hmm, köszönöm

zzrek · 2012. Feb. 29. (Sze), 19.03
Értem, szóval kapcsolatonként egy php szál is fut állandóan???
Gondolom, a példádban a ciklusban lévő forrásokat nem lehet lementeni (serializálni), aztán ha kell, felhasználni (csak bizonyos esemény esetén vizsgálni a socket állapotát)

Nyilván ez így felejtős.

Egyéb push technika nincsen, ami php-vel megoldható?
16

Egyetlen

Poetro · 2012. Feb. 29. (Sze), 19.14
A websocket szerver annyiban jobb, mint egy long-polling, hogy websocket esetén összesen egyetlen szál fut, míg long-polling esetén kliensenként egy szál. Viszont websocketnek van egy nagy hátránya, hogy nem támogatja minden böngésző, illetve van már jó pár verziója, és lehet hogy a böngésző még egy régebbi protokollt támogat, mint amit te szolgálsz, illetve fordítva. Azért is ajánlottam például a Socket.IO használatát Node.js alatt, mivel az fel van készítve a protokollok közötti különbségre, valamint nem csak websockettel működik, hanem több protokollt is képes használni a kliens képességeitől függően. Nem tudom, mire szeretnéd használni, de a websocket PHP-ben való futtatását a szolgáltató egy idő után biztosan nem nézi majd jó szemmel (mivel annak végtelenül kell futnia, mint egyfajta démon).
17

OK, még kérdeznék

zzrek · 2012. Feb. 29. (Sze), 19.33
Értem szóval egy szál fut állandóan (ez is túl sok ;-)

Még kérdeznék gyorsan...

Előszöris egy költői kérdés:
...és Node.js-ben miért nem fut egy szál állandóan? Mert az egy linux-os daemontól kapja az eseményt? Ezt miért nem oldották meg PHP-ben?

Elvileg egy push technika esetén nem is lenne muszáj, hogy a szerver állandóan figyeljen, ilyenkor inkább a kliensnek kéne figyelnie. Rosszul gondolom?

És most a lényegi kérdésem: a long pollingnál nyitva marad a http minden kliensnél, ugyanígy nyitva marad a php websocket esetében is?
Mi lenne, ha azt csinálnám, hogy a kommunikációigényes időszakban (amikor a kliens gyors választ vár) odaszólnék a szervernek, hogy most indítsd el légyszi pár másodpercre a websocket szervert (ha még nem fut) aztán az adatok érkezése után azt mondanám, hogy "köszi, felőlem becsukhatjátok" és ha épp senki sem kéri, akkor nem fut...
Lehet, hogy így kevesebb erőforrást pazarolnék, mint long pollinggal, vagy pár másodpercenkénti ajax lekéréssel, nem?

Egyébként itt találtam egy websocket kliens negvalósítást, ami graceful degradálódik (még nem néztem át tüzetesen, de réginek tűnik sajnos)

Graceful WebSocket
19

Egy szálon

Poetro · 2012. Feb. 29. (Sze), 21.03
A Node.js egy szálon fut, ha olvastad az eddigi cikkeimet róla, illetve megnézted valamelyik előadásom a témában azokban ezt már elmondtam. És igen a Node.js alkalmazás általában egyfajta démonként fut, azaz folyamatosan, megszakítás nélkül. Csak mivel eseményvezérelt, ezért a várakozást nem kell egy while vagy más hasonló ciklusba tenni (ahogy a böngészőben sem figyel az eseménykezelőd ciklusban). Ezzel szemben a PHP nem tud másként viselkedni, mivel nem eseményvezérelt, ezért neki időről-időre rá kell néznie, hogy nem esett-e be egy újabb kapcsolat. Ugyanez Node.js esetén pedig úgy működik, hogy raksz egy eseménykezelőt a kapcsolódásra, és az akkor fog meghívódni, amikor bekövetkezik a kapcsolódás (hasonlóan a böngészőbeli onclick-hez).

A másik dolog, amiért alkalmasabb erre a feladatra a Node.js, hogy jóval kevesebb memóriát fogyaszt. A fenti alkalmazás teljes egészében 10-12Mb memóriából megoldaná a feladatot, valamint ebből szinte korlátlan mennyiségű klienst is ki tudna szolgálni.

És most a lényegi kérdésem: a long pollingnál nyitva marad a http minden kliensnél, ugyanígy nyitva marad a php websocket esetében is?

A long-polling kb. a következőképp működik. A kliens csinál egy hagyományos AJAX kérést a szerver felé, a szerver fogadja a kérést, de nem ad vissza választ (a HTTP fejléceken kívül), majd a szerver figyel, hogy bekövetkezett-e már az az esemény, amire a kliens vár. Ha ez bekövetkezett, akkor visszaadja a válasz maradék részét is, ezzel le is záródik a kapcsolat. Ugye egészen eddig a kliens arra várt, hogy megjöjjön az adat a szervertől. A böngészők egy ideig ezért nyitva tartják a HTTP kapcsolatot, várva, hogy megjöjjön az adat. Ha bontódott a kapcsolat, mert vagy megjött a teljes válasz, vagy timeout miatt, akkor a kliens csinál egy újabb AJAX kérést stb.
Ennek az az előnye, hogy nem kell például másodpercenként újabb és újabb kérést intézni a szerverhez, mivel a kapcsolat már nyitva van, csak még nem érkezett meg a válasz. Ezzel megspóroljuk a HTTP kapcsolat gyakori kiépítését, valamint a szervert is részben tehermentesítjük (persze ez a szerver architektúrájától függ, hogy mennyiben lehetséges).

PHP alatt a sleep lehetséges megoldás lehet, annak viszont az a baja, hogy másodpercben lehet csak megadni, az pedig szerintem túl hosszú. Érdemes helyette usleep-et vagy time_nanosleep használni, mivel azok sokkal rugalmasabbak.

Én kliens oldalon szintén a Socket.IO-t ajánlom, mivel azt rendesen karbantartják, jól konfigurálható, és rengeteg szolgáltatást nyújt.
20

Kezd érdekes lenni

zzrek · 2012. Feb. 29. (Sze), 22.01
Köszönöm a választ!

Lehet, hogy egy kicsit félreértettél, a long pollingot (comet) ismerem, csak arra lettem volna kíváncsi, hogy a websocket esetében is egy nyitvatartott csatorna van-e minden kliensnél (amit a szerver szolgáltatók nem szeretnek és korlátoznak), vagy ez egy újraépülő kapcsolat-e. Mert ha a kapcsolat állandó, akkor nem igazán van értelme a websocketnek a long polling mellett.

A Node.js ok, hogy eseményvezérelt, de csak azért tudja így végezni a feladatát, mert van mögötte egy technikai háttér, egy szál, ami mindig figyel (mondjuk maga a node.js indít egy démont/szervízt, vagy az oprendszer egy már erre a célra való alacsonyabb szintű TCP/UDP funkcióját konfigurálja, hogy a várt eseménykor a kért szálat indítsa el), amit a php is megtehetne (annak ellenére, hogy alapvetően nem olyan felépítésű). Ha a PHP le tudja kezelni a beérkező sima HTTP kéréseket (mint eseményeket) akkor meg tudhatná oldani ezt a (z egyéb) socket forgalom esetében is. Itt jön a képbe az is, hogy a push eseményeket pedig elég lenne kiváltani valamivel, nem kéne folyamatos figyelés sem.
Arra akartam kilyukadni, hogy ha elterjed a websocket a böngészőkben, akkor a PHP-nek is egy erőforráskímélőbb megoldást kell találnia, különben szépen eltűnik a süllyesztőben.

A Socket.IO-hoz nem találtam "hivatalos" PHP oldali megoldást; amit linkeltél, azzal el lehet indulni?
(Csak akkor foglalkoznék vele, ha a PHP megoldású websocket nem hagy nyitva folyamatos kapcsolatot, mert egyébként nem jobb, mint a long polling. Viszont ha nem hagy nyitva, akkor nekem a sleep-es megoldás is jó, akár több másodperces késleltetéssel is -- a lényeg, hogy ne kelljen mindig fölösleges kéréseket elindítgatni, és ne legyen folyamatosan nyitott kapcsolat minden felhasználónál)

Bocs, visszaolvastam, és erre már válaszoltál, PHP esetén olyan, mint a long polling, vagyis kapcsolatonként egy szál fenn van tartva.... Vagyis nem jó.
Ez így van a Node.js-nél is?
21

Folyamatos

Poetro · 2012. Feb. 29. (Sze), 22.07
Annyival jobb a websocket, hogy a kapcsolat folyamatos, és egyetlen websocket szerver ki tudja szolgálni az összes hozzá kapcsolódó klienst. Így az összes kliensnek csak egyszer kell kiépíteni a HTTP/TCP kapcsolatot, onnantól kezdve az él, maximum akkor kell újraéleszteni, ha valami technikai probléma miatt megszakad a kapcsolat. Ezek után a szerver kezeli a kapcsolatokat, és a kliensek között küldözgethet adatokat teljesen push alapon. Ennek a websocket szervernek természetesen illik folyamatosan futnia, hogy a kliensek tudjanak hozzá kapcsolódni, és például egymásnak üzeneteket küldeni.

Tehát összefoglalva: websocket esetén egyetlen szerver szál fut, amihez a kliensek mindegyike kapcsolódik. Ez a szál folyamatosan fut, hogy lehessen hozzá kapcsolódni, de minden kliens ugyanahhoz a szálhoz kapcsolódik.
22

Áhá

zzrek · 2012. Feb. 29. (Sze), 22.47
Áhá, szóval a websocket szervernek azért kell folyamatosan futnia, hogy kapcsolódni tudjanak hozzá (és nem elsősorban azért, mert különben bontódna a kapcsolat).
A kliensek ha akarnak valamit, akkor küldhetik nem websockettel is, sima http-vel. Emiatt nem kell feltétlenül futnia állandóan a websocket szervernek.
Pl. előre elküldöm xmlhttpreqesttel, hogy most websocketezni akarok, a szerver elindul (ha még nem fut), rákonnektálok, aztán majd jelzem, hogyha nem kell. Aztán ha senkinek sem kell, akkor nem fut.
Annakidején winsockettel csináltam adatátvitelt, úgy emlékszem, hogy UDP-vel nincs folyamatos kapcsolat, (csakhát a kapcsolati erőforrásokat meg kell őrizni). Ha ilyen megoldható lenne PHP-vel, akkor nem kellene kliensenként nyitva tartani a kapcsolatot, csak egy pusht küldenék a kliensnek.
Gondolom ez nem lehetséges.

Mindegy, marad a sima xmlhttpreq.
Köszi az infókat!
24

Ahhoz, hogy pusholni tudjál a

inf · 2013. Nov. 26. (K), 02.16
Ahhoz, hogy pusholni tudjál a kliens-nek http-n keresztül, neki is egy http szervernek kellene lennie, amire a szerver fel tud kapcsolódni... A http kérés-válasz alapú protokoll, nem esemény alapokon működik, mint pl a websocket, szóval az ilyesmi esélytelen vele. Comet technikákkal lehet tákolni rajta valamit, de soha nem lesz az igazi...
25

A long polling szerintem egy

Joó Ádám · 2013. Nov. 26. (K), 17.08
A long polling szerintem egy egészen elegáns megoldás.
18

még egy kérdés

zzrek · 2012. Feb. 29. (Sze), 19.38
Bocsi, nem akarlak kimeríteni...

A fenti példádban az erőforrás állandóan (folyamatosan) vizsgálva van.

Mi lenne, ha a ciklusba tennék egy sleep-et?
Gondolom a státuszváltozásnak van egy timeoutja, vagyis egy megfelelően megválasztott pihentetés mellett még működne a dolog, nem?
23

Imho lehetne hibrid rendszert

inf · 2013. Nov. 26. (K), 02.07
Imho lehetne hibrid rendszert is használni ilyen célra. Az CRUD kéréseket meg lehet csinálni PHP-vel is, egyedül a felhasználó felé irányuló push a gond, arra van szükség nodejs-re. Lehetne PHP-vel hozzácsapni új chat üzentet az adatbázishoz, és ha sikerült, akkor PHP-vel ki lehetne váltani egy eseményt, amit átküld nodejs-nek, hogy küldje tovább a klienseknek. Így az amúgy szinkron dolgokat nem kellene nodejs-ben tök feleslegesen aszinkron leprogramozni... X másodperces http-s frissítést így is be lehetne vinni a rendszerbe a websocket támogatás hiányakor minimális erőfeszítéssel...