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)