Node.js Web Frameworksの本当の動き方 (Inside Express.js & Next.js)
Grace Collins
Solutions Engineer · Leapcell

Node.jsのHTTPモジュールを使用してWebフレームワークを作成する方法
Node.jsでWebアプリケーションを開発する際、http
モジュールは基本的かつ重要なコンポーネントです。これを利用すると、わずか数行のコードでHTTPサーバーを起動できます。次に、http
モジュールを使用して簡単なWebフレームワークを作成し、HTTPリクエストの受信からレスポンスまでの全プロセスを理解する方法を詳しく説明します。
HTTPサーバーの起動
以下は、HTTPサーバーを起動するための簡単なNode.jsコードの例です。
'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') });
上記のコードを実行し、ターミナルでcurl localhost:3000
コマンドを使用すると、サーバーから返されるHello World
メッセージを確認できます。これは、Node.jsがソースコード内の多くの詳細をカプセル化しており、メインコードがlib/_http_*.js
のようなファイルに保存されているためです。次に、HTTPリクエストの受信からレスポンスまでのソースコードの実装について詳しく説明します。
HTTPリクエストの処理
サーバーインスタンスの作成
Node.jsでHTTPリクエストを受信するには、まずhttp.Server
クラスのインスタンスを作成し、そのrequest
イベントをリッスンする必要があります。HTTPプロトコルはアプリケーション層にあり、基盤となるトランスポート層は通常TCPプロトコルを使用するため、net.Server
クラスはhttp.Server
クラスの親クラスです。特定のHTTP関連部分は、net.Server
クラスのインスタンスのconnection
イベントをリッスンすることによってカプセル化されます。
// 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);
リクエストデータの解析
このとき、TCP経由で送信されたデータを解析するには、HTTPパーサーが必要です。
// 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; // ... }
パーサーparser
は「プール」から取得されることに注意してください。この「プール」はfree list
データ構造を使用します。その目的は、コンストラクターの頻繁な呼び出しによって発生するパフォーマンスの消費を回避するために、パーサーを可能な限り再利用することであり、数に上限もあります(http
モジュールでは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; };
データはTCP経由で継続的に送信されるため、パーサーはイベントに基づいて動作し、これはNode.jsの中核となるアイデアと一致しています。http-parser
ライブラリが使用されます。
// 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; }
受信から完全な解析まで、完全なHTTPリクエストは、パーサー上の次のイベントリスナーを順番に通過します。
parserOnHeaders
: 受信するリクエストヘッダーデータを継続的に解析します。parserOnHeadersComplete
: リクエストヘッダーが解析された後、header
オブジェクトを構築し、リクエスト本文のhttp.IncomingMessage
インスタンスを作成します。parserOnBody
: 受信するリクエスト本文データを継続的に解析します。parserOnExecute
: リクエスト本文が解析された後、解析にエラーがあるかどうかを確認します。エラーがある場合は、clientError
イベントを直接トリガーします。リクエストがCONNECT
メソッドを使用しているか、Upgrade
ヘッダーがある場合は、connect
またはupgrade
イベントを直接トリガーします。parserOnIncoming
: 解析された特定のリクエストを処理します。
request
イベントのトリガー
以下は、最終的なrequest
イベントのトリガーを完了するparserOnIncoming
リスナーのキーコードです。
// 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) { // If true, it means the socket is being occupied by a previous ServerResponse instance in the queue 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); } }
同じsocket
から送信されたリクエストの場合、ソースコードはそれぞれIncomingMessage
インスタンスと対応するServerResponse
インスタンスをキャッシュするために使用される2つのキューを維持していることがわかります。以前のServerResponse
インスタンスが最初にsocket
を占有し、そのfinish
イベントをリッスンします。イベントがトリガーされると、ServerResponse
インスタンスと対応するIncomingMessage
インスタンスをそれぞれのキューから解放します。
HTTPリクエストへの応答
レスポンス段階では、物事は比較的簡単です。受信したServerResponse
はすでにsocket
を取得しています。http.ServerResponse
は内部クラスhttp.OutgoingMessage
から継承されます。ServerResponse#writeHead
を呼び出すと、Node.jsはヘッダー文字列をまとめてServerResponse
インスタンスの_header
プロパティにキャッシュします。
// 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; }
すぐに、ServerResponse#end
を呼び出すと、ヘッダー文字列の後にデータを連結し、対応するテールを追加して、TCP接続に書き込みます。特定の書き込み操作は、内部メソッド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); };
結論
この時点で、リクエストはTCP経由でクライアントに返送されました。この記事では、主な処理フローのみを調査します。実際、Node.jsのソースコードは、タイムアウト処理、socket
が占有されている場合のキャッシュメカニズム、特別なヘッダー処理、アップストリームの問題に対する対策、より効率的な書き込みヘッダークエリなど、より多くの状況も考慮しています。これらの詳細はすべて、詳細な調査と学習に値します。 http
モジュールのソースコードを分析することで、それを使用して強力なWebフレームワークを構築する方法をよりよく理解できます。
Leapcell:最高のサーバーレスWebホスティング
最後に、Goサービスをデプロイするのに最適なプラットフォームをお勧めします:Leapcell
🚀 お気に入りの言語で構築する
JavaScript、Python、Go、またはRustで簡単に開発。
🌍 無制限のプロジェクトを無料でデプロイする
使用量に応じて支払い—リクエストも料金もなし。
⚡ 従量制、隠れたコストなし
アイドル料金なし、シームレスなスケーラビリティのみ。
🔹 Twitterでフォローしてください:@LeapcellHQ