Een eenvoudige webserver met Node.js

February 16th, 2013 | 10 min read | JavaScript, Node.js, NPM

In deze tweede tutorial rond Node.js ga ik een eenvoudige webserver maken met logging. In deze tutorial ga ik de modules http, url en fs gebruiken, maar tevens een module via de Node.js package manager (NPM).

Project opzetten

Als eerste stap ga ik de omgeving in orde brengen. Als eerste stap maak je een project map (ik geef het de naam web-server). In deze map maak je het bestand

web-server.js

aan en een submap

web

.
In de submap die je hebt aangemaakt gaan we enkele HTMLs plaatsen die door de web-server getoond moeten worden. Zelf ga ik een HTML template gebruiken die ik eerder gemaakt heb, maar je kan dus ook gewoon een index.html pagina aanmaken in deze map met wat afbeeldingen, CSS en JavaScript.

Als volgende stap moeten we, zoals ik eerder al zei, een package installeren met de package manager. Open een command prompt of terminal in je project map en gebruik het volgende commando:

npm install simple-mime

De package manager zal nu een map

node_modules

aanmaken met daarin de module simple-mime.

De code

Nu is er niets meer dat ons tegenhoud om een web server te maken. Open het bestand web-server.js en je kan nu beginnen met code schrijven.
Ik ga nu de hele code hier plaatsen en in de rest van de tutorial ga ik stuk voor stuk beschrijven en uitleggen wat het doet.

De code:

// Module imports
var http = require("http");
var url = require("url");
var fs = require("fs");
var getMime = require("simple-mime")("text/html");

var logFile = "web.log";

http.createServer(
    function (request, response) {
        var _PATH = url.parse(request.url, true).pathname;
        _PATH = "web" + (_PATH.charAt(_PATH.length - 1) == "/" ? "/index.html" : _PATH);

        fs.appendFile(logFile, request.method + " " + request.url + " HTTP/" + request.httpVersion + "\r\n");
        for (name in request.headers) {
            fs.appendFile(logFile, name + ": " + request.headers[name] + "\r\n");
        }

        request.on("end", function() {
            fs.appendFile(logFile, "\r\n");
            fs.exists(_PATH, function(isExisting) {
                var _DATA = "";
                if (isExisting) {
                    fs.readFile(_PATH, function(err, data) {
                        if (err) {
                            response.writeHead(500);
                            response.end();
                        } else {
                            response.writeHead(200, {
                                "Content-Type" : getMime(_PATH)
                            }); 
                            response.end(data, 'utf-8');
                        }
                    });
                } else {
                    response.writeHead(404);
                    response.end();
                }
            });
        });
}).listen(8090, function() {
    fs.exists(logFile, function(isExisting) {
        if (isExisting) {
            fs.rename(logFile, logFile + "." + Math.round((new Date()).getTime() / 1000), function() {
                fs.appendFile(logFile, "Server initialized\r\n\r\n");
            });
        } else {
            fs.appendFile(logFile, "Server initialized\r\n\r\n");
        }
    });
});

Module imports

Het eerste wat we doen alvorens code te schrijven is de nodige modules importeren.  Zoals eerder gezegd gaan we vier modules gebruiken met elk z’n eigen doel. Zo gaan we de  

http

module gebruiken om de HTTP webserver op te kunnen zetten, de

url

module om de binnenkomende URL te parsen, de

fs

module voor logging en controle of de bestanden op de web server aanwezig zijn en de

simple-mime

module om de juiste mimetype terug te geven voor de bestanden op de web-server.

De code hiervoor is:

var http = require("http");
var url = require("url");
var fs = require("fs");
var getMime = require("simple-mime")("text/html");

Alle modules-imports zien er hetzelfde uit met uitzondering van de simple-mime module. Zoals je in de simple-mime documentatie kan lezen kan je via een parameter meegeven wat de default mime-type moet worden.

Web server aanmaken

De volgende stap is dat we een web server kunnen aanmaken met de HTTP module. Hiervoor hebben we de volgende code geschreven:

