Sziasztok!
Galiba Péter vagyok, jelenleg az NowPublic.com és a Examiner.com oldalon dolgozok fejlesztőként illetve frontend fejlesztőként. Számos cikkemet megtaláljátok a Weblaboron, kódomat a GitHub-on.
Elöljáróban szeretném megkérdezni,
2009 januárjában Ryan Dahl publikálta ötletét, hogy szeretne készíteni egy aszinkron webszervert. Ehhez számos programozási nyelvet és rendszert megnézett (Python Twisted, Ruby EventMachine).
A 2009 áprilisában megtartott JSConf 2009 rendezvényen Ryan bemutatja a rendszert, ekkor már JavaScript alapokon a V8 rendszerre alapozva. Kezdeményezését nagy ováció és taps követte.
Ekkor rengeteg fejlesztő kezdte el a rendszert formálni, a GitHub-on hatalmas rajongótábora lett. Ugyan eléggé sokat kellett várni, de egy évvel később megjelent az első stabil változat a 0.2. Itt fontos megjegyezni, hogy a fejlesztői változatok a páratlan alverzió számmal rendelkezők, a stabil változatok a párosak, hasonlóan a Linux kernelhez.
Azóta a rendszer nagyon sokat fejlődött, az API-k nagy részét lecserélték egy áttekinthetőbb, korszerűbb, használhatóbb változatra. A 0.4.0, azaz a legutóbbi stabil változat 2011 február 10-én jött ki.
A Node.js a rendszert igen átfogó, ugyanakkor eléggé alacsony szintű rendszer. A rendszer magját C/C++ nyelveken fejlesztik, és több komponensből épül fel. Ezen komponensekről mindjárt szó is lesz.
A rendszert szerver oldali futásra tervezték, tehát a legtöbb JavaScript futási környezettel ellentétben nem a böngészőben fut, hanem a szerveren.
Mit jelent az aszinkron I/O? Azt hogy a rendszer majd minden bemenetet és kimenetet kezelő függvénye aszinkron működik. Ez azt takarja, hogy intézünk egy kérést például egy fájl beolvasására, és megadunk egy függvényt, amit a rendszer majd meghív, hogyha a beolvasás megtörtént, de eközben a program futása nem szakad meg. Azaz addig is tudunk további műveleteket végezni, amíg arra várunk, hogy a beolvasás megtörténjen. Ez nagyon hasonlít a böngészőbeli AJAX műveletekhez.
Az egyes aszinkron folyamatok kiválthatnak eseményeket, ezekre feliratkozhatunk, majd kezelhetjük, hogy mi történjen, amikor az esemény bekövetkezik. Ez igen hasonló a böngésző béli különböző egér és billentyűzet események kezeléséhez.
A Node.js alkalmazásunkat elsődlegesen JavaScriptben írjuk, de van lehetőség C++ modulok használatára is.
A modulok betöltése, azok tulajdonságainak, metódusainak elérése a CommonJS alapokra épül.
A rendszer jelenleg 4 fontos építőelemből áll. A Google V8 JavaScript motorja adja a JavaScript futtatási környezetet. A libuv nevű függvénytár gondoskodik az aszinkron I/O és események kezeléséért. A HTTP Parser Ryan saját fejlesztése, és a HTTP 1.1 üzenetek feldolgozásáért felelős, az OpenSSL pedig a kódolásért.
A belső komponenseket a következő kockákon leírom, de idő hiányában nem szeretnék részletekbe bocsátkozni velük kapcsolatban.
A rendszer fejlesztői változata előre fordított binárisként elérhető Windows alá, valamint egyes Linux disztribúciók alatt csomagként is. Unix/Linux rendszerek alatt a fordítás nagyon egyszerű és gyors, valamint nem is igényel túl sok feltelepített csomagot. A fordításhoz make, gcc és OpenSSL fejlesztői csomag szükséges. A fordítást egy Python kód vezényli, ezért annak legalább 2.4-es változatára szükség van.
1 2 | var modul = require('modul');
module.func();
|
Amint mondtam, a Node.js modulkezelése CommonJS alapú. A modulok lehetnek pusztán JavaScript-ben, illetve C++ megírtak (utóbbiak jelentősen gyorsabbak). Magunk is létrehozhatunk saját modulokat, illetve elérhetővé tehetjük őket más alkalmazásaink és npm segítségével az egész közösség számára.
Az npm a Node.js hivatalos modulkezelője. Segítségével egy közös tárból letölthetünk és telepíthetünk modulokat. Kezelhetjük azok verzióit.
Az alkalmazásunkban a modulok betöltése nagyon egyszerű. A globálisan elérhető require
függvénnyel betöltjük a modult, és egyből használhatjuk is. Fontos megjegyezni, hogy a modulok betöltése nem aszinkron, viszont a betöltött kódot a rendszer gyorsító-tárazza, azaz az első betöltés után már memóriából lesz kiszolgálva.
Az alap rendszer számos beépített ugyanakkor viszonylag alacsony szintű modulból áll. Ezeket megpróbáltam csoportokba szedni használat szerint, de idő hiányában nem szeretném részletezni őket.
Itt látható egy hagyományos kiszolgáló felépítése. A különböző kliensektől, legyen az egy asztali számítógép, egy játékkonzol, vagy egy tablet, a kérés a szerverhez érkezik. Az itt futó, általában Apache HTTP szerver a kérést értelmezi, és amennyiben az egy fájlt kér le, akkor kiszolgálja, vagy amennyiben egy alkalmazást kell futtatni, akkor meghívja az alkalmazást, majd annak kimenetét adja vissza a kliensnek.
A Node.js rendszer felépítése azonban ettől jelentősen eltér. Mivel Node.js alatt a HTTP szervert is magunk írjuk (a beépített modulok használatával), ezért annak minden részletéről nekünk kell gondoskodni.
Tehát a kérés magához a Node.js alkalmazásunkhoz érkezik, az értelmezi, majd adja vissza a kimenetet a kliensnek. Amennyiben fájlokat is ki szeretnénk szolgálni (ami nagyon valószínű), akkor a fájlok kiszolgálásáról is nekünk kell gondoskodni.
Mivel az alkalmazásunk egy szálon fut, ezért szoktak elé egy terhelés elosztót is tenni. Ez lehet egy hardveres, vagy szoftveres megoldás, vagy egy hagyományos http szerver, mint például az nginx, vagy windowsos környezetben egy IIS.
Természetesen a terhelés elosztást megvalósíthatjuk Node.js alapokon is. Ekkor írunk egy HTTP szervert, ami valamilyen algoritmus alapján elosztja a kéréseket a vele párhuzamosan futó valódi alkalmazások között.
Hagyományos callback függvényekkel
1 2 | fs.readdir('/path/to/directory',
function (error, files) {});
|
Feliratkozunk egy eseményre
1 2 3 4 5 | request.on('data',
function (chunk) { });
// illetve, ami ugyanez:
request.addListener('data',
function (chunk) {});
|
A rendszer alapja, ahogy már a legelején írtam, az hogy aszinkron módon következnek be az események, és ezeket callback függvényeken keresztül kezeljük. A beépített modulok esetén vagy már a művelet elkezdésekor megadunk egy callback függvényt, vagy pedig utólag iratkozunk fel a bekövetkező eseményekre.
Események kezelése
1 2 3 4 5 6 7 | var MyObj = function () {}, myobj,
events = require('events'),
util = require('util');
util.inherits(MyObj, events.EventEmitter);
myobj = new MyObj();
myobj.on('event', function () {});
myobj.emit('event');
|
A rendszer az events
modul segítségével lehetővé teszi, hogy kényelmesen definiáljunk saját eseményeket. Azaz feliratkozhatunk egy objektum eseményeire, illetve kiválthatunk eseményeket az objektumon.
Az, hogy a Node.js alkalmazásunk egy szálon fut számos buktatót hoz magával. Ha egy műveletünk túl sokáig fut, akkor a többi művelet addig áll. Ezért fontos, hogy vagy daraboljuk fel a műveletünket több kis részműveletre (process.nextTick
, vagy setTimeout
segítségével) vagy szervezzük ki őket (például gyermek processzek).
Azt is megtehetjük, hogy létrehozunk egy Object pool-t, ami korlátozott számban ad vissza objektumokat, amin dolgozunk. Így korlátozzuk, hogy egyszerre mennyi elemünk fusson, ezzel csökkentve a párhuzamosan futók számát.
Az aszinkron futó kódok kényelmesebb kezeléséhez számos modul áll rendelkezésre, ezek közül a legnépszerűbbek az async és a fibers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var fs = require('fs');
fs.open('./data.txt', 'a', 0666,
function (error, fd) {
var b, d;
if (!error) {
d = (new Array(1e6)).join('data\n');
b = new Buffer(d + "\n",'utf8');
fs.write(fd, b, 0, b.length, null,
function (err, written, buffer) {
fs.close(fd, function () {});
}
);
}
}
);
console.log('Ez azonnal megjelenik');
|
Az eddig említett tulajdonságok magyarázatára álljon itt egy szemléltető példa.
A kódunk első sorában behívjuk az fs
modult, ami a fájlrendszer kezelésért felelős. Ezután az aktuális könyvtárban megnyitjuk a data.txt
nevű fájlt hozzáírásra, ami amennyiben nem létezik létrejön 666-os jogosultsággal. A 3. sorban levő callback függvény a fájl megnyitását követően fut le.
Fontos megemlíteni, hogy az olyan műveletek, amik esetleg hibával térhetnek vissza, az első paraméterben magát a hibát fogják tartalmazni, minden további paraméter a hasznos adat, amennyiben nem történt hiba.
Az ötödik sorban ezért ellenőrizzük, hogy történt-e hiba a fájl megnyitása, illetve létrehozása során. Amennyiben nem, akkor létrehozunk egy 999999 sorból álló stringet, amiből létrehozunk egy Buffert. A Buffer a Node.js bináris adattárolásáért felelős, globálisan elérhető objektum.
A 8. sorban pedig kiírjuk az adatot a fájl aktuális pozíciójába. Majd amikor az írás befejeződött, akkor bezárjuk a fájlt.
Fontos leszögezni, hogy ezek az események aszinkron történnek, azaz ezzel párhuzamosan több más kód is lefutna. Ez azt jelenti, hogy az utolsó sorban levő konzolra írás azonnal megjelenik, még a fájl tényleges megnyitása előtt.
1 2 3 4 5 6 7 8 | require('http').createServer(
function (req, res) {
res.writeHead('200',
{'Content-type': 'text/plain'}
);
res.end('Hello World\n');
}
}).listen(1337);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var http = require('http'),
server = new http.Server(),
port = 1337, host = '127.0.0.1';
server.on('request', function (req, res) {
res.writeHead('200',
{'Content-type': 'text/plain'}
);
res.end('Hello World\n');
});
server.listen(port, host, function () {
var address = server.address();
console.log('Listening on %s:%d',
address.address, address.port);
});
|
Következő példánk egy minimális webszerver kicsit jobban kifejtve, mint az előző legrövidebb, 5-6 soros változat. A cél az volt, hogy lássuk a beépített segédfüggvények igazából mit is csinálnak a mélyükön. Először is létrehozunk egy új HTTP szervert, majd annak feliratkozunk a request
eseményére. Ez akkor következik be, amikor egy HTTP kérés érkezik a szerverhez. A req
argumentumban a kérés objektuma, a res
-ben pedig a válasz objektuma. A 9. sorban pedig megkérjük a HTTP szerverünket, hogy figyeljen a megadott porton a megadott IP címre érkező kérésekre. Az átadott callback függvény akkor fog lefutni, amikor a szerverünk elindult.
A Node.js számos kedvező tulajdonsággal rendelkezik. Az egyik, hogy relatíve nagyon kicsi a memóriaigénye. Egy közepesen összetett alkalmazásnak sincs szüksége 20 megabyte-nál több memóriaára, és ez a legtöbb ma használt rendszerre nem jellemző. Ezt pedig még jobban segíti a V8 hatékony garbage collectora. Amint a példákban láttuk a rendszer teljes egészében eseményvezérelt, és pár kivételtől eltekintve az I/O műveletek nem blokkolják a kód futását. Ugyanakkor van egy viszonylag nehezen kezelhető tulajdonsága, mégpedig hogy egy szálon fut. Ezért minden hosszabb műveletet érdemes eseményvezéreltté tenni, illetve kisebb darabokra felbontani. Egy hosszabb számolás feltartja a többi esemény kezelését.
A rendszer esetében magunk írjuk a HTTP / TCP illetve UDP szerverünket, és ezek a szerverek daemonként futnak, azaz folyamatosan memóriában vannak. Amennyiben frissítettük a kódjukat, akkor az alkalmazásokat újra kell indítani, hogy érvényre jussanak. Írhatunk segítségével parancssoros alkalmazásokat kisebb-nagyobb feladatok elvégzésére, ilyen például a CoffeeScript fordító, vagy az UglifyJS JavaScript tömörítő. Alkalmazásunkban meghívhatunk más alkalmazásokat, hozzáférhetünk a fájlrendszerhez, processzekhez, socketekhez, streamekhez. A Buffer objektum segítségével pedig kezelhetünk bináris adatokat.
A fentiek fényében tehát milyen alkalmazásokra érdemes a Node.js rendszert használni? Leginkább olyan feladatokra, amikor vagy nem lényeges, hogy programunk egy szálon fut. Ilyen például a már említett fejlesztői parancssoros alkalmazások. A nem blokkoló eseményvezérelt I/O miatt pedig igazán alkalmas mindenféle I/O érzékeny feladat ellátására, mint például intelligens proxy/reverse proxy szerver, stream szerver, CDN, load balancer. Ha ki akarjuk használni a fejlett HTTP képességeket, akkor írhatunk benne weboldalakat, szolgálhatunk ki vele widgeteket, reklámokat, API-kat. Leginkább tehát ezek a kevéssé számításigényes ugyanakkor I/O érzékeny élő alkalmazások ellátására használható, amikor fontos, hogy párhuzamosan nagyon sok kérést tudjunk kiszolgálni és irányításunk alatt akarjuk tartani a hálózati folyamatokat.
Ugyanakkor van egy elég nagy halmaza az feladatoknak, amikre nem, vagy csak igen korlátozottan alkalmas. Talán a legfontosabb, ami mostanában a legtöbb vízhangot keltette, az a CPU igényes feladatok ellátásában nyújtott viszonylag gyenge teljesítmény. Hasonlóan nem érdemes, legfeljebb csak részben felhasználni a Node.js képességeit, ha van egy már működő alkalmazásunk. Ugyanakkor ekkor is van lehetőség a Node.js bevetésére, ugyanis egyes feladatokat kiszervezhetjük a Node-ba. Például az API kérések kiszolgálását, amennyiben azok inkább adatbázis, vagy I/O igényesek, de hagyjuk a számolást valami más rendszerre.
Hoszting tekintetében, amennyiben nem rendelkezünk saját, vagy virtuális szerverrel, akkor is van lehetőségünk az alkalmazásunkat éles környezetben bevetni. A fenti szolgáltatók különböző parancssoros és webes felületet nyújtanak az alkalmazásunk telepítésére, futtatására, újraindítására.
A rendszerhez jelenleg is rengeteg külső modul érhető el. Az npm repozitorájában több mint 4000 csomagot tartanak számon, de GitHub-on további modulokat tudunk találni. A közösség nagy része ugyanis ott teszi közzé moduljait, és azok fejlesztése is igen aktívan folyik. A felsorolt modulok egy kezdeti lökést adnak a programunk kifejlesztéséhez, és rengeteg hasznos szolgáltatást adnak a további fejlesztésekhez is.
Már most több hasznos fejlesztői eszköz létezik hibakeresésre, teljesítmény mérésre és tesztelésre.
A fenti linkeken további információhoz lehet jutni az alapoktól kezdve egészen a fejlett témákig. Természetesen a nodejs.org oldalon megtalálható a teljes API mind a stabil, mind a fejlesztői változathoz, régebbi verziókra visszamenőleg is. A felsorolt könyvek nagy része ingyenesen hozzáférhető. A témával számos blog, screencast foglalkozik, elég egy rövid kis keresést végezni kedvenc keresőoldalunkon.
Köszönöm.
Kérdések?
Table of Contents | t |
---|---|
Exposé | ESC |
Full screen slides | e |
Presenter View | p |
Source Files | s |
Slide Numbers | n |
Toggle screen blanking | b |
Show/hide slide context | c |
Notes | 2 |
Help | h |