mitmario.dev

Der erste Server

Node.js Sandbox 4 Min Lesezeit 3 BeispieleLektion 1 von 8

Bisher war jedes Programm in diesem Kurs ein Auftrag. Es startete, tat etwas, und war fertig. Ein Server ist kein Auftrag, sondern ein Dienst: Er startet, und dann wartet er darauf, dass jemand etwas von ihm will.

Zwei Aufrufe, mehr braucht es nicht

createServer nimmt eine Funktion entgegen und gibt dir ein Server-Objekt zurück. listen legt diesen Server auf einen Port.

Der kürzeste Server
import { createServer } from "node:http";

const server = createServer((req, res) => {
  res.end("Guten Tag");
});

server.listen(3000, () => {
  console.log("Server hoert auf Port 3000");
});

Ein Port ist eine Nummer, unter der ein Programm auf einem Rechner erreichbar ist. Der Rechner hat eine Adresse, das Programm darauf eine Nummer, und beides zusammen ergibt die Stelle, an die eine Anfrage geht. Viel mehr Netzwerktheorie brauchst du für diesen Kurs nicht. Die eine Regel, die dir im Alltag wirklich begegnet, ist: Zwei Programme können nicht gleichzeitig denselben Port belegen. Wenn du irgendwann EADDRINUSE liest, ist genau das passiert, meistens weil ein alter Serverprozess von vorhin noch läuft. In der Aufgabe gleich kannst du dir den Fehler ansehen, ohne auf ihn zu warten.

Und dann steht da noch etwas: Das Programm endet nicht. Nach dem listen ist die letzte Zeile abgearbeitet, aber Node bleibt. Das ist kein Versehen, sondern dieselbe Regel wie in Lektion 1.2: Node beendet sich, wenn nichts mehr offen ist. Ein Server, der auf einem Port hört, ist die offenste Sache, die es gibt. Er wartet, also bleibt das Programm.

Beim ersten Beispiel kannst du dabei zusehen. Rechts im Reiter „Browser” steht die Antwort, und sie steht da auch noch, wenn du die Seite neu lädst. Niemand hat den Prozess wieder gestartet, er war die ganze Zeit da. Ein Serverprogramm hört eben nicht von selbst auf, und genau deshalb kannst du es anfassen, statt ihm beim Ablaufen zuzusehen.

Die Funktion läuft einmal je Anfrage

Die Funktion, die du an createServer übergibst, wird nicht einmal beim Start aufgerufen, sondern jedes Mal neu, wenn eine Anfrage hereinkommt. Bei zehn Aufrufern läuft sie zehnmal.

Zwei Anfragen, eine Funktion
import { createServer } from "node:http";

// Die Variable lebt ausserhalb der Funktion, also ueberlebt sie jede
// einzelne Anfrage. Die Funktion darunter faengt bei jeder neu an.
let anfragen = 0;

const server = createServer((req, res) => {
  anfragen = anfragen + 1;
  res.end(`Guten Tag. Das war Anfrage Nummer ${anfragen} auf ${req.url}`);
});

server.listen(3000);

Sie bekommt zwei Argumente, und die Namen req und res sind Gewohnheit, keine Vorschrift.

req ist alles, was hereinkommt. Welche Methode benutzt wurde, welche Adresse gemeint war, welche Kopfzeilen mitgeschickt wurden. Ein Objekt zum Lesen, an dem du nichts zu ändern hast. Was alles darin steckt, ist Lektion 8.3.

res ist der Block Papier, auf den die Antwort geschrieben wird. Ein Objekt zum Schreiben. Statuscode, Kopfzeilen, Inhalt, und am Ende ein Strich darunter.

Im Beispiel oben kannst du das nachzählen. Die Zahl steht in der Antwort, nicht im Programm: Lade die Seite rechts ein paarmal neu, und sie geht jedes Mal um eins hoch. Tipp eine andere Adresse in die Zeile darüber, etwa /irgendwas, und sie geht trotzdem weiter, denn es ist immer dieselbe Funktion. Das Terminal zählt mit:

curl http://localhost:3000/

curl http://localhost:3000/irgendwas

Die Variable anfragen steht dabei außerhalb der Funktion. Was drinnen steht, entsteht bei jeder Anfrage neu und ist danach weg. Was draußen steht, lebt so lange wie der Server.

Eine Antwort muss abgeschlossen werden

Das ist der Punkt, an dem in diesem Abschnitt die meiste Zeit verloren geht, deshalb steht er hier so früh.

Die vergessene Antwort
import { createServer } from "node:http";

const server = createServer((req, res) => {
  // Hier fehlt res.end(). Die Anfrage kommt an, die Funktion laeuft
  // durch, aber niemand schliesst die Antwort ab.
});

server.listen(3000);

Die Anfrage kommt an, die Funktion läuft durch. Und dann passiert nichts mehr. Kein Fehler, kein Absturz, keine Warnung. Die Seite im Reiter „Browser” lädt und hört nicht auf damit, der Aufrufer sitzt da und wartet, bis er von sich aus aufgibt. Im Terminal kannst du ihm eine Frist setzen und zusehen, wie sie abläuft:

curl --max-time 3 http://localhost:3000/

Nach drei Sekunden steht dort Operation timed out. Das ist alles, was ein Aufrufer je erfährt.

Der Grund ist, dass Node nicht wissen kann, ob du fertig bist. Vielleicht willst du noch eine zweite Zeile schreiben, vielleicht wartest du noch auf eine Datei. Erst res.end() sagt: Das war alles, schick es los.

Diese Sorte Fehler erkennst du in Zukunft am Verhalten. Ein Absturz gibt dir einen Stacktrace, ein falscher Statuscode gibt dir eine falsche Antwort. Aber wenn eine Anfrage einfach hängt und nach dreißig Sekunden mit einer Zeitüberschreitung endet, dann ist auf dem Weg ein res.end() ausgelassen worden, oder ein Zweig deiner Verzweigung erreicht es nie.

res.end() nimmt übrigens optional den Inhalt gleich mit, wie in den Beispielen oben. Du kannst stattdessen auch mehrfach res.write() aufrufen und am Ende ein leeres res.end() setzen. Für kurze Antworten ist die erste Form die übliche.

Zum Mitnehmen

createServer nimmt eine Funktion, listen legt sie auf einen Port. Und jede Antwort muss abgeschlossen werden, sonst wartet der Aufrufer bis er aufgibt.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.