vorba.ch

Node.js

von Paul Vorbach, 13.04.2012

Wie ich feststellen musste, lässt sich Bread kaum erklären, ohne ein paar Grundlagen zu Node.js gelegt zu haben. (Achtung: Hier geht’s ans Eingemachte!)

“Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.”

So heißt es lapidar auf der Node.js-Website. Über manche dieser Punkte lässt sich sicherlich streiten. Auf die wichtigsten will ich aber hier kurz eingehen und die Besonderheiten bzw. Unterschiede des neuen Hicen Shice der Webentwicklung zu PHP und anderen Plattformen aufzeigen.

Im Grunde ist Node.js eine Umgebung, mit der man JavaScript ohne einen Browser ausführen kann. Dabei kommt die V8-JavaScript-Engine zum Einsatz. Diese zeichnet sich dadurch aus, nicht gerade die langsamste Implementierung von JavaScript zu bieten. ;-)

Es gab auch vorher zahlreiche andere Anwendungsmöglichkeiten für JavaScript außerhalb des Browsers. So wird es beispielsweise in Adobes Flash-Plattform unter dem Namen ActionScript genutzt oder auch zum Scripting verschiedener Programme, die eigentlich nichts mit JavaScript am Hut haben, zum Beispiel in PDF, der Gnome 3 Shell oder als Makrosprache in OpenOffice.

JavaScript ist anders als die meisten prozeduralen (und objektorientierten) Sprachen. Es sieht aus wie Java oder C, ist aber eher ein Lisp mit geschweiften Klammern. So sagt man zumindest. So sehr möchte ich aber gar nicht auf die Sprache eingehen. Eine gute Einführung in die Sprache gibt es bei Mozilla.

Node.js bietet ein paar Erweiterungen der winzigen Standardbibliothek. Node enthält hauptsächlich zusätzliche Module für typische Netzwerk- oder Dateisystem-Aufgaben. Es lassen sich aber auch prima Konsolenprogramme mit aber auch ohne jegliche Netzwerk-Interaktion programmieren und mittlerweile gibt es auch Erweiterungen, die grafische Benutzeroberflächen ermöglichen.

Unterschiede zu anderen Plattformen

Ein großer Unterschied zwischen Node und PHP ist, dass PHP entweder über FastCGI oder direkt als Apache-Modul immer einen einzelnen Request von einem einzelnen Browser vorgesetzt bekommt, den man dann nach Belieben verarbeiten und beantworten kann.

Da Node.js nicht immer HTTP-Requests beantwortet, muss man sich einen solchen Webserver selbst schreiben. Dafür gibt es aber die nötigen Bibliotheken, die diese Aufgabe erheblich vereinfachen.

var http = require('http');
var address = 'localhost';
var port = 8080;

var server = http.createServer(function handle(req, resp) {
  response.end('Hallo Welt!');
});

server.listen(port, address, function () {
  console.log('Server running at "http://' + address + ':'
      + port + '/".');
});

Speichert man diese Zeilen Code in eine Datei namens server.js und startet diese über die Kommandozeile mit node server.js, so erhält man die Ausgabe Server running at "http://localhost:8080/". auf der Konsole. Ruft man nun im Browser die angegebene Adresse auf, so bekommt man „Hallo Welt!“ angezeigt.

Das Server-Objekt aus dem Beispiel wartet auf eingehende HTTP-Verbindungen und führt jeweils die Funktion handle aus. Diese bekommt über den Parameter req (Request) die Informationen über den Request mitgeteilt.1 Das Objekt resp (Response) ermöglicht dann das Antworten auf den Request mit den Methoden writeHead, write und end (u.a.).2

Das gleiche Programm lässt sich in PHP viel kürzer schreiben:

<?php
echo 'Hallo Welt!';
?>

Alles, was innerhalb der Funktion handle steht, kann in der Regel auch mit PHP bewerkstelligt werden.

Vorteile

Asynchronizität

(Furchtbares Wort.) Ein weiterer wichtiger Unterschied zu herkömmlichen Umgebungen ist die besondere Behandlung von Festplatten-, Datenbank- und Netzwerkzugriffen. Während PHP bei einer Datenbankabfrage (oder Festplattenzugriff oder Netzwerkrequest) solange nichts tut, bis die Datenbank (die Festplatte, das Netzwerk) entweder das Ergebnis oder einen Fehler liefert, blockieren solche „langsamen“ Operationen in Node den Programmablauf nicht. Das heißt, eine Datenbank-Query wird losgeschickt und eine Funktion registriert, die das Resultat verarbeitet. Anstatt zu warten, läuft das Programm danach jedoch weiter und der Callback erfolgt erst, wenn das Ergebnis da ist. Wo mehrere Zustände erreicht werden können, werden auch teilweise Event-Objekte und entsprechende Funktionen eingesetzt.

