Skip to content

Anatomie einer HTTP-Transaktion

Der Zweck dieses Leitfadens ist es, ein solides Verständnis des Prozesses der Node.js HTTP-Verarbeitung zu vermitteln. Wir gehen davon aus, dass Sie im Allgemeinen wissen, wie HTTP-Anfragen funktionieren, unabhängig von Sprache oder Programmierumgebung. Wir gehen auch davon aus, dass Sie mit Node.js EventEmitters und Streams ein wenig vertraut sind. Wenn Sie damit nicht ganz vertraut sind, lohnt es sich, die API-Dokumente für jedes dieser beiden Elemente kurz durchzulesen.

Den Server erstellen

Jede Node-Webserveranwendung muss irgendwann ein Webserverobjekt erstellen. Dies geschieht mit createServer.

javascript
const http = require('node:http')
const server = http.createServer((request, response) => {
  // Hier geschieht die Magie!
})

Die Funktion, die an createServer übergeben wird, wird für jede HTTP-Anfrage, die an diesen Server gerichtet wird, einmal aufgerufen, daher wird sie Anfragehandler genannt. Tatsächlich ist das von createServer zurückgegebene Serverobjekt ein EventEmitter, und was wir hier haben, ist nur eine Kurzform für das Erstellen eines Serverobjekts und das Hinzufügen des Listeners später.

javascript
const server = http.createServer()
server.on('request', (request, response) => {
  // Die gleiche Art von Magie passiert hier!
})

Wenn eine HTTP-Anfrage den Server erreicht, ruft Node die Anfragehandlerfunktion mit einigen praktischen Objekten für die Abwicklung der Transaktion, Anfrage und Antwort auf. Wir werden diese in Kürze behandeln. Um tatsächlich Anfragen zu bedienen, muss die Methode listen für das Serverobjekt aufgerufen werden. In den meisten Fällen müssen Sie an listen nur die Portnummer übergeben, an der der Server lauschen soll. Es gibt auch einige andere Optionen, daher sollten Sie die API-Referenz konsultieren.

Methode, URL und Header

Beim Behandeln einer Anfrage ist das Erste, was Sie wahrscheinlich tun möchten, einen Blick auf die Methode und die URL zu werfen, damit geeignete Maßnahmen ergriffen werden können. Node.js macht dies relativ schmerzlos, indem es dem Anfrageobjekt praktische Eigenschaften zuweist.

javascript
const { method, url } = request

Das Anfrageobjekt ist eine Instanz von IncomingMessage. Die Methode ist hier immer eine normale HTTP-Methode/Verb. Die URL ist die vollständige URL ohne Server, Protokoll oder Port. Bei einer typischen URL bedeutet dies alles nach und einschließlich des dritten Schrägstrichs.

Header sind auch nicht weit entfernt. Sie befinden sich in ihrem eigenen Objekt in der Anfrage namens headers.

javascript
const { headers } = request
const userAgent = headers['user-agent']

Es ist hier wichtig zu beachten, dass alle Header nur in Kleinbuchstaben dargestellt werden, unabhängig davon, wie der Client sie tatsächlich gesendet hat. Dies vereinfacht das Parsen von Headern für jeden Zweck.

Wenn einige Header wiederholt werden, werden ihre Werte überschrieben oder als durch Komma getrennte Zeichenketten zusammengefügt, je nach Header. In einigen Fällen kann dies problematisch sein, daher ist auch rawHeaders verfügbar.

Anforderungstext

Beim Empfang einer POST- oder PUT-Anfrage kann der Anforderungstext für Ihre Anwendung wichtig sein. Der Zugriff auf die Textdaten ist etwas aufwendiger als der Zugriff auf Anfrageheader. Das Anforderungsobjekt, das an einen Handler übergeben wird, implementiert die ReadableStream-Schnittstelle. Dieser Stream kann abgehört oder wie jeder andere Stream an anderer Stelle weitergeleitet werden. Wir können die Daten direkt aus dem Stream abrufen, indem wir auf die Ereignisse 'data' und 'end' des Streams hören.

