Wie Node.js Web-Frameworks wirklich funktionieren? (Inside Express.js & Next.js)
Grace Collins
Solutions Engineer · Leapcell

So schreiben Sie ein Web-Framework mit dem Node.js-HTTP-Modul
Bei der Entwicklung von Webanwendungen mit Node.js ist das http
-Modul eine grundlegende und entscheidende Komponente. Mit seiner Hilfe können Sie mit wenigen Codezeilen einen HTTP-Server starten. Als Nächstes werden wir uns eingehend damit befassen, wie man mit dem http
-Modul ein einfaches Web-Framework schreibt und den gesamten Prozess von der Ankunft einer HTTP-Anfrage bis zur Antwort versteht.
Starten des HTTP-Servers
Das folgende ist ein einfaches Node.js-Codebeispiel zum Starten eines HTTP-Servers:
'use strict'; const { createServer } = require('http'); createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World\n'); }) .listen(3000, function () { console.log('Listening on port 3000') });
Wenn Sie den obigen Code ausführen und den Befehl curl localhost:3000
im Terminal verwenden, sehen Sie die vom Server zurückgegebene Meldung Hello World
. Dies liegt daran, dass Node.js viele Details im Quellcode gekapselt hat und der Hauptcode in Dateien wie lib/_http_*.js
gespeichert ist. Als Nächstes werden wir die Quellcode-Implementierung von der Ankunft einer HTTP-Anfrage bis zur Antwort im Detail untersuchen.
Behandlung von HTTP-Anfragen
Erstellen einer Serverinstanz
In Node.js müssen Sie zuerst eine Instanz der Klasse http.Server
erstellen und auf deren request
-Ereignis lauschen, um HTTP-Anfragen zu empfangen. Da sich das HTTP-Protokoll auf der Anwendungsschicht befindet und die darunter liegende Transportschicht normalerweise das TCP-Protokoll verwendet, ist die Klasse net.Server
die übergeordnete Klasse der Klasse http.Server
. Der HTTP-spezifische Teil wird durch das Abhören des connection
-Ereignisses einer Instanz der Klasse net.Server
gekapselt:
// lib/_http_server.js // ... function Server(requestListener) { if (!(this instanceof Server)) return new Server(requestListener); net.Server.call(this, { allowHalfOpen: true }); if (requestListener) { this.addListener('request', requestListener); } // ... this.addListener('connection', connectionListener); // ... } util.inherits(Server, net.Server);
Parsen von Anforderungsdaten
Zu diesem Zeitpunkt ist ein HTTP-Parser erforderlich, um die über TCP übertragenen Daten zu parsen:
// lib/_http_server.js const parsers = common.parsers; // ... function connectionListener(socket) { // ... var parser = parsers.alloc(); parser.reinitialize(HTTPParser.REQUEST); parser.socket = socket; socket.parser = parser; parser.incoming = null; // ... }
Es ist erwähnenswert, dass der Parser parser
aus einem "Pool" bezogen wird und dieser "Pool" die Datenstruktur Free List
verwendet. Der Zweck besteht darin, den Parser so weit wie möglich wiederzuverwenden, um den durch häufige Aufrufe des Konstruktors verursachten Leistungsverbrauch zu vermeiden, und es gibt auch eine Obergrenze für die Anzahl (im http
-Modul sind es 1000):
// lib/freelist.js 'use strict'; exports.FreeList = function(name, max, constructor) { this.name = name; this.constructor = constructor; this.max = max; this.list = []; }; exports.FreeList.prototype.alloc = function() { return this.list.length ? this.list.pop() : this.constructor.apply(this, arguments); }; exports.FreeList.prototype.free = function(obj) { if (this.list.length < this.max) { this.list.push(obj); return true; } return false; };
Da die Daten kontinuierlich über TCP übertragen werden, arbeitet der Parser ereignisbasiert, was mit der Kernidee von Node.js übereinstimmt. Die Bibliothek http-parser
wird verwendet:
// lib/_http_common.js // ... const binding = process.binding('http_parser'); const HTTPParser = binding.HTTPParser; const FreeList = require('internal/freelist').FreeList; // ... var parsers = new FreeList('parsers', 1000, function() { var parser = new HTTPParser(HTTPParser.REQUEST); // ... parser[kOnHeaders] = parserOnHeaders; parser[kOnHeadersComplete] = parserOnHeadersComplete; parser[kOnBody] = parserOnBody; parser[kOnMessageComplete] = parserOnMessageComplete; parser[kOnExecute] = null; return parser; }); exports.parsers = parsers; // lib/_http_server.js // ... function connectionListener(socket) { parser.onIncoming = parserOnIncoming; }
Eine vollständige HTTP-Anfrage durchläuft von der Entgegennahme bis zur vollständigen Analyse nacheinander die folgenden Ereignis-Listener auf dem Parser:
parserOnHeaders
: Analysiert kontinuierlich die eingehenden Anfrageheader-Daten.parserOnHeadersComplete
: Nachdem der Anfrageheader analysiert wurde, erstellt er dasheader
-Objekt und einehttp.IncomingMessage
-Instanz für den Anfragebody.parserOnBody
: Analysiert kontinuierlich die eingehenden Anfragebody-Daten.parserOnExecute
: Nachdem der Anfragebody analysiert wurde, wird geprüft, ob ein Fehler bei der Analyse vorliegt. Wenn ein Fehler vorliegt, wird direkt das EreignisclientError
ausgelöst. Wenn die Anfrage die MethodeCONNECT
verwendet oder einenUpgrade
-Header hat, wird direkt das Ereignisconnect
oderupgrade
ausgelöst.parserOnIncoming
: Behandelt die analysierte spezifische Anfrage.
Auslösen des Ereignisses request
Das Folgende ist der Schlüsselcode des Listeners parserOnIncoming
, der das Auslösen des endgültigen Ereignisses request
abschließt:
// lib/_http_server.js // ... function connectionListener(socket) { var outgoing = []; var incoming = []; // ... function parserOnIncoming(req, shouldKeepAlive) { incoming.push(req); // ... var res = new ServerResponse(req); if (socket._httpMessage) { // Wenn true, bedeutet dies, dass der Socket von einer vorherigen ServerResponse-Instanz in der Warteschlange belegt wird outgoing.push(res); } else { res.assignSocket(socket); } res.on('finish', resOnFinish); function resOnFinish() { incoming.shift(); // ... var m = outgoing.shift(); if (m) { m.assignSocket(socket); } } // ... self.emit('request', req, res); } }
Es ist ersichtlich, dass der Quellcode für Anfragen, die vom selben socket
gesendet werden, zwei Warteschlangen verwaltet, die zum Zwischenspeichern von IncomingMessage
-Instanzen bzw. entsprechenden ServerResponse
-Instanzen verwendet werden. Die frühere ServerResponse
-Instanz belegt zuerst den socket
und wartet auf dessen finish
-Ereignis. Wenn das Ereignis ausgelöst wird, werden die ServerResponse
-Instanz und die entsprechende IncomingMessage
-Instanz aus ihren jeweiligen Warteschlangen freigegeben.
Antworten auf HTTP-Anfragen
In der Antwortphase sind die Dinge relativ einfach. Die eingehende ServerResponse
hat bereits den socket
erhalten. Die http.ServerResponse
erbt von der internen Klasse http.OutgoingMessage
. Wenn ServerResponse#writeHead
aufgerufen wird, setzt Node.js die Header-Zeichenkette zusammen und speichert sie in der Eigenschaft _header
der ServerResponse
-Instanz:
// lib/_http_outgoing.js // ... OutgoingMessage.prototype._storeHeader = function(firstLine, headers) { // ... if (headers) { var keys = Object.keys(headers); var isArray = Array.isArray(headers); var field, value; for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; if (isArray) { field = headers[key][0]; value = headers[key][1]; } else { field = key; value = headers[key]; } if (Array.isArray(value)) { for (var j = 0; j < value.length; j++) { storeHeader(this, state, field, value[j]); } } else { storeHeader(this, state, field, value); } } } // ... this._header = state.messageHeader + CRLF; }
Unmittelbar danach, wenn ServerResponse#end
aufgerufen wird, werden die Daten nach der Header-Zeichenkette angefügt, das entsprechende Ende hinzugefügt und dann in die TCP-Verbindung geschrieben. Der spezifische Schreibvorgang erfolgt in der internen Methode ServerResponse#_writeRaw
:
// lib/_http_outgoing.js // ... OutgoingMessage.prototype.end = function(data, encoding, callback) { // ... if (this.connection && data) this.connection.cork(); var ret; if (data) { this.write(data, encoding); } if (this._hasBody && this.chunkedEncoding) { ret = this._send('0\r\n' + this._trailer + '\r\n', 'binary', finish); } else { ret = this._send('', 'binary', finish); } if (this.connection && data) this.connection.uncork(); // ... return ret; } OutgoingMessage.prototype._writeRaw = function(data, encoding, callback) { if (typeof encoding === 'function') { callback = encoding; encoding = null; } var connection = this.connection; // ... return connection.write(data, encoding, callback); };
Fazit
An diesem Punkt wurde eine Anfrage über TCP an den Client zurückgesendet. Dieser Artikel untersucht nur den Hauptverarbeitungsablauf. Tatsächlich berücksichtigt der Node.js-Quellcode auch weitere Situationen, wie z. B. die Behandlung von Timeouts, den Caching-Mechanismus, wenn der Socket
belegt ist, die spezielle Header-Behandlung, Gegenmaßnahmen für Probleme Upstream und die effizientere Abfrage geschriebener Header usw. Diese Details sind alle einer eingehenden Untersuchung und des Lernens wert. Durch die Analyse des Quellcodes des http
-Moduls können wir besser verstehen, wie wir damit leistungsstarke Web-Frameworks erstellen können.
Leapcell: The Best of Serverless Web Hosting
Zum Schluss möchte ich eine Plattform empfehlen, die sich am besten für die Bereitstellung von Go-Diensten eignet: Leapcell
🚀 Build with Your Favorite Language
Develop effortlessly in JavaScript, Python, Go, or Rust.
🌍 Deploy Unlimited Projects for Free
Only pay for what you use—no requests, no charges.
⚡ Pay-as-You-Go, No Hidden Costs
No idle fees, just seamless scalability.
🔹 Follow us on Twitter: @LeapcellHQ