http.createServer(
    function (request, response) {
            ...
        });
}).listen(8090, function() {
    ...
});

Op de plaatsen waar de

...

staat komt de implementatie. De eerste … duiden aan waar de implementatie gaat komen voor een request naar de web server. De tweede … daarentegen duiden aan wat er moet gebeuren als de server opgestart wordt.

De

listen()

functie die we aanroepen start de webserver op op een bepaalde poort, in dit geval poort 8090.

Je kan trouwens zien dat we hier de http-module gebruiken omdat we de

createServer()

functie aanroepen op de variabel http wat in het vorige code-voorbeeld geïnitialiseerd werd via de module import.

Server startup code

Het volgende stuk code is de code die we uitvoeren indien de server opstart, dat is dus de code die binnen de callback-functie van de

listen()

functie staat.

De code hiervoor is:

fs.exists(logFile, function(isExisting) {
    if (isExisting) {
        fs.rename(logFile, logFile + "." + Math.round((new Date()).getTime() / 1000), function() {
            fs.appendFile(logFile, "Server initialized\r\n\r\n");
        });
    } else {
        fs.appendFile(logFile, "Server initialized\r\n\r\n");
    }
});

Wat ik hier eigenlijk doe is dat ik een logfile aanmaak waar ik alle requests ga loggen. Maar omdat ik niet veel zin heb om te moeten zoeken ga ik een aparte logfile creëeren elke keer als ik de server opstart. Dit doe ik door de voorgaande log te hernoemen en het een timestamp te geven.

Als eerste stap moeten we dus controleren of er al een logfile bestaat, dit doen we met de

exists()

functie van de module fs. Hier zien we ook direct wat eigen is aan JavaScript en Node.js, het werken met callbacks.
Om een actie uit te voeren ALS een andere actie afgelopen is (in dit geval controleren of een bestand bestaat), dan werken we met callbacks.
Deze asynchrone aanpak zorgt ervoor dat je enorm hoge performantie kan verwachten omdat er nooit echt “gewacht” moet worden. Stel dat er bijvoorbeeld na de controle of het bestand bestaat nu nog iets zou gebeuren, dan zou de compiler niet wachten tot we weten of het bestand bestaat of niet, maar gaat het gewoon verder.
Als dan na een tijdje toch gecontroleerd is of het bestand bestaat of niet, dan wordt de callback uitgevoerd.

In dit geval bestaat de callback uit een functie met een boolean die aanduid of het bestand bestaat of niet. In de code gaan we deze boolean in een if gebruiken om als het bestand bestaat het voorgaande bestand te renamen. Dit doen we met de

rename()

functie van de fs-module.

Ook hier gaan we weer gebruik maken van een callback. Stel dat we in de nieuwe log direct een tekst willen plaatsen (in dit geval “Server initialized”), dan moeten we wachten totdat het oude bestand hernoemd is. Doen we dat niet, dan gaan we misschien de tekst in de oude logfile schrijven.

We moeten dus eerst wachten voordat het bestand hernoemd is, dit kunnen we bereiken via callbacks of een synchrone call. De meeste functies in de fs-module hebben namelijk ook een synchronized variant die eigenlijk ervoor zorgt dat de compiler wacht totdat de actie voltooid is alvorens verder te gaan. Raadpleeg hiervoor de documentatie.

Uiteraard moeten we, als het bestand niet bestond ook nog de “Server initialized” loggen.

Request loggen

De volgende stap is dat we de request headers gaan loggen. In principe doe je dit niet op productie-servers, maar dit is louter om te bewijzen dat het kan (en hoe).

De code die daarvoor nodig is is de volgende:

var _PATH = url.parse(request.url, true).pathname;
_PATH = "web" + (_PATH.charAt(_PATH.length - 1) == "/" ? "/index.html" : _PATH);

fs.appendFile(logFile, request.method + " " + request.url + " HTTP/" + request.httpVersion + "\r\n");
for (name in request.headers) {
    fs.appendFile(logFile, name + ": " + request.headers[name] + "\r\n");
}

