JavaScript Server Node.js
Willemers Informatik-Ecke

Aktueller Stand auf nodejs.htm

Im weiteren die alte Version ...

Node.js

Node.js ist eine JavaScript-Programmierplattform, die besonders für die Netzwerkprogrammierung geeignet ist, Mit Node.js können dynamische HTTP-Server realisiert werden.

Node.js basiert auf der V8-Javascript-Engine von Google.

Installation von node.js

Linux aus dem Repository

Unter Linux wird das Paket nodejs installiert. Sinnvollerweise sollte auch gleich das Paket npm installiert werden. Für Debian, Linux Mint oder Ubuntu lautet der Konsolenbefehl zur Installation:
sudo apt install nodejs npm

Aus dem Internet

Node kann von der Webseite https://nodejs.org heruntergeladen werden.

Linux aus dem Internet

Diese Vorgehensweise ist nicht empfehlenswert. Sinnvoller ist die Installation aus dem Repository.

Für Linux bietet die Seite das Paket den Namen node-vxx.xx.x-linux-x64.tar.xz an. xx steht für Versionsnummern. Die Erweiterung xz wird nicht per gunzip, sondern per xz -d dekomprimiert werden. Alternativ kann auch tar mit der Option -J verwendet werden.

tar xfJ Downloads/node-vxx.xx.x-linux-x64.tar.xz
Es entsteht ein Verzeichnis node-vxx.xx.x-linux-x64. Darin befindet sich ein Verzeichnis bin, indem sich die Befehle node, npm und npx stehen.

Unter Other Downloads wird aber auch eine Linux-Binary für die Installation angeboten. (nicht getestet)

Aufruf

Der Befehl node startet einen Interpreter für JavaScript. Nun können Javascript-Befehle direkt eingegeben werden. Der Interpreter kann mit der Tastenkombination [Strg]+[D] wieder verlassen werden.

Typischerweise wird er mit einer JavaScript-Datei als Argument aufgerufen.

node hello.js
In die Datei hello.js kann man einen einfachen Ausgabebefehl für die Console schreiben:
console.log("Hallo Welt")
Anschließend erscheint Hallo Welt auf dem Bildschirm.

Serverprogramm für HTTP

Durch die Einbindung des HTTP-Moduls kann ein HTTP-Server erstellt werden.
const http = require("http"); // Einbinden von http
  
var server = http.createServer(requestCallback);
Ein http-Server wird erzeugt und in der Variablen server abgelegt. Für Aktivitäten des Servers wird die Funktion requestCallback übergeben. Der Server wird sie aufrufen, sobald ein Client den Server aktiviert. Da sie erst bei einem späteren Ereignis aktiv wird, nennt man eine solche Funktion Callback-Funktion. Sie wird weiter unten näher beschrieben.

Nachdem der Server erzeugt ist, wird er mit dem Aufruf von listen gestartet und mit einem festgelegten Port und einem Netzwerkadapter verbunden. Als dritter Parameter wird wieder eine Funktion aufgerufen, die bei Start des Servers aktiv wird.

const http = require("http"); // Einbinden von http
  
var server = http.createServer(requestCallback);
server.listen(8080, "localhost", listener);
Das Programm wird daraufhin blockieren. Der Port wird hier mit 8080 festgelegt und als Netzwerkadapter einfach localhost verwendet. Bei einem Gerät mit mehreren Netzwerkanschlüssen kann hier dafür gesorgt werden, dass der Server nur über eine Schnittstelle erreichbar ist.

Callback-Funktion für createServer

Die Callback-Funktion für createServer muss zwei Parameter aufweisen.
  1. Der erste Parameter enthält die Details zu der Anfrage des Clients und wird darum üblicherweise request oder auch kurz req genannt.
  2. Der zweite Parameter enthält die Antwort des Servers und wird durch die Callback-Funktion gefüllt.
function requestCallback(request, response) {
    // In den Response-Kanal wird geschrieben
    response.write("Hallo Welt");
    // Der Response-Kanal wird abgeschlossen
    response.end();
}
Im ersten Versuch werden die Daten des Clients nicht näher untersucht und damit bleibt der Parameter request ungenutzt. Als response wird eine Zeichenkette (Hallo Welt) per write in die Variable response geschrieben. Hier könnte beliebiger HTML-Code stehen. Mit dem Aufruf von end wird die Ausgabe abgeschlossen.

