【Node.js】httpサーバーを立ち上げる その2(TCP接続の確立と切断の検出)

  • このエントリーをはてなブックマークに追加

Node.jsでhttpサーバーを立ち上げる。
TCP接続が確立された時とTCP接続閉じた時にコマンドプロンプトにログを出すようにする。
それと、keep-alive機能を無効にするにはserver.keepAliveTimeout = 0;としたらいいらしい。
しかし、server.keepAliveTimeout = 0;としてもclosedが2回実行されているから無効にならないみたい。

環境
Windows10(64bit)
nvm(Node Version Manager)を使用する。
nvmはnodeとnpmのバージョンの切り替えができる。
node.jsはアップデートが頻繁に行われるため、バージョン管理ツールを使うと都合が良いことが多い。
「特定のプロジェクトに必要なNode.jsのバージョンを簡単にインストールして使用することができる。」や「プロジェクト毎にNode.jsのバージョンの切り替えが簡単になる。」等のメリットがある。
nvm 1.1.10
node v18.15.0
npm 9.5.0



httpサーバーを立ち上げるコード「http.js」を次に示す。
現地時間を取得するために自作モジュールLocalTime.jsを使用している。


(function() {

    // 自作モジュールLocalTime.jsをrequireを使ってインポートして、定数localTimeに格納する。
    const localTime = require("./LocalTime.js");

    // LocalTimeの言語を日本語に設定
    localTime.SetLang("JP");
    // LocalTimeのタイムゾーンを東京(TYO)に設定
    localTime.SetTimeZone("TYO");

    // Node.jsのhttpモジュールをrequireを使ってインポートして、定数httpに格納する。
    const http = require("http");

    const port = 3000; //port番号
    //const hostname = "localhost"; // ホスト名またはドメイン名
    const hostname = "192.168.2.100"; // ホスト名またはドメイン名


    // httpオブジェクトからcreateServerメソッドを実行する。
    // createServerメソッドは、引数としてリクエストを処理するコールバック関数を受け取る。
    // このコールバック関数は、リクエストが発生した際に実行され、リクエストオブジェクト(req)とレスポンスオブジェクト(res)を受け取る
    // つまり、このコールバック関数は、http://localhost:3000にアクセスしたときに実行される。
    // このコールバック関数内で、適切なレスポンス(res)を生成してクライアントに送信する処理を実装する。
    // リクエストオブジェクト(req)は、http.IncomingMessageオブジェクトのインスタンスである。
    // レスポンスオブジェクト(res)は、http.ServerResponseオブジェクトのインスタンスである。
    // レスポンスオブジェクト(res)は、Writableストリームとして機能する。
    // レスポンスオブジェクト(res)は、クライアントに対してデータを送信するためのストリームとして扱われる。

    // reqはhttp.IncomingMessageオブジェクトのインスタンスであり、HTTPリクエストのデータを読み取るための
    // Readableストリームです。リクエストヘッダーやボディなどのデータを読み取るために使用します。

    // resはhttp.ServerResponseオブジェクトのインスタンスであり、HTTPレスポンスを生成および送信するための
    // Writableストリームです。resに対して書き込まれたデータはクライアントに送信されます。
    // writeメソッドやendメソッドを使用してレスポンスボディを書き込むことができます。
    // レスポンスオブジェクト(res)のボディに書き込むにはwriteメソッドを使う。
    // レスポンスオブジェクト(res)の送信を完了するためにはendメソッドを使う。

    const server = http.createServer((req, res) => {

        // この部分のコード req.connection.remoteAddress は、HTTPリクエストの接続情報に含まれるクライアントのIPアドレスを取得するためのものです。
        // reqはリクエストオブジェクトであり、connectionプロパティは接続情報を表すオブジェクトを取得する。そして、remoteAddressプロパティは
        // その接続元のIPアドレスを取得します。

        // なお、remoteAddressは一般的にはクライアントの実際のIPアドレスを表す。
        // しかし、プロキシサーバやロードバランサを経由している場合には、そのサーバのIPアドレスが取得されることがありえる。
        // この点は注意すること。

        //取得したクライアントのIPアドレスをclientIPに格納する。
        const clientIP = req.connection.remoteAddress;

        console.log(
            `Executed. \n` +
            `${localTime.GetLocalTimeString()} \n` +
            `クライアントのIPアドレス:${clientIP}\n` +
            `${req.method} \n` +
            `${req.url} \n` +
            `${req.headers["user-agent"]}`
        );

        const url = `http://${req.headers.host}`;

        // URL解析
        // req.urlは相対URLが入る。
        // urlはベースurl(相対URLを解決するための基準となるURL)が入る。
        const myURL = new URL(req.url, url);
        const whitespace = 2;
        const result = [];
        const names = [];

        let stringLengthMax = 0;
        let buf = " ";
        let blanks = "";

        console.log("");

        result.push(myURL.href);
        result.push(myURL.origin);
        result.push(myURL.protocol);
        result.push(myURL.username);
        result.push(myURL.password);
        result.push(myURL.host);
        result.push(myURL.port);
        result.push(myURL.hostname);
        result.push(myURL.pathname);
        result.push(myURL.hash);
        result.push(myURL.search);

        names.push("href");
        names.push("origin");
        names.push("protocol");
        names.push("username");
        names.push("password");
        names.push("host");
        names.push("port");
        names.push("hostname");
        names.push("pathname");
        names.push("hash");
        names.push("search");

        // names配列の中の文字列で最大長をstringLengthMaxに格納する。
        names.forEach(t => {
            if (t.length > stringLengthMax) stringLengthMax = t.length;
        });

        // bufに格納した空の文字列を「stringLengthMax + whitespace」回数分のコピーを含む新しい文字列blanksを作る。 
        blanks = buf.repeat(stringLengthMax + whitespace);

        // URLの解析結果を出す。
        names.forEach((t, u) => (console.log(t + ":" + blanks.slice(t.length) + result[u])));

        console.log("");
        console.log("req.headers = ");
        console.log(req.headers);
        console.log("");

        let str = [];

        str.push("method:");
        str.push(req.method);
        str.push("\n");
        str.push("req.url:");
        str.push(req.url);
        str.push("\n");

        if (myURL.pathname === "/") {

            // ドキュメントルート(/)がリクエストさた場合
            res.writeHead(200, {
                "Content-Type": "text/plain;charset=utf-8"
            });

            if (req.method === "POST") {

                let i = 0

                // data受信イベントの発生時に断片データ(chunk)を取得する。
                req.on("data", chunk => {
                    console.log(localTime.GetLocalTimeString() + " i = " + i + ", chunk = " + chunk + "");
                    i += 1;
                    str.push(chunk);
                });

                // 受信完了(end)イベント発生時
                //「end」イベントは、レスポンスのデータの読み取りが完了した場合に発生する。
                req.on("end", function() {
                    str.push("\n");
                    //レスポンスデータをchunk分割する。
                    res.write(str.join(""));
                    res.end("Hello World! " + req.method + "\n");
                });

            } else if (req.method === "GET") {

                // URLSearchParamsに含まれる値はforEach()メソッドで取り出し可能である。
                myURL.searchParams.forEach(function(value, key) {
                    str.push(key + " = " + value + "\n");
                    console.log(localTime.GetLocalTimeString() + ", " + key + " = " + value);
                });

                // レスポンスデータをchunk分割する。
                res.write(str.join(""));
                res.end("Hello World! " + req.method + "\n");

            } else {

                let i = 0

                // data受信イベントの発生時に断片データ(chunk)を取得する。
                req.on("data", chunk => {
                    console.log(localTime.GetLocalTimeString() + " i = " + i + ", chunk = " + chunk + "");
                    i += 1;
                    str.push(chunk);
                });

                // 受信完了(end)イベント発生時
                // 「end」イベントは、レスポンスのデータの読み取りが完了した場合に発生する。
                req.on("end", function() {
                    str.push("\n");
                    //レスポンスデータをchunk分割する。
                    res.write(str.join(""));
                    res.end("Hi! " + req.method);
                });

            }

        } else {

            // ドキュメントルート(/)がリクエストされない場合はメソッドに関係なくここを通る。
            res.writeHead(404, {
                "Content-Type": "text/plain;charset=utf-8"
            });

            // レスポンスデータをchunk分割する。
            res.write(str.join(""));
            res.end("404 Not Found");

        }

    });

    // Keep-Aliveを無効にする。
    // しかし、タイムアウトのcloseとブラウザの切断のcloseの2つが実行される。
    // 効いていないな。
    // server.keepAliveTimeout = 0;

    // connectionイベントが発生した際に実行されるコールバック関数を設定する。
    // このイベントは、クライアントとのTCP接続が確立された時に発生する。

    server.on('connection', (socket) => {

        // socket.remoteAddressは、クライアントのIPアドレスを取得するためのプロパティである。
        // socketは、TCP接続のためのソケットオブジェクトを表しており、そのremoteAddressプロパティを使用することで、
        // クライアントのIPアドレスを取得することができる。

        const clientIP = socket.remoteAddress;

        console.log(`${localTime.GetLocalTimeString()} TCP connection established. クライアントのIPアドレス:${clientIP}`);

        // タイムアウトの設定
        // 2秒のタイムアウト値を設定
        socket.setTimeout(2000);

        // タイムアウトの値を取得
        const timeout = socket.timeout;
        console.log('タイムアウト:' + timeout);

        //closeイベントは、クライアントとのTCP接続が閉じられた時に発生する。
        socket.on('close', () => {
            console.log(`${localTime.GetLocalTimeString()} TCP connection closed. クライアントのIPアドレス:${clientIP}\n`);
        });

    });

    // serverインスタンスオブジェクトからlistenメソッドを実行する。
    // httpサーバーが指定されたポート(3000)でリクエストを受け付けるようになる。
    server.listen(port, hostname, () => {
        console.log(localTime.GetLocalTimeString() + ` Server running at http://${hostname}:${port}`);
    });

})();