Der in jedem 'data'-Ereignis ausgegebene Chunk ist ein Buffer. Wenn Sie wissen, dass es sich um Stringdaten handelt, ist es am besten, die Daten in einem Array zu sammeln und sie dann beim 'end' zu verketten und in einen String umzuwandeln.

javascript
let body = []
request.on('data', chunk => {
  body.push(chunk)
})
request.on('end', () => {
  body = Buffer.concat(body).toString()
  // An dieser Stelle enthält 'body' den gesamten Anforderungstext als String.
})

HINWEIS

Dies mag etwas mühsam erscheinen, und in vielen Fällen ist es das auch. Glücklicherweise gibt es Module wie concat-stream und body auf npm, die helfen können, einen Teil dieser Logik zu verbergen. Es ist wichtig, ein gutes Verständnis davon zu haben, was vor sich geht, bevor man diesen Weg geht, und deshalb sind Sie hier!

Eine kurze Anmerkung zu Fehlern

Da das Anforderungsobjekt ein ReadableStream ist, ist es auch ein EventEmitter und verhält sich wie einer, wenn ein Fehler auftritt.

Ein Fehler im Anforderungsstream äußert sich durch das Auslösen eines 'error'-Ereignisses im Stream. Wenn Sie keinen Listener für dieses Ereignis haben, wird der Fehler ausgelöst, was Ihr Node.js-Programm zum Absturz bringen könnte. Sie sollten daher einen 'error'-Listener zu Ihren Anforderungsstreams hinzufügen, auch wenn Sie ihn nur protokollieren und weitermachen. (Es ist jedoch wahrscheinlich am besten, eine Art HTTP-Fehlerantwort zu senden. Mehr dazu später.)

javascript
request.on('error', err => {
  // Dies gibt die Fehlermeldung und den Stack-Trace in stderr aus.
  console.error(err.stack)
})

Es gibt andere Möglichkeiten, diese Fehler zu behandeln, wie z. B. andere Abstraktionen und Tools, aber Sie sollten sich immer bewusst sein, dass Fehler auftreten können und auftreten, und dass Sie sich damit auseinandersetzen müssen.

Was wir bisher haben

Bisher haben wir die Erstellung eines Servers und das Erfassen der Methode, URL, Header und des Bodys aus Anfragen behandelt. Wenn wir das alles zusammenfügen, könnte es ungefähr so aussehen:

javascript
const http = require('node:http');

http.createServer((request, response) => {
    const { headers, method, url } = request;
    let body = [];
    request.on('error', err => console.error(err));
    request.on('data', chunk => {
        body.push(chunk);
    });
    request.on('end', () => {
        body = Buffer.concat(body).toString();
        // An diesem Punkt haben wir die Header, Methode, URL und den Body und können jetzt
        // alles tun, was wir tun müssen, um auf diese Anfrage zu antworten.
    });
});

.listen(8080); // Aktiviert diesen Server und lauscht auf Port 8080.

Wenn wir dieses Beispiel ausführen, können wir Anfragen empfangen, aber nicht auf sie antworten. Tatsächlich würde Ihre Anfrage bei diesem Beispiel in einem Webbrowser eine Zeitüberschreitung verursachen, da nichts an den Client zurückgesendet wird.

Bisher haben wir das Antwortobjekt, eine Instanz von ServerResponse, das ein WritableStream ist, noch gar nicht angesprochen. Es enthält viele nützliche Methoden zum Senden von Daten an den Client zurück. Das werden wir als nächstes behandeln.

HTTP-Statuscode

Wenn Sie ihn nicht festlegen, ist der HTTP-Statuscode einer Antwort immer 200. Natürlich rechtfertigt nicht jede HTTP-Antwort dies, und irgendwann werden Sie definitiv einen anderen Statuscode senden wollen. Um dies zu tun, können Sie die Eigenschaft statusCode setzen.

javascript
response.statusCode = 404 // Teilen Sie dem Client mit, dass die Ressource nicht gefunden wurde.

Es gibt einige andere Abkürzungen dafür, wie wir bald sehen werden.

Festlegen von Antwortheadern

Header werden über eine praktische Methode namens setHeader festgelegt.

javascript
response.setHeader('Content-Type', 'application/json')
response.setHeader('X-Powered-By', 'bacon')

