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


[![Group179.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/36e68350-d0d6-4a7c-8426-db9424171880.png)](https://leapcell.io/)


**[Leapcell: The Best of Serverless Web Hosting](https://leapcell.io/)**

# Node.jsの`http`モジュールを使ってWebフレームワークを作成する方法

Node.jsでWebアプリケーションを開発する際、`http`モジュールは基本的で重要なコンポーネントです。これを使えば、わずか数行のコードでHTTPサーバを起動することができます。次に、`http`モジュールを使ってシンプルなWebフレームワークを作成する方法を掘り下げ、HTTPリクエストの到着からレスポンスまでの一連のプロセスを理解しましょう。

## HTTPサーバの起動
以下は、HTTPサーバを起動するためのシンプルなNode.jsのコード例です：
```javascript
'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`イベントをリスンすることでカプセル化されています：
```javascript
// 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パーサが必要になります：
```javascript
// 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）：
```javascript
// 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`ライブラリが使われています：
```javascript
// 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`イベントのトリガーを完了させます：
```javascript
// 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`プロパティにキャッシュします：
```javascript
// 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`にあります：
```javascript
// 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](https://leapcell.io/)

最後に、Goサービスのデプロイに最適なプラットフォームをおすすめします：**[Leapcell](https://leapcell.io/)**

[![brandpic7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/2db63b82-325a-49ed-9035-b710b7cbd3e2.png)](https://leapcell.io/)


### 🚀 好きな言語で構築
JavaScript、Python、Go、またはRustで簡単に開発できます。  

### 🌍 無料で無制限のプロジェクトをデプロイ
使用した分だけ支払います — リクエストがなければ、請求もありません。  

### ⚡ 使った分だけ支払い、隠された費用はありません
アイドル料金はなく、シームレスなスケーラビリティが実現できます。  

[![Frame3-withpadding2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/9ca94a37-9c6f-4c81-b3c9-9bacae1d31e9.png)](https://leapcell.io/)


[📖 ドキュメントを探索する](https://docs.leapcell.io/)  

🔹 Twitterでフォローしてください：[@LeapcellHQ](https://x.com/LeapcellHQ)