Node.js: Das JavaScript-Framework im Überblick
Traditionelle Skriptsprachen wie beispielsweise PHP, die im Bereich der Web-Entwicklung zum Einsatz kommen, basieren auf Anfragen. Eine Anwendung wird also durch eine Anfrage des Clients gestartet. Diese Architektur erweist sich aber vor allem dann als unpraktisch, wenn es darum geht, einen Browser aktiv über Änderungen zu informieren. Eine Anforderung, die immer häufiger im Bereich von Realtime-Web-Anwendungen erforderlich ist.
Hier ergänzt serverseitiges JavaScript sinnvoll den Technologie-Stack einer Web-Anwendung, da eine JavaScript-Anwendung unabhängig von Anfragen der Clients auf dem Server läuft. Nach dem Start ist die Anwendung an einen bestimmten TCP-Port gebunden und wartet auf Anfragen der Clients. Sobald eine Verbindung aufgebaut ist, besteht die Möglichkeit, dass der Server aktiv mit dem Client kommuniziert.
Node.js
Eine konkrete Implementierung von JavaScript auf dem Server ist Node.js [1], das 2009 von Ryan Dahl ins Leben gerufen wurde und seit dieser Zeit stetig an Bekanntheit gewinnt. Den Kern bildet Googles V8-Engine, diese kommt auch als JavaScript-Engine in Googles Browser Chrome zum Einsatz.
Bislang kommt Node.js jedoch nur selten über die experimentelle Nutzung hinaus. Grund dafür ist zum einen das frühe Entwicklungsstadium, auf das auch die aktuelle Versionsnummer 0.8 hinweist. Es zeigt sich vor allem in häufigen Änderungen der APIs, was es Entwicklern schwer macht, immer die aktuelle Version zu verwenden, ohne die eigene Applikation grundlegend zu ändern. Doch die Entwicklungstendenz von Node.js geht hin zum Einsatz von Node.js in professionellen Web-Applikationen, vor allem im Bereich der bidirektionalen Kommunikation zwischen Client und Server.
Single- vs. Multi-Threading
Node.js setzt auf Single-Threading, es steht also lediglich ein Programm-Thread für die Anwendung zur Verfügung. In der Folge kann auch nur ein Kommando „gleichzeitig“ ausgeführt werden, was im ersten Schritt keine Parallelisierung erlaubt. Diese Architektur ist allerdings bewusst gewählt, da mit Multi-Threading auch die Anforderungen an den Entwickler steigen. So steht beim Multi-Threading nicht nur ein Thread für das Programm zur Verfügung, sondern mehrere, die sich im gleichen Programmkontext bewegen. Dabei muss etwa auf Thread-Safety, also die Steuerung des Zugriffs paralleler Threads auf eine Ressource, geachtet werden. Auch die Fehlersuche gestaltet sich bei dieser Art der Programmierung schwieriger, da bestimmte Arten von Fehlern nur bei einer bestimmten Konstellation und zeitlichen Abfolge von abhängigen Threads auftreten.
Um dennoch den Prozess nicht mit einer Anfrage für andere Anfragen zu blockieren, setzt Node.js auf ein ausgelagertes I/O-System. I/O steht für Input/Output, also Schreib- und Leseoperationen, sowohl auf der Festplatte als auch in der Datenbank. Während eine Anfrage bearbeitet wird, ist der Prozess für gewöhnlich blockiert. Aus diesem Grund wird I/O in Node.js ausgelagert und asynchron durchgeführt.
Den Kern der asynchronen Bearbeitung von Node.js bildet der Event Loop. Er nimmt Anfragen für I/O und Callbacks, also Funktionen, die ausgeführt werden, sobald I/O-Anfragen bearbeitet wurden, entgegen. Die Kontrolle erhält anschließend ein externer Prozess, Node.js kann sich um andere Operationen kümmern. Sobald der I/O-Prozess beendet ist, wird das Ergebnis an den Event Loop zurückgegeben, der den zuvor definierten Callback mit den Ergebnissen aufruft; die Bearbeitung läuft regulär weiter.
Die Kombination des ausgelagerten I/O-Systems mit Event Loop und dem Single-Threading macht Node.js zu einer äußerst performanten Lösung. Im Zusammenspiel mit verschiedenen anderen Technologien und Plattformen ist es deshalb ein idealer Bestandteil hochperformanter Web-Applikationen.
Websockets
In traditionellen Web-Applikationen antwortet der Server nur auf Anfrage des Clients. Es existiert kein Rückkanal, also keine Möglichkeit für den Server, den Client zu erreichen, falls sich Daten auf Serverseite verändert haben. Große Plattformen wie Facebook und Twitter leben jedoch von der Echtzeit-Interaktion der Nutzer. Dadurch entsteht die Notwendigkeit, neue Daten an verschiedene Clients auszuliefern, die die Darstellung der Daten im Idealfall partiell aktualisieren und so die Veränderungen widerspiegeln.
HTML5 standardisiert neben reinen HTML-Tags eine Reihe von Technologien, mit deren Hilfe die Anforderungen moderner Web-Applikationen besser erfüllt werden können. Im Fall der bidirektionalen Kommunikation zwischen Client und Server sind dies sogenannte Websockets [2], ein auf TCP aufbauendes Protokoll.
Das Protokoll operiert auf der gleichen Ebene wie HTTP. Der größte Unterschied zu HTTP liegt aber darin, dass es sich bei einem Websocket um eine permanente Verbindung handelt, über die sowohl der Client als auch der Server Daten schicken kann.
Problematisch ist dabei lediglich, dass diese Technologie nicht von allen Browsern unterstützt wird. Microsofts Internet Explorer etwa unterstützt das Websocket-Protokoll erst ab der nächsten Version (10). Bis es soweit ist, lässt sich das Protokoll mittels Bibliothek abstrahieren: Socket.io [3] ist komplett in JavaScript implementiert und steht sowohl auf Serverseite als auch auf Clientseite zur Verfügung. Unterstützt ein Browser keine Websockets, existiert ein Fallback auf Long Polling, Flashsockets und andere Technologien.
Installation von Socket.io
$ npm install socket.io
Listing 1
Um Socket.io in Verbindung mit Node.js verwenden zu können, muss das entsprechende Paket installiert sein. Node.js bietet hierfür den Node Package Manager (NPM) [4] an, mit dessen Hilfe sich einfach zusätzliche Module installieren lassen. Die wichtigsten Optionen für NPM sind entsprechend „install“ und „remove“, um Pakete zu installieren und wieder zu entfernen, sowie „list“, um die installierten Pakete samt Abhängigkeiten aufzulisten.
Node.js Socket Server
var io = require('socket.io').listen(1337); io.sockets.on('connection', function (socket) { socket.on('message', function (data) { // do something }); });
Listing 2
Listing 2 zeigt, wie einfach ein vollständiger Socket-Server in Node.js aussieht. Prinzipiell wird der Server in drei Schritten erstellt. Zuerst wird das Socket.io-Modul mittels „require“ eingebunden und mit einem TCP-Port versehen, über den die Kommunikation erfolgt. Das Socket.io-Objekt, das dieser Aufruf zurückgibt, dient im weiteren Programmablauf dazu, das Verhalten des Servers festzulegen. Im nächsten Schritt definiert die Methode „on(‚connection‘, …)“ was passiert, sobald sich ein Client mit dem Server verbindet. Innerhalb der Callback-Funktion (zweiter Parameter) wird wiederum mittels „on“-Methode durch ein Callback festgelegt, was beim Eintreten bestimmter Ereignisse (im Beispiel beim Eintreffen einer Nachricht) auf dem Socket passieren soll.
Auf Serverseite besteht die Möglichkeit, über die „emit“-Methode Nachrichten über die Socket-Verbindung an den Client zu schicken. Dazu werden der Typ der Nachricht und weitere Daten angegeben, die die Nachricht spezifizieren.
Nachrichten über eine Socket-Verbindung senden
socket.emit('hello', { result: 'hello world' });
Listing 3
Wichtig dabei ist, dass „emit“ lediglich einen Client anspricht. Obwohl oft eine Point-to-Point-Kommunikation erwünscht ist, gibt es immer wieder Fälle, in denen an alle verbundenen Sockets oder eine bestimmte Gruppe von Sockets gesendet werden soll. Um alle Sockets zu erreichen, existiert die Methode „broadcast“ im Socket-Objekt, die wiederum die „emit“-Methode implementiert.
Eine Möglichkeit, um die größtmögliche Kontrolle über die verfügbaren Sockets zu erlangen, ist die Speicherung der Socket-Handles in einer Hashmap. Als Gruppierung der Sockets ermöglicht sie eine sehr feine Steuerung, auf welchen Socket zu welcher Gelegenheit welche Nachricht geschickt werden soll.
Wenn über die bestehende Socket-Verbindung persönliche Daten ausgetauscht werden, die für Dritte nicht einsehbar sein sollen, lässt sich von unverschlüsselten Websockets auf die sichere, verschlüsselte Variante zurückgreifen. Standardmäßig wird für Websockets das Protokollpräfix „ws://“ verwendet, bei der sicheren Variante „wss://“. Für eine explizit verschlüsselte Verbindung dient auf Clientseite einerseits das Präfix „https://“ in der „connect“-Methode, andererseits die Option „secure: true“.
Sichere Websocket-Verbindung
var socket = io.connect('https://example.com:1337', { secure: true });
Listing 4
Asynchrone Ausführung
Das zentrale Konzept von Node.js basiert also auf der asynchronen Abarbeitung in einem Single-threaded-Event-Loop, um aufwändige I/O-Operationen auszulagern. Bei eigenem Code ist man selbst dafür verantwortlich, Node.js nicht zu blockieren. Warum diese Überlegung durchaus relevant ist, zeigt ein einfaches Beispiel.
Synchrone Berechnung
require('http').createServer(function (req, res) { console.log('incoming request'); var i = 0, x = 1; while(i++ < 1000000000) { x = 40 * 50 * i + 47; } res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World'); console.log('request answered'); }).listen(1337, '127.0.0.1');
Listing 5
Mit diesem Skript wird ein einfacher Webserver erstellt. Bei einer Anfrage gibt er auf der Konsole „incoming request“ aus. Die anschließenden Berechnungen sollen aufwändige Vorgänge innerhalb des Skripts simulieren, bevor der Server eine Rückgabe an den Client erstellt und verschickt, sowie auf der Konsole „request answered“ ausgibt. Der große Nachteil dieses Skripts besteht darin, dass während der Beantwortung eines Requests keine weiteren Anragen beantwortet werden können, da der Server durch die Berechnung blockiert ist. Die Ausgabe bei zwei annähernd gleichzeitig abgesendeten Requests ist entsprechend synchron (Listing 6).
Konsolenausgabe bei synchroner Berechnung
$ node server.js incoming request request answered incoming request request answered
Listing 6
Um die Berechnungen asynchron durchzuführen, bietet Node.js das „child_process“-Modul. Dieses Modul erlaubt im Zuge einer Applikation, Kind-Prozesse zu erstellen, in die verschiedene Aufgaben ausgelagert werden können. Eine bidirektionale Kommunikation zwischen Eltern- und Kind-Prozess ermöglicht darüber hinaus Events und Callbacks, die ein blockierendes Warten auf Antworten unnötig machen.
Listing 7 zeigt, wie eine einfache Lösung des Problems aussehen könnte. Es besteht im Prinzip aus den gleichen Komponenten wie das Beispiel der synchronen Berechnung. Der einzige Unterschied
besteht darin, dass der rechenintensive Teil in einer eigenen Datei liegt und als separater Kind-Prozess ausgeführt wird.
Durch das Kommando „fork“ wird aus dem Inhalt der angegebenen Datei ein
Kind-Prozess erzeugt. Mittels „send“ und „ok“ erfolgt die Kommunikation
zwischen den beiden Prozessen. Sobald der Eltern-Prozess eine
Rückmeldung erhält, wird die Antwort an den Client
gesendet.
Asynchrone Berechnung
// server.js require('http').createServer(function (req, res) { console.log('incoming request'); var task = require('child_process').fork('someTask.js'); task.on('message', function (response) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World'); console.log('request answered'); }); task.send({ start: true }); }).listen(1337, '127.0.0.1'); // someTask.js process.on('message', function(m) { var i = 0, x = 1; while(i++ < 1000000000) { x = 40 * 50 * i + 47; } process.send('finished'); });
Listing 7
Die Ausgabe dieses Skripts, ebenfalls mit zwei annähernd gleichzeitig abgesetzten Requests, ist in Listing 8 zu sehen. Es zeigt deutlich das Ergebnis einer asynchronen Bearbeitung. Dabei werden beide Requests sofort angenommen und bearbeitet. Sobald die
Berechnung abgeschlossen ist, erfolgt die Rückmeldung an den Client. Auf diese Weise wird das Skript nicht unnötig blockiert und
Node.js kann die Ressourcen besser ausnutzen.
Konsolenausgabe bei asynchroner Berechnung
$ node server.js incoming request incoming request request answered request answered
Listing 8
Ausblick
Die beiden Beispiele im Umgang mit Node.js zeigen, wie mächtig die Plattform ist. Verfolgt man das Konzept der Asynchronität konsequent, können mit einer Node.js-Instanz sehr viele Client-Anfragen bedient werden. Verschiedene Benchmarks sprechen in diesem Zusammenhang von mehreren zehntausend, manche gar von hunderttausenden parallelen Anfragen.
Aller Leistung zum Trotz entfaltet Node.js seine wahre Stärke jedoch erst im Zusammenspiel mit anderen Technologien. So empfiehlt es sich etwa, statische Inhalte von einem Nginx-Server ausliefern zu lassen und über einen Proxy-Server die Anfragen an Node.js weiterzuleiten.
Ein weiteres denkbares Szenario ist der Einsatz von Node.js in Verbindung mit Request-basierten Skriptsprachen wie beispielsweise PHP, um deren Schwächen bei bidirektionaler Echtzeit-Kommunikation auszugleichen. Durch die Kombination könnte so die Schwäche des einen (PHP) durch die Stärke des anderen (Node.js) ausgeglichen werden.
Die letzte stabile Version von Node.js verbesserte die Geschwindigkeit im Vergleich zur Vorgängerversion noch einmal deutlich. Das liegt unter anderem an einer aktualisierten Version der V8-Engine und einer Reihe von Erneuerungen verschiedener Kern-Bibliotheken.
An seine Grenzen stößt Node.js lediglich bei Überskalierung. Ist eine Instanz von Node.js mit den Anfragen überfordert, gibt es aber die Möglichkeit, über eine intelligente Proxy-Lösung die Anfragen von Clients auf verschiedene Node.js-Server zu verteilen. Der Applikationsstatus kann dann in einer Datenbank wie beispielsweise Redis zwischen den einzelnen Servern gespeichert werden.
Insgesamt zeigen all diese Denkanstöße, dass Node.js mit seinen Fähigkeiten noch lange nicht am Ende ist. Die Einsatzmöglichkeiten sind heute noch nicht überschaubar, einzig eines ist bislang klar: JavaScript auf Serverseite ist keineswegs absurd.
Frühes Entwicklungsstadium?
Was hat das mit dem Versionierungschema zu tun?
Node.js ommt auf jeder menge Seiten zum Einsatz, und das teilweie schon seit Jahren!
Habt ihr andere Artikel genauso schlecht recherchiert?
Der Post ist doch an mehreren Stellen schon nicht mehr aktuell…
@Pascal Der Artikel stammt aus dem t3n-Magazin Nr. 29. Die Erstveröffentlichung war – wie du oben nachlesen kannst – am 28.08.2012. Möglicherweise erklärt das die fehlende Aktualität, die du bemängelst.
Node.js ist defintiv ausgereift. Die vielen Änderungen der Funktionen und Methoden (und damit auch der Dokumentation), sind seit Ende letzten Jahres m.M.n. nicht mehr das Problem. Natürlich muss, im Gegensatz zu PHP oder Ruby, der Code zur Zeit noch regelmäßig auf die Funktionalität geprüft werden (wenn man Node.js aktualisiert), da nicht unbedingt eine 100%ige Abwärtskompatibilität gegeben sein muss. Aber es ist nicht mehr so dramatisch wie vor einem Jahr.
Node.js ist eine sehr attraktive Alternative (ja, Alternative) zu Ruby, PHP und co. Applikationen wie Calipso zeigen eindrucksvoll, was wirklich mit Node.js machbar ist. Ich bin gegenwärtig am überlegen, warum man noch bei PHP bleiben sollte. Besonders bei Anwendungen die höherer Last ausgesetzt sind und wo man auch nur eine Anwendung auf einem Host betreibt, gebe ich Node.js den Vorzug. Für multi-node.js-Umgebungen (also Server auf denen mehrere Instanzen bzw. Anwendungen laufen), habe ich bisher noch nicht das Tool gefunden, welches das Organisieren ebendieser Anwendungen bequem ermöglicht (es gibt einige Tools, bisher aber noch nicht so bequem, dass Lieschen Müller damit umgehen könnte). Da bleiben Ruby und PHP *noch* erste Wahl. Ich kann mir aber vorstellen, dass sich dies bald ändern könnte.
Was natürlich auch beachtet werden muss, ist die Schwerfälligkeit der Using-Community. Also jener Webmaster, Hoster und IT-Anbieter, die später Node.js einsetzten müssten. Das Beispiel Nginx zeigt ja, dass bessere Software nicht unbedingt ein Argument für einen Umstieg ist. Anders kann man die herrschende Dominanz von Apachewebservern nicht erklären. Ähnlich kann/wird es wohl auch Node.js gehen. Ein Tool für Nerds, Geeks & Enterprise-Entwickler.
Ich habe zur Zeit 5 Webapplikationen im Einsatz, die skalieren deutlich besser als ihre Tomcat Vorgänger und sind sehr stabil.
Wer mehr über die technische Umsetzung von node.js und Co. erfahren will, für den haben wir ein fünfteiliges Tutorial zum Thema „Entwicklung einer Echzeit-Multiscreen-App“ in unserem Blog zusammengestellt: http://www.app-agentur-bw.de/blog/multiscreen-tutorial-chat-mit-websockets-teil-1-einfuehrung