Beim Festlegen der Header in einer Antwort wird die Groß- und Kleinschreibung ihrer Namen nicht beachtet. Wenn Sie einen Header wiederholt setzen, ist der zuletzt gesetzte Wert der Wert, der gesendet wird.

Explizites Senden von Header-Daten

Die Methoden zum Setzen der Header und des Statuscodes, die wir bereits besprochen haben, gehen davon aus, dass Sie "implizite Header" verwenden. Das bedeutet, dass Sie sich darauf verlassen, dass Node die Header zum richtigen Zeitpunkt für Sie sendet, bevor Sie mit dem Senden von Body-Daten beginnen.

Wenn Sie möchten, können Sie die Header explizit in den Antwort-Stream schreiben. Dazu gibt es eine Methode namens writeHead, die den Statuscode und die Header in den Stream schreibt.

Explizites Senden von Header-Daten

javascript
response.writeHead(200, {
  'Content-Type': 'application/json',
  'X-Powered-By': 'bacon',
})

Sobald Sie die Header gesetzt haben (entweder implizit oder explizit), sind Sie bereit, mit dem Senden von Antwortdaten zu beginnen.

Senden des Antwort-Bodies

Da das Antwortobjekt ein WritableStream ist, ist das Schreiben eines Antwort-Bodies an den Client nur eine Frage der Verwendung der üblichen Stream-Methoden.

javascript
response.write('<html>')
response.write('<body>')
response.write('<h1>Hallo, Welt!</h1>')
response.write('</body>')
response.write('</html>')
response.end()

Die end-Funktion für Streams kann auch optionale Daten entgegennehmen, die als letztes Datenstück im Stream gesendet werden sollen, sodass wir das obige Beispiel wie folgt vereinfachen können.

javascript
response.end('<html><body><h1>Hallo, Welt!</h1></body></html>')

HINWEIS

Es ist wichtig, den Status und die Header festzulegen, bevor Sie mit dem Schreiben von Datenblöcken in den Body beginnen. Dies ist sinnvoll, da die Header in HTTP-Antworten vor dem Body stehen.

Noch eine kurze Sache zu Fehlern

Der Antwort-Stream kann auch 'error'-Ereignisse ausgeben, und irgendwann müssen Sie sich auch damit auseinandersetzen. Alle Ratschläge für Fehler im Anforderungs-Stream gelten auch hier.

Alles zusammenfügen

Nachdem wir nun gelernt haben, wie man HTTP-Antworten erstellt, wollen wir alles zusammenfügen. Aufbauend auf dem früheren Beispiel werden wir einen Server erstellen, der alle Daten zurücksendet, die uns vom Benutzer gesendet wurden. Wir werden diese Daten mit JSON.stringify als JSON formatieren.

javascript
const http = require('node:http')
http
  .createServer((request, response) => {
    const { headers, method, url } = request
    let body = []
    request
      .on('error', err => {
        console.error(err)
      })
      .on('data', chunk => {
        body.push(chunk)
      })
      .on('end', () => {
        body = Buffer.concat(body).toString()
        // BEGINN DER NEUEN SACHEN
        response.on('error', err => {
          console.error(err)
        })
        response.statusCode = 200
        response.setHeader('Content-Type', 'application/json')
        // Hinweis: Die beiden Zeilen oben könnten durch die folgende Zeile ersetzt werden:
        // response.writeHead(200, {'Content-Type': 'application/json'})
        const responseBody = { headers, method, url, body }
        response.write(JSON.stringify(responseBody))
        response.end()
        // Hinweis: Die beiden Zeilen oben könnten durch die folgende Zeile ersetzt werden:
        // response.end(JSON.stringify(responseBody))
        // ENDE DER NEUEN SACHEN
      })
  })
  .listen(8080)

EchoServer Beispiel

Vereinfachen wir das vorherige Beispiel zu einem einfachen Echo-Server, der einfach alle Daten, die in der Anfrage empfangen werden, direkt in der Antwort zurücksendet. Alles, was wir tun müssen, ist, die Daten aus dem Anfrage-Stream zu holen und diese Daten in den Antwort-Stream zu schreiben, ähnlich wie wir es zuvor getan haben.

