Leapcell: The Best of Serverless Web Hosting
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
イベントのトリガー
以下はparserOnIncoming
リスナーの重要なコードで、最終的なrequest
イベントのトリガーを完了させます:
// 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) {
// 真の場合、ソケットがキュー内の前のServerResponseインスタンスによって占有されていることを意味します
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
から送信されたリクエストについて、ソースコードは2つのキューを維持しており、それぞれIncomingMessage
インスタンスと対応するServerResponse
インスタンスをキャッシュするために使用されます。より早い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);
};
まとめ
これで、1つのリクエストがTCPを介してクライアントに返されました。この記事では主な処理フローのみを探っています。実際には、Node.jsのソースコードはより多くの状況も考慮しており、タイムアウトの処理、socket
が占有されているときのキャッシュメカニズム、特殊なヘッダーの処理、上流の問題への対策、より効率的な書き込み済みヘッダーの照会などがあります。これらの詳細はすべて、深く学ぶ価値があります。http
モジュールのソースコードの分析を通じて、私たちはそれを使って強力なWebフレームワークを構築する方法をよりよく理解することができます。
Leapcell: The Best of Serverless Web Hosting
最後に、Goサービスのデプロイに最適なプラットフォームをおすすめします:Leapcell
🚀 好きな言語で構築
JavaScript、Python、Go、またはRustで簡単に開発できます。
🌍 無料で無制限のプロジェクトをデプロイ
使用した分だけ支払います — リクエストがなければ、請求もありません。
⚡ 使った分だけ支払い、隠された費用はありません
アイドル料金はなく、シームレスなスケーラビリティが実現できます。
🔹 Twitterでフォローしてください:@LeapcellHQ