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 EreignisclientErrorausgelöst. Wenn die Anfrage die MethodeCONNECTverwendet oder einenUpgrade-Header hat, wird direkt das Ereignisconnectoderupgradeausgelö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