Wat hier eigenlijk gebeurt is dat we eerst de binnen komende URL gaan parsen met de url-module. Alle gegevens die te maken hebben met de request komen binnen via het

request

-object.
In de for-lus gaan we alle aparte headers wegschrijven in de logfile.

Request beantwoorden

In het volgende (en laatste) stuk code gaan we ervoor zorgen dat de binnenkomende request ook beantwoord wordt met het juiste bestand. Indien dat er geen bestand gevonden wordt gaan we een 404 HTTP error terug geven (Not found). Indien we een fout krijgen met het openen van het bestand, dan geven we een 500 HTTP error terug (Internal Server Error).

De code die we daarvoor gebruiken is:

request.on("end", function() {
    fs.appendFile(logFile, "\r\n");
    fs.exists(_PATH, function(isExisting) {
        var _DATA = "";
        if (isExisting) {
            fs.readFile(_PATH, function(err, data) {
                if (err) {
                    response.writeHead(500);
                    response.end();
                } else {
                    response.writeHead(200, {
                        "Content-Type" : getMime(_PATH)
                    }); 
                    response.end(data, 'utf-8');
                }
            });
        } else {
            response.writeHead(404);
            response.end();
        }
    });
});

Wat we hier gaan doen is controleren of de file al dan niet bestaat. Het pad naar het bestand hebben we eerder al voorzien via de url-module en staat in

_PATH

.
De code hiervoor is dezelfde als we eerder gebruikt hebben om een nieuwe logfile aan te maken. We controleren of het bestand bestaat en via een callback gaan we daar dan iets mee doen.

Wat we er mee gaan doen is dat we het bestand gaan uitlezen via de functie

readFile()

, net zoals alle functies die we eerder gezien hebben in de fs-module is dit een asynchrone functie met een callback. Deze callback kan met twee argumenten werken, een fout (

err

) en de data (

data

).

Als er een fout is bij het lezen van het bestand, dan gaan we een HTTP 500 status code als antwoord geven. Dit doen we met de

writeHead()

functie van het response object. We moeten tevens aanduiden dat de response beëindigd kan worden met de

end()

functie.

Indien er geen probleem was bij het lezen van het bestand, dan gaan we de status code 200 meegeven (OK) en gaan we het mime-type van het bestand meegeven via de simple-mime module. De mime-type duidt aan over wat voor type bestand het gaat en hoe de web browser ermee om moet gaan.
Zo heb je graag dat een afbeelding getoond wordt als een afbeelding en niet als een tekst (maar ook omgekeerd wil je niet dat een tekst als een afbeelding getoond wordt). Hiervoor bestaan verschillende mime types zoals text/html, image/png, image/jpeg, text/javascript, … .
Eindigen doen we door de data van het bestand weg te schrijven.

Indien het bestand niet bestond, dan gaan we een 404 foutcode terug geven en net zoals alle andere cases moeten we ook hier de response beëindigen.

Uitvoeren met Node.js

De code hebben we nu volledig overlopen, nu is het tijd om het script uit te voeren. Open een terminal of command prompt op de locatie waar je JavaScript bestand staat en gebruik het volgende commando:

node web-server.js

Je zal merken dat er in diezelfde map nu een web.log bestand staat dat, als je het opent, de tekst “Server initialized” bevat.
Als je nu je browser opent en surft naar http://localhost:8090, dan zal je merken dat alles mooi werkt en je een werkende webpagina te zien krijgt.

Het log-bestand is nu ook verder aangevuld met alle requests (HTML pagina’s, CSS stylesheets, JavaScript bestanden en andere resources).

Als je nu ten slotte het script beëindigd (Ctrl +C) en opnieuw opstart, dan zal je merken dat het oude logbestand hernoemd is en er een nieuw (leeg) bestand met de naam web.log te vinden is.

log-result

Hiermee beëindigen we dan ook deze tweede tutorial rond Node.js.

Download project

Download – web-server.rar (1,3 MB)

Anything not clear? Feel free to contact me on Twitter or Keybase.