client.query(
  'SELECT * FROM table',
  function selected(err, results, fields) {
    if (err)
      throw err;

    console.log(results);
    console.log(fields);
    client.end();
  }
);

Dies steht im Gegensatz zu Apache/PHP, wo für jeden eingehenden Request ein neuer Thread erzeugt wird. Threads sind für diesen Zweck jedoch vergleichsweise ineffizient.3 So kann PHP auf schwächeren Rechnern schon bei 10 bis 100 gleichzeitigen Zugriffen ins Straucheln kommen, während Node.js weit über 10.000 gleichzeigige Verbindungen abarbeiten kann.

Für Python gibt es mit Twisted, für Ruby mit Event Machine ähnliche Ansätze zur Programmierung schnellerer Webserver. Der Vorteil von Node liegt aber darin, dass JavaScript genau für Event-basierte Programmierung konzipiert wurde. Oberflächen (bzw. Benutzer, die diese bedienen) verhalten sich genauso, wie Netzwerke oder Festplatten: Man weiß nie, wann eine Aktion ausgeführt wird.

Daher lässt sich in JavaScript sehr einfach und natürlich Event-basiert programmieren.

Durch die Events lassen sich viele Dinge mehr oder weniger parallelisieren. Das trifft aber nur auf „ausgelagerte“ Aktionen zu. Man kann beispielsweise gleichzeitig mehrere Datenbankabfragen laufen lassen und eine Datei auslesen. Das Programm wird jedoch niemals mitten im Ablauf unterbrochen. Somit können keine Race Conditions entstehen. Das macht die Programmierung wesentlich leichter überschaubar als mit Threads.4 Der Zustand der Objekte bleibt immer gewahrt.

Wenn man den Programmablauf selbst parallelisieren möchte, muss man Teile des Programms als eigene Prozesse starten und dann mit Nachrichten (über Unix-Domain-Sockets oder TCP-Verbindungen) synchron halten, ähnlich wie es im Aktor-Modell beschrieben wird. Das kann bei sehr rechenintensiven Programmen nötig werden. Meist reicht es aber, wenn lediglich asynchrone I/O verwendet wird. In Node lässt sich das Aktor-Modell umsetzen, ist aber nicht in die Plattform integriert wie beispielsweise in Erlang.

Paketsystem

Ich schrieb ja bereits, dass man mit JavaScript sehr modularen Code schreiben kann. Das erkläre ich mal am Beispiel von dive.

var dive = require('dive');

dive('/some/directory', function action(path) {
  // wird für jede Datei aufgerufen
}, function complete() {
  // wird aufgerufen nachdem alle Dateien durchlaufen wurden
});

Dive nimmt einen String mit einem absoluten Ordnerpfad entgegen, „taucht“ in dieses Verzeichnis ein und durchläuft alle Unterverzeichnisse und Dateien rekursiv. Für jede Datei wird dann der Callback action ausgeführt. Nachdem alle Dateien durchlaufen wurden, wird complete ausgeführt. That’s it. Mehr kann es nicht, ist aber trotzdem für viele Zwecke einsetzbar, weil man ja ganze Funktionen übergeben kann. Diese können dann ihrerseits beispielsweise nach dem Dateinamen filtern und so kann die Funktionalität verfeinert werden.

Node.js verwendet das CommonJS-Modulsystem und erlaubt es dadurch, solchen Code unkompliziert in Pakete zu verpacken. Für ein Paket benötigt man nur zwei Dateien. Eine Datei namens package.json sowie das eigentliche Programm.

Die Datei package.json enthält Informationen über das Paket.

{
  "name": "dive",
  "description": "walk through directory trees and apply an action to every file",
  "tags": [ "recursive", "file walking", "directories", "async" ],
  "author": "Paul Vorbach <paul@vorb.de> (http://vorb.de)",
  "version": "0.2.0",
  "main": "./dive.js",
  "repository": {
    "type": "git",
    "url": "git://github.com/pvorb/node-dive.git"
  },
  "dependencies": {
    "append": ">=0.1.1"
  }
}