javascript
const http = require('node:http');

http.createServer((request, response) => {
    let body = [];
    request.on('data', chunk => {
        body.push(chunk);
    });
    request.on('end', () => {
        body = Buffer.concat(body).toString();
        response.end(body);
    });
});

.listen(8080);

Lassen Sie uns dies nun optimieren. Wir möchten nur unter den folgenden Bedingungen ein Echo senden:

  • Die Anfrage-Methode ist POST.
  • Die URL ist /echo.

In jedem anderen Fall möchten wir einfach mit einem 404 antworten.

javascript
const http = require('node:http')
http
  .createServer((request, response) => {
    if (request.method === 'POST' && request.url === '/echo') {
      let body = []
      request
        .on('data', chunk => {
          body.push(chunk)
        })
        .on('end', () => {
          body = Buffer.concat(body).toString()
          response.end(body)
        })
    } else {
      response.statusCode = 404
      response.end()
    }
  })
  .listen(8080)

HINWEIS

Indem wir die URL auf diese Weise überprüfen, führen wir eine Form von "Routing" durch. Andere Formen des Routings können so einfach wie switch-Anweisungen oder so komplex wie ganze Frameworks wie express sein. Wenn Sie nach etwas suchen, das nur Routing macht, versuchen Sie router.

Großartig! Versuchen wir nun, dies zu vereinfachen. Denken Sie daran, dass das Anfrageobjekt ein ReadableStream und das Antwortobjekt ein WritableStream ist. Das bedeutet, dass wir pipe verwenden können, um Daten von einem zum anderen zu leiten. Das ist genau das, was wir für einen Echo-Server wollen!

javascript
const http = require('node:http')

http
  .createServer((request, response) => {
    if (request.method === 'POST' && request.url === '/echo') {
      request.pipe(response)
    } else {
      response.statusCode = 404
      response.end()
    }
  })
  .listen(8080)

Juhu, Streams!

Wir sind aber noch nicht ganz fertig. Wie in diesem Leitfaden mehrfach erwähnt, können Fehler auftreten und tun dies auch, und wir müssen uns damit auseinandersetzen.

Um Fehler im Anfrage-Stream zu behandeln, protokollieren wir den Fehler nach stderr und senden einen 400-Statuscode, um eine Bad Request anzuzeigen. In einer realen Anwendung würden wir jedoch den Fehler untersuchen wollen, um herauszufinden, welcher der korrekte Statuscode und die korrekte Nachricht wäre. Wie üblich bei Fehlern sollten Sie die Fehlerdokumentation konsultieren.

Auf der Antwort protokollieren wir den Fehler nur nach stderr.

javascript
const http = require('node:http')

http
  .createServer((request, response) => {
    request.on('error', err => {
      console.error(err)
      response.statusCode = 400
      response.end()
    })
    response.on('error', err => {
      console.error(err)
    })
    if (request.method === 'POST' && request.url === '/echo') {
      request.pipe(response)
    } else {
      response.statusCode = 404
      response.end()
    }
  })
  .listen(8080)

Wir haben nun die meisten Grundlagen der Bearbeitung von HTTP-Anfragen behandelt. An diesem Punkt sollten Sie in der Lage sein:

  • Einen HTTP-Server mit einer request-Handler-Funktion zu instanziieren und ihn an einem Port abhören zu lassen.
  • Header, URL, Methode und Körperdaten von request-Objekten zu erhalten.
  • Routing-Entscheidungen basierend auf der URL und/oder anderen Daten in request-Objekten zu treffen.
  • Header, HTTP-Statuscodes und Körperdaten über response-Objekte zu senden.
  • Daten von request-Objekten zu leiten und zu response-Objekten zu leiten.
  • Stream-Fehler sowohl im request- als auch im response-Stream zu behandeln.

Aus diesen Grundlagen können Node.js HTTP-Server für viele typische Anwendungsfälle aufgebaut werden. Es gibt noch viele andere Dinge, die diese APIs bereitstellen. Lesen Sie daher unbedingt die API-Dokumentation für EventEmitters, Streams und HTTP.