A Node.js bemutatása

Presenter Notes

Galiba Péter

Presenter Notes

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,

  • hányan vannak fejlesztők,
  • hányan foglalkoznak JavaScript-tel,
  • kik ismerik a Node.js rendszert?

Node.js történelem

  • 2009-ben Ryan Dahl szeretne írni egy aszinkron webszervert
  • A JSConf 2009 rendezvényen bemutatja a rendszert
  • 2010-08-20 első stabil változat (0.2.0)
  • Jelenlegi stabil változat a 0.4.12 a fejlesztői 0.5.8

Presenter Notes

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 jellemzői

  • C/C++-ban írt rendszer
  • Aszinkron I/O
  • Eseményvezérelt (event queue)
  • JavaScript-ben fejleszthető
  • CommonJS modulok

Presenter Notes

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.

Belső felépítés

  • Google V8 JavaScript motor
  • libuv aszinkron réteg
  • HTTP Parser
  • OpenSSL

Presenter Notes

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.

Belső felépítés - Google V8

  • Gyors
  • FOSS licenc
  • Könnyen integrálható
  • Minden fontos JavaScript szolgáltatást tud

Presenter Notes

Belső felépítés - libuv

  • Aszinkron fájlkezelés / UDP / TCP / DNS / szálkezelés / pipes
  • Nagy pontosságú óra
  • Gyermek processzek
  • Windows támogatás!

Presenter Notes

Belső felépítés - HTTP Parser

  • A rendszer webszervernek készült
  • Független szoftver
  • HTTP 1.1 támogatás

Presenter Notes

Belső felépítés - OpenSSL

  • Kódolás
  • Hash
  • HTTPS / TLS / SSL

Presenter Notes

Telepítés

  • Windows bináris
  • *nix rendszerek alatt elérhető csomagban
  • Fordítás:
    • make
    • gcc
    • OpenSSL
    • Python >= 2.4 < 3.0

Presenter Notes

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.

Modulok

  • CommonJS alapú commonjs.org
  • JavaScript / C++ modulok
  • Csomagkezelő: npm (Windows alatt még gyerekcipőben)

kód:

1
2
var modul = require('modul');
module.func();

Presenter Notes

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.

Beépített modulok - általános

  • Process
  • Events
  • Crypto
  • TLS/SSL
  • Child Processes
  • Buffers
  • Streams
  • ZLIB

Presenter Notes

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.

Beépített modulok - I/O

  • File System
    • Path
  • Net
    • DNS
    • HTTP
    • HTTPS
  • UDP/Datagram
  • URL, Query Strings

Presenter Notes

Beépített modulok - konzol / dev

  • Readline
  • REPL
  • VM
  • Assertion Testing
  • TTY
  • OS
  • Utilities
  • Debugger

Presenter Notes

„Hagyományos” kiszolgáló felépítés

Presenter Notes

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.

Node.js kiszolgáló felépítés

Presenter Notes

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.

Aszinkron műveletek

  1. Hagyományos callback függvényekkel

    1
    2
    fs.readdir('/path/to/directory',
      function (error, files) {});
    
  2. Feliratkozunk egy eseményre

    1
    2
    3
    4
    5
    request.on('data',
      function (chunk) { });
    // illetve, ami ugyanez:
    request.addListener('data',
      function (chunk) {});
    

Presenter Notes

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.

EventEmitter

  • Saját objektum kiterjesztése
  • Események kiváltása
  • 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');
    

Presenter Notes

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.

Aszinkron műveletek - buktatók

  • A műveletek ne fussanak sokáig
  • Túl sok művelet ne fusson párhuzamosan
  • Kérések korlátozása - Object pool
  • Segédmodulok: async, fibers

Presenter Notes

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.

Példák - Fájlrendszer

 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');

Presenter Notes

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.

Példák - Minimális HTTP szerver

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);

Presenter Notes

Példák - HTTP szerver

 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);
});

Presenter Notes

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.

Node.js tulajdonságok

  • Kis memóriahasználat
  • Esemény vezérelt
  • I/O nem blokkol
  • Egy szálon fut

Presenter Notes

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.

Node.js tulajdonságok (2)

  • Magunk írjuk a webszervert, nincs httpd.
  • Alkalmazásunk daemon-ként (is) fut(hat) (például HTTP / DNS szerver)
  • Parancssoros alkalmazások
  • Futtathatunk más programokat
  • Hozzáférünk a fájlrendszerhez, hálózathoz, processzekhez, socketekhez, streamekhez
  • Tudunk kezelni bináris adatokat

Presenter Notes

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.

Mire használjuk?

  • Fejlesztői segédalkalmazások
  • Proxy / Reverse proxy
  • Stream szerver
  • CDN
  • Load balancer
  • Widget / reklám / API kiszolgálás
  • „Élő” alkalmazások

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.

Presenter Notes

Mire ne használjuk?

  • CPU igényes alkalmazások
  • Működő alkalmazásunk más nyelvben
  • Olyan szolgáltatásokat akarunk használni, ami nem elérhető Node.js alatt

Presenter Notes

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

  • Heroku
  • Joyent
  • Nodejitsu
  • Nodester
  • Stackato
  • ...

Presenter Notes

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.

Hasznos külső modulok

  • npm - Node „hivatalos” csomagkezelője
  • Connect - HTTP middleware
  • Express - Web fejlesztői keretrendszer
  • Socket.IO - Websockets egyszerűen

Presenter Notes

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.

Fejlesztői eszközök

Presenter Notes

Már most több hasznos fejlesztői eszköz létezik hibakeresésre, teljesítmény mérésre és tesztelésre.

További információ

Presenter Notes

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?