Im Quelltext kann man dann beliebige Teile des Codes „exportieren“. Sprich: Man kann der Variable exports dann alle möglichen Werte zuweisen. Im Beispiel oben wurde einfach eine Funktion exportiert. Das sieht im Quelltext so aus:

function dive(dir, action, complete) {
  // ...
}

exports = dive;

Mit Node Package Manager (npm) existiert außerdem ein Verzeichnis solcher Pakete. Über ein einfaches npm install paketname lassen sich diese installieren und im Code dann, wie oben schon gesehen, über var paketname = require('paketname'); einbinden.

Solch ein Paketsystem ist aber nichts neues, das gibt es für viele Programmierumgebungen.5 Mit momentan über 8000 Paketen hat das Verzeichnis in nur knapp anderthalb Jahren eine stolze Größe erreicht.

Wiederverwendbarkeit

Der große Vorteil von JavaScript als serverseitiger Scriptsprache liegt in der Wiederverwendbarkeit der Pakete. Sie lassen sich sowohl in Node als auch im Browser verwenden. Über Ender lassen sich diese dann auch einfach zu Bibliotheken so zusammenstellen, wie man es gerade braucht. So könnte man zum Beispiel den Code, der Nutzereingaben validiert, sowohl auf dem Server als auch im Browser benutzen.

Nachteile

Node ist alles andere als perfekt. Man sollte sich genau überlegen, ob es für ein bestimmtes Projekt geeignet ist, bevor man sich ins kühle Nass wirft.

Callback-Hölle:

Wenn man viele asynchrone Dinge hintereinander ausführen möchte, kann das Programm schnell unübersichtlich werden. Für jeden Callback wird der Code in der Regel eine Ebene weiter eingerückt. Es gibt zwar Möglichkeiten, das zu umgehen, trotzdem muss man sich damit beschäftigen.

Kleine Standardbibliothek

Die kleine Standardbibliothek von JavaScript ist Segen und Fluch zugleich. Die Erweiterungen von Node helfen auch nur bei speziellen Problemen. An die Java Platform oder das .Net-Framework kommt die Funktionalität bei weitem nicht heran. Viele Dinge muss man entweder selbst schreiben oder aus den zahlreichen Paketen von npm zusammensuchen. Dann hat man aber häufig inkonsistente Schnittstellen und muss die benötigten Informationen an vielen verschiedenen Stellen nachlesen.

Parallelisierung

Node.js bietet von sich aus keine Möglichkeiten zur echten Parallelisierung des Programmablaufs. Über das Aktor-Modell lässt sich Parallelisierung erreichen. Das ist jedoch aufwendig. Mit FabricEngine soll es angeblich auch möglich sein, Programme für mehrere Threads zu optimieren.

Fazit

Durch Node.js wird es wesentlich einfacher, schnelle und schlanke Programme zu schreiben. Es ist aber keine eierlegende Wollmilchsau. Es ist einfach ein Werkzeug, das für bestimmte Anwendungszwecke Sinn ergibt. Für viele Aufgaben, die bisher mit typischen Web-Frameworks wie Ruby on Rails oder dem Klon in der jeweils bevorzugten Sprache gelöst werden, sind diese immer noch angebracht.

Wer aber mit JavaScript gut klarkommt und bereit ist, seine Denkweise teilweise anzupassen, der findet mit Node.js eine Alternative, die durch die Asynchronizität und die Schnelligkeit von V8 einiges aus so manch alter Kiste herausholt. Die Plattform ist aber auf jeden Fall einen Blick wert.

Weiterführendes Material


  1. Vergleichbar mit den Variablen $_GET und $_POST in PHP.

  2. In PHP wäre das alles, was nicht zwischen <?php und ?> steht, oder die Ausgabe von Strings mit echo und print.

  3. siehe das C10K-Problem.

  4. Bei den gängigen Plattformen wie PHP, Python oder Ruby spielen Threads für den Programmierer keine Rolle. Bei Java und C# kann man jedoch echte Parallelität des Codes durch Threads erreichen und diese selbst kontrollieren.

  5. siehe CPAN (Perl; über 100.000 Pakete), RubyGems (Ruby; über 37.000 Pakete), pypi (Pyhton; über 20.000 Pakete), PEAR (PHP; über 586 Pakete).