クライアント(IP:192.168.2.100)がChromeブラウザでhttp:/192.168.2.100:3000にアクセスすると、


[TYO] 2023-06-07(Wed) 13:50:17.527 TCP connection established. クライアントのIPアドレス:192.168.2.100
タイムアウト:2000
[TYO] 2023-06-07(Wed) 13:50:19.542 TCP connection closed. クライアントのIPアドレス:192.168.2.100
[TYO] 2023-06-07(Wed) 13:50:22.536 TCP connection closed. クライアントのIPアドレス:192.168.2.100

とコマンドプロンプトに出る。
closedが2回出る。これはなぜか?
TCPコネクションが確立された時間から2秒後のcloseがタイムアウトによるclosedである。
そして、TCPコネクションが確立された時間から5秒後がクロームのブラウザがTCPコネクションを閉じたときのclosedである。

クライアント(IP:192.168.2.101)がChromeブラウザでhttp:/192.168.2.100:3000にアクセスすると、


[TYO] 2023-06-07(Wed) 14:05:25.641 TCP connection established. クライアントのIPアドレス:192.168.2.101
タイムアウト:2000
[TYO] 2023-06-07(Wed) 14:05:25.714 TCP connection closed. クライアントのIPアドレス:192.168.2.101

とコマンドプロンプトに出る。
何故か、closedが2回出ない。TCPコネクションが確立されてから2秒後がタイムアウトなので、タイムアウトのclosedが出ていないということになる?
closedが1回しか出ない原因はどうやって調査するか分からんな。ネットワークは難しいな。

関連
HTTP通信 (attacktube.com)
【Node.js】httpサーバーを立ち上げる (attacktube.com)
自己署名証明書(オレオレ証明書)を使ってHTTPSサーバーをNode.jsで立ち上げる(windows10) その1 (attacktube.com)
自己署名証明書(オレオレ証明書)を使ってHTTPSサーバーをNode.jsで立ち上げる(windows10) その2 (attacktube.com)

  • このエントリーをはてなブックマークに追加

SNSでもご購読できます。

コメントを残す

*