Felületfrissítés Node.js-sel websocketen keresztül
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.
■
socket.io
Nem ismertem.
Server sent events
Nem találkoztam még ezzel.
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.
Érdekes
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/
Php -val a SIGHUP küldése
Tetszik a cikk, szépen
Mi van akkor, ha a triggert
A biztonsági rést a
rossz link!
Javítottam, kösz.
NowJS?
http://nowjs.com/
Ilyesmi kellene nekem is
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?
Nem tudom
Hmm, köszönöm
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ó?
Egyetlen
OK, még kérdeznék
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
Egy szálon
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őbelionclick
-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.
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.
Kezd érdekes lenni
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?
Folyamatos
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.
Áhá
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!
Ahhoz, hogy pusholni tudjál a
A long polling szerintem egy
még egy kérdés
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?
Imho lehetne hibrid rendszert