Callback-Funktion für listen

Die Funktion listener wird bei Start des Servers mit listen aufgerufen. Hier kann eine Meldung auf dem Bildschirm ausgegeben werden.
function listener() {
    console.log("Server gestartet");
}

Anonyme Funktionen

Da die Callback-Funktionen nicht nur an einer Stelle aufgerufen werden, kann sie auch direkt im Parameter definiert werden.
server.listen(8080, "localhost", listener);

function listener() {
    console.log("Server gestartet");
}
Die Funktion wird direkt bei Aufruf implementiert.
server.listen(8080, "localhost", function () {
    console.log("Server gestartet");
}
);
Da die Funktion nur hier verwendet wird, kann auf den Namen verzichtet werden.

Auf die gleiche Art und Weise wird auch createServer mit seiner Callback-Funktion vereint.

var server = http.createServer(function(request, response) {
    response.write("Hallo Welt");
    response.end();
});
Wer damit angeben will, dass er den Begriff Lambda schon einmal gehört hat, kann den sogenannten Fat-Arrow verwenden, der lediglich verschleiert, dass es sich um eine Funktion handelt. Dazu wird das Schlüsselwort function entfernt und hinter der Parameterklammer ein Gleichheitszeichen und ein Größerzeichen kombiniert.
var server = http.createServer(function(request, response) {
// Syntaktische Variante einer anonymen Funktion
var server = http.createServer((request, response) => {
Das Gesamtkunstwerk sieht (ohne syntaktischen Zucker) so aus:
const http = require("http"); // Einbinden von http
  
var server = http.createServer(function(request, response) {
    response.write("Hallo Welt");
    response.end();
});

server.listen(8080, "localhost", function() {
    console.log("Server gestartet");
});

Start und Test des HTTP-Servers

Mit dem Aufruf über node wird der Server gestartet. Er blockiert dabei die Terminalsitzung, da ein Server dauernd läuft.
node httpserver.js
Nun läuft der Server im Hintergrund und wartet auf Anfragen über den Port 8080. Um das zu testen, wird ein Browser gestartet und als Adresse localhost:8080 eingegeben. Es sollte "Hallo Welt" auf dem Browser erscheinen.

Umgang mit den Anfrage- und Antwortparametern

Auslesen des Anfrageparameters request

Aus dem Parameter request können diverse Informationen über den Anfrager gewonnen werden, wie die angefragte URL, die den Pfad innerhalb des Servers angibt, oder die HTTP-Methode.
const url = request.url;
const method = request.method;
Mit dem folgenden Aufruf können Methode und aufgerufene URL mit einer Zuweisung ausgelesen werden:
const { method, url } = request;
Es können auch die Header-Bereiche des HTTP-Pakets ausgelesen werden. So zeigt die folgende Sequenz den Browser des Aufrufers auf der Console an:
const {headers} = request; // oder: const headers = request.headers;
console.log(`Aufgerufen mit dem Browser: ${headers['user-agent']}`);
Der Header kann dann wieder über die Attribute ausgelesen werden, wie hier der userAgent, also der Browser, mit dem der Server angefragt wurde.

Antworten an den Client: response

Die Antwortumgebung befindet sich im Parameter response.

Dateninhalte

Durch die Methode write können die Inhalte an den Client gesendet werden.

response.write("Hallo Welt");

Mit der Methode end wird das Ende der Übertragung signalisiert.

response.end(); 
Man kann auch write und end zusammenfassen, indem die Ausgabe als Parameter an end übergeben wird.
response.end("Hallo nochmal");

Header

Headerinformationen können durch Eintragung in die Attribute erreicht werden. Beispielsweise kann der Status-Code folgendermaßen an den Client gemeldet werden:
response.statusCode = 404;
Es kann allerdings auch die Methode writeHead verwendet werden.
response.writeHead(200);
Die Statusausgabe in writeHead kann durch die Headerattribute ergänzt werden.
response.writeHead(201, {"content-type": "text/plain"});
Der folgende Server verändert die Parameter.
const http = require("http"); // Einbinden von http
  
var server = http.createServer(function (request, response) {
    const {headers, method, url} = request;
    console.log(url);
    console.log(method);
    console.log(headers['user-agent']);
    response.writeHead(201, {"content-type": "text/plain"});
    response.end("Hallo Welt"); // Browser-Inhalt
});
server.listen(8080, "localhost", function () {
    console.log("Server gestartet");
});
Zum Test wird der Befehl curl verwendet.
$ curl -v http://localhost:8080
* Rebuilt URL to: http://localhost:8080/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 201 Created
< content-type: text/plain
< Date: Sun, 28 Nov 2021 09:21:29 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
Hallo Welt

Auf dem Server gab es durch den Aufruf von console.log folgende Zeilen:

$ node server1.js 
Server gestartet
/
GET
curl/7.58.0

Ein Webserver, der Dateien ausgibt

Neben dynamischen Ausgaben an den Client werden viele Daten auch aus statischen Dateien ausgeliefert. Unterstützung liefert das Modul fs, das genauso per require eingebunden wird wie http.

Die Methode fs.readFile erhält als Parameter den Pfad zu der Datei und eine Callback-Funktion, die asynchron abläuft. Der Callback-Funktion werden in ihren zwei Parametern error und data entweder die Fehlersituation oder im Erfolgsfall der Dateninhalt der Datei übergeben.

const http = require("http"); // Einbinden von http
const fs = require("fs"); // Einbinden von fs
// ...
        fs.readFile("index.htm", function(error, data) {
            if (data) {
                response.writeHead(200);
                response.end(data.toString());
            } else { // error
                response.writeHead(500);
                response.end(error.toString());
            }
        });
Es ist zu berücksichtigen, dass readFile asynchron läuft. Node wartet also nicht, bis die Datei fertig gelesen ist, sondern läuft weiter. Darum muss die Antwort an den Client innerhalb der Callback-Funktion von readFile erfolgen.

Unterscheidung der URL und Lesen der Dateien

Um eine Datei zu senden, muss sie vom Client angefordert werden. Der hinterlässt seinen Wunsch typischerweise in der URL. Das folgende Beispielprogramm liefert die Datei index.htm, wenn der Client localhost:8080/index als URL angibt.
const http = require("http"); // Einbinden von http
const fs = require("fs"); // Einbinden von fs

var server = http.createServer(function(request, response) {
    if (request.url === "/index") {
        fs.readFile("index.htm", function(error, data) {
            if (data) {
                response.setHeader('Content-Type', 'text/html');
                response.writeHead(200);
                response.end(data.toString());
            } else { // error
                response.writeHead(500);
                response.write(error.toString());
                response.end("\nDateifehler");
            }
        });
    } else {
        response.writeHead(404);
        response.end("Nichts da");
    }
});
server.listen(8080, "localhost", function () {
    console.log("Server gestartet");
});

POST-Formular und Auswertung

Für die Auswertung von Web-Formularen muss einerseits eine einfache HTML-Datei an den Client geliefert werden und anschließend auf die POST-Methode des Formulars reagiert werden.

Die Datei formular.htm enthält im Kern eine FORM:

<form action="/newuser" method="POST">
    <label>User: </label>
    <input type="text" name="user">
    <label>E-Mail: </label>
    <input type="text" name="email">
    <button>OK</button>
</form>
Diese Datei muss vom Server an den Client gesandt werden, sobald die URL / angesprochen wird.
const http = require("http"); // Einbinden von http
const fs = require("fs"); // Einbinden von fs

var server = http.createServer(function(request, response) {
    if (request.url === "/") { // sende die Formulardatei
        fs.readFile("formular.htm", function(error, data) {
            if (data) {
                response.setHeader('Content-Type', 'text/html');
                response.writeHead(200);
                response.end(data.toString());
            } else { // error
                response.writeHead(500);
                response.write(error.toString());
                response.end("\nDateifehler");
            }
        });
Nach dem Ausfüllen des Formulars wird der OK-Button eine POST-Anfrage mit der Action /newuser auslösen. Die muss aufgefangen und bearbeitet werden.
var server = http.createServer(function(request, response) {
    if (request.url === "/") { // sende die Formulardatei
        // ... wie oben gezeigt
    } else if (request.url==='/newuser' && request.method==='POST') {
         // werte POST mit action /newuser aus ... siehe unten
    } else { // irgendetwas ist schiefgelaufen
        response.writeHead(404);
        response.end("Nichts da");
    }
});
server.listen(8080, "localhost", function () {
    console.log("Server gestartet");
});
Wenn per POST Daten kommen, müssen diese nicht in einem Rutsch kommen. Darum werden die eingehenden Pakete nach data und nach end unterschieden. Erst beim Eintreffen des end ist alles angekommen und kann ausgewertet werden.
    } else if (request.url==='/newuser' && request.method==='POST') {
         // werte POST des Formulars mit action /newuser aus
         let postData = "";
         request.on("data", function(chunk) {
             postData += chunk;
         });
         request.on("end", function() {
             // postData enthält etwas wie: user=Huhu&email=huhu%40provider.de
             // ...
         });
    } else { // irgendetwas ist schiefgelaufen
Die Variable postData enthält nun die Zuweisungspaare, die jeweils durch Ampersands unterteilt sind. Diese lassen sich durch die Parse-Methode von querystring so zerlegen, dass sie anschließend über Assozationen zugegriffen werden können:
             const msg = "Neuer Benutzer: "+params['user']+
                     " mit Mail-Adresse: "+params['email'];
             response.writeHead(200);
             response.end(msg);
Dazu muss per require aber querystring eingebunden werden.
const qs = require("querystring");
Zusammengesetzt sieht der komplette Server so aus:
const http = require("http"); // Einbinden von http
const fs = require("fs"); // Einbinden von fs
const qs = require("querystring"); // Auswerten von Query-Strings

var server = http.createServer(function(request, response) {
    if (request.url === "/") { // sende die Formulardatei
        fs.readFile("formular.htm", function(error, data) {
            if (data) {
                response.setHeader('Content-Type', 'text/html');
                response.writeHead(200);
                response.end(data.toString());
            } else { // error
                response.writeHead(500);
                response.write(error.toString());
                response.end("\nDateifehler");
            }
        });
    } else if (request.url==='/newuser' && request.method==='POST') {
         // werte POST des Formulars mit action /newuser aus
         let postData = "";
         // Die Daten kommen in mehreren data-Blöcken
         request.on("data", function(chunk) {
             postData += chunk;
         });
         // Die Daten werden mit einem end-Block abgeschlossen
         request.on("end", function() {
             console.log(postData); // user=Huhu&email=huhu%40provider.de
             const params = qs.parse(postData);
             const msg = "Neuer Benutzer: "+params['user']+
                     " mit Mail-Adresse: "+params['email'];
             response.writeHead(200);
             response.end(msg);
         });
    } else { // irgendetwas ist schiefgelaufen
        response.writeHead(404);
        response.end("Nichts da");
    }
});
server.listen(8080, "localhost", function () {
    console.log("Server gestartet");
});
Wird dies über localhost:8080 aufgerufen, erscheint folgendes Formular.

Nach Klicken auf OK erscheint folgende Meldung:

Paketerstellung mit npm

Ein Paketverzeichnis wird angelegt und mit npm init initialisiert.
mkdir projekt
cd projekt
npm init
Es werden einige Eingaben eingefordert, die man alle einfach durch Bestätigen abhaken kann. Anschließend existiert eine Datei package.json, die die Vorgaben enthält.

expressjs

const express = require('express');
const path = require('path');

const PORT = 8080;
const HOST = 'localhost';

const app = express();
app.use(express.static(path.join(__dirname, 'static')));
app.get('/', function(request, response) {
    const pathToFile = path.join(__dirname, 'static', 'html', 'index.html');
    response.sendFile(pathToFile);
});
const server = app.listen(PORT, function() {
    console.log(`Server running http://${HOST}:${PORT}`);
});

Eigene Module

Exportierbare Funktionen oder Variablen müssen mit dem Schlüsselwort export eingeleitet werden.

Routing

Unter Routing versteht man in diesem Zusammenhang das Umleiten der Anfragen aufgrund der HTTP-Methoden und des URL-Pfads.

Der Pfad nach der Basis-URI kann auf Ressourcen umgelegt werden. Der Pfad wird über das Attribut url aus der request-Variable ausgelesen.

const url = request.url;
const method = request.method;

if(method == 'GET' && url ==='/about') {
    response.write(' Wir sind wir '); 
    response.end(); 
 } else if(method == 'POST' && url ==='/contact') {
    response.write(' Don't call us - we call you.'); 
    response.end(); 
 } else {
    response.write('Hallo!'); 
    response.end(); 
 }

REST-Server

Links