1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Node.js Webフレームワークの動作原理とは?Express.jsやNext.jsの原理を解説

Posted at

Group179.png

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

brandpic7.png

🚀 好きな言語で構築

JavaScript、Python、Go、またはRustで簡単に開発できます。

🌍 無料で無制限のプロジェクトをデプロイ

使用した分だけ支払います — リクエストがなければ、請求もありません。

⚡ 使った分だけ支払い、隠された費用はありません

アイドル料金はなく、シームレスなスケーラビリティが実現できます。

Frame3-withpadding2x.png

📖 ドキュメントを探索する

🔹 Twitterでフォローしてください:@LeapcellHQ

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?