Node.js socket.setTimeout() の徹底解説:タイムアウト設定とエラー処理

2025-05-01

主な目的と機能

  • 自動的なソケットの終了(オプション)
    socket.setTimeout() の第二引数にコールバック関数を指定した場合、タイムアウト時にその関数が実行されます。このコールバック関数内で socket.end()socket.destroy() を呼び出すことで、ソケットを明示的に閉じることができます。コールバック関数を指定しない場合、タイムアウトが発生してもソケットは自動的には閉じられません。 'timeout' イベントをリッスンして、適切な処理(例えば、エラーログの記録や再試行、ソケットのクローズなど)を実装する必要があります。
  • エラーイベントの発生
    タイムアウトが発生すると、ソケットオブジェクトは 'timeout' イベントを発行(emit)します。
  • タイムアウトの検出
    設定された時間を超えてソケットが何も送受信しない場合、Node.jsはソケットがタイムアウトしたと判断します。

基本的な使い方

const net = require('net');

const server = net.createServer((socket) => {
  console.log('クライアントが接続しました。');

  // 5秒間(5000ミリ秒)データ送受信がない場合、タイムアウトとします。
  socket.setTimeout(5000, () => {
    console.log('ソケットがタイムアウトしました。');
    socket.end(); // クライアントにFINパケットを送信し、接続を閉じます。
    // または socket.destroy(); を使用して強制的にソケットを閉じることができます。
  });

  socket.on('data', (data) => {
    console.log(`受信したデータ: ${data.toString()}`);
    socket.write('データを処理しました。\n');
  });

  socket.on('end', () => {
    console.log('クライアントが切断しました。');
  });

  socket.on('error', (err) => {
    console.error(`ソケットエラー: ${err}`);
  });
});

server.listen(3000, () => {
  console.log('サーバーがポート3000で起動しました。');
});

解説

  1. socket.setTimeout(5000, () => { ... }); : この行では、socket オブジェクトに対してタイムアウト時間を5000ミリ秒(5秒)に設定しています。第二引数には、タイムアウトが発生したときに実行されるコールバック関数を指定しています。
  2. コールバック関数内では、タイムアウトが発生したことをログに出力し、socket.end() を呼び出してクライアントとの接続を正常に閉じます。socket.destroy() を使用すると、より強制的にソケットを閉じることができます。
  3. socket.on('timeout', () => { ... }); : コールバック関数を指定しない場合、またはコールバック関数に加えてタイムアウト時の処理を追加したい場合は、'timeout' イベントのリスナーを設定します。
  • タイムアウト時間は、アプリケーションの要件やネットワーク環境に応じて適切に設定する必要があります。
  • タイムアウトを設定することで、ネットワークの不具合や相手側の応答がない場合に、リソースが無限に占有されるのを防ぐことができます。
  • タイムアウトタイマーは、ソケットでデータが送信または受信されるたびにリセットされます。


一般的なエラーとトラブルシューティング

    • 原因
      • タイムアウト時間が短すぎる: ネットワークの遅延や処理に時間がかかる場合に、意図せずタイムアウトしてしまうことがあります。
      • データの送受信が頻繁に行われている: タイムアウトタイマーはデータの送受信があるたびにリセットされるため、短い間隔でデータが流れているとタイムアウトが発生しません。
      • ソケットの状態が正しくない: ソケットが既に閉じられている、または接続が確立されていない状態で setTimeout() を呼び出しても効果がない場合があります。
    • トラブルシューティング
      • タイムアウト時間を適切に長く設定することを検討してください。ネットワークの状況やアプリケーションの処理時間を考慮する必要があります。
      • 意図しないデータの送受信がないか確認してください。例えば、keep-aliveの設定やポーリング処理などが影響している可能性があります。
      • socket.readyState プロパティなどを確認し、ソケットの状態が期待通りであることを確認してください。
  1. タイムアウト後のソケットの処理が適切でない

    • 原因
      • 'timeout' イベントリスナーで適切な処理(socket.end()socket.destroy() など)を行っていない場合、ソケットが閉じられず、リソースリークの原因になる可能性があります。
      • タイムアウト後のエラー処理が不十分な場合、アプリケーションが予期しない動作をすることがあります。
    • トラブルシューティング
      • 'timeout' イベントリスナー内で、必ずソケットを閉じる処理 (socket.end() または socket.destroy()) を実装してください。どちらを使うかは、その後の処理や相手側への通知の必要性によって判断します。
      • タイムアウト後に発生する可能性のあるエラー(例えば、閉じられたソケットへの書き込みなど)を適切にキャッチし、処理するようにしてください。'error' イベントリスナーも適切に設定することが重要です。
  2. サーバー側とクライアント側でタイムアウト設定が一致していない

    • 原因
      • サーバー側とクライアント側で異なるタイムアウト時間を設定している場合、どちらか一方だけがタイムアウトし、予期しない切断が発生することがあります。
    • トラブルシューティング
      • サーバーとクライアントの両方で、アプリケーションの要件に合わせて適切なタイムアウト時間を設定し、可能であれば一貫性を持たせるようにしてください。
  3. Keep-Aliveとの相互作用

    • 原因
      • HTTPのKeep-Aliveなどの機能を使用している場合、ソケットがアイドル状態になることがありますが、これは必ずしもエラーではありません。socket.setTimeout() の設定がKeep-Aliveの設定と競合する可能性があります。
    • トラブルシューティング
      • Keep-Aliveを使用する場合は、socket.setTimeout() の値をKeep-Aliveのタイムアウト時間よりも短く設定しないように注意してください。適切に設定しないと、Keep-Aliveの恩恵を受けられずに接続が頻繁に再確立される可能性があります。
      • Keep-Aliveの設定(例えば、agent.keepAliveTimeout など)も確認し、socket.setTimeout() との兼ね合いを考慮してください。
  4. TLS/SSL接続におけるタイムアウト

    • 原因
      • TLS/SSLハンドシェイクに時間がかかり、socket.setTimeout() で設定した時間を超えてしまうことがあります。
    • トラブルシューティング
      • TLS/SSL接続の場合は、ハンドシェイクにかかる時間も考慮してタイムアウト時間を設定する必要がある場合があります。

デバッグのヒント

  • シンプルなテスト
    問題を切り分けるために、最小限のコードでタイムアウトの挙動を確認するテストを作成してみるのも有効です。
  • ネットワーク監視
    Wiresharkなどのツールを使用して、実際にネットワーク上でどのようなデータのやり取りが行われているかを確認することで、タイムアウトの原因を特定できる場合があります。
  • ログ出力
    タイムアウトが発生したタイミングや、その前後のソケットの状態、関連するエラーなどをログに出力するようにしましょう。


例1: サーバー側でのタイムアウト処理 (コールバック関数を使用)

const net = require('net');

const server = net.createServer((socket) => {
  console.log('クライアントが接続しました。');

  // 5秒間データ送受信がない場合、タイムアウトとみなし、ソケットを閉じます。
  socket.setTimeout(5000, () => {
    console.log('ソケットが5秒間アイドル状態だったため、タイムアウトしました。');
    socket.end('タイムアウトにより接続を閉じます。\n'); // クライアントにメッセージを送信して閉じます
  });

  socket.on('data', (data) => {
    console.log(`受信: ${data.toString()}`);
    socket.write('サーバーがデータを受信しました。\n');
  });

  socket.on('end', () => {
    console.log('クライアントが切断しました。');
  });

  socket.on('error', (err) => {
    console.error(`ソケットエラー: ${err}`);
  });
});

server.listen(3000, () => {
  console.log('サーバーがポート3000で起動しました。');
});

この例では、サーバーがクライアントからの接続を受け付け、各ソケットに対して setTimeout(5000, callback) を設定しています。5秒間データを受信も送信もしない場合、指定されたコールバック関数が実行され、タイムアウトのメッセージをコンソールに出力し、socket.end() で接続を閉じます。

例2: サーバー側でのタイムアウト処理 ('timeout' イベントを使用)

const net = require('net');

const server = net.createServer((socket) => {
  console.log('クライアントが接続しました。');

  // 5秒間データ送受信がない場合、'timeout' イベントが発生します。
  socket.setTimeout(5000);

  socket.on('timeout', () => {
    console.log('ソケットがタイムアウトしました (イベント)。');
    socket.destroy(); // 強制的にソケットを閉じます
  });

  socket.on('data', (data) => {
    console.log(`受信: ${data.toString()}`);
    socket.write('サーバーがデータを受信しました。\n');
  });

  socket.on('end', () => {
    console.log('クライアントが切断しました。');
  });

  socket.on('error', (err) => {
    console.error(`ソケットエラー: ${err}`);
  });
});

server.listen(3000, () => {
  console.log('サーバーがポート3000で起動しました。');
});

この例では、socket.setTimeout(5000) の第二引数にコールバック関数を指定せず、代わりに 'timeout' イベントのリスナーを設定しています。タイムアウトが発生すると 'timeout' イベントが発行され、リスナー関数が実行されます。ここでは socket.destroy() を呼び出してソケットを強制的に閉じています。

例3: クライアント側でのタイムアウト処理

const net = require('net');

const client = net.connect({ port: 3000 }, () => {
  console.log('サーバーに接続しました。');

  // 3秒間サーバーから応答がない場合、タイムアウトとします。
  client.setTimeout(3000, () => {
    console.log('サーバーからの応答が3秒間なかったため、タイムアウトしました。');
    client.destroy(); // ソケットを強制的に閉じます
  });

  client.write('こんにちは、サーバー!\n');
});

client.on('data', (data) => {
  console.log(`サーバーから受信: ${data.toString()}`);
  client.end(); // データを受信したらクライアント側から切断します
});

client.on('end', () => {
  console.log('サーバーとの接続を閉じました。');
});

client.on('error', (err) => {
  console.error(`クライアントエラー: ${err}`);
});

client.on('timeout', () => {
  console.log('クライアント側でタイムアウトイベントが発生しました。');
  // ここで追加の処理(再試行など)を行うこともできます
});

この例はクライアント側のコードです。サーバーに接続後、client.setTimeout(3000, ...) で3秒のタイムアウトを設定しています。もしサーバーから3秒以内にデータが送信されてこない場合、タイムアウトが発生し、指定されたコールバック関数が実行され、クライアントソケットを閉じます。また、'timeout' イベントのリスナーも設定しており、タイムアウト発生時に別の処理を行うことも可能です。

const net = require('net');

const server = net.createServer((socket) => {
  console.log('クライアントが接続しました。');

  // タイムアウトを無効にする(デフォルトは無効です)
  socket.setTimeout(0);

  socket.on('data', (data) => {
    console.log(`受信: ${data.toString()}`);
    socket.write('サーバーがデータを受信しました。\n');
  });

  socket.on('end', () => {
    console.log('クライアントが切断しました。');
  });

  socket.on('error', (err) => {
    console.error(`ソケットエラー: ${err}`);
  });
});

server.listen(3000, () => {
  console.log('サーバーがポート3000で起動しました。');
});


http モジュールや https モジュールのタイムアウト設定

HTTPリクエストを行う場合、http モジュールや https モジュールが提供する機能を利用することで、より高レベルなタイムアウト設定が可能です。

  • client.setTimeout()
    http.Agent を使用している場合、クライアントソケットに対して直接 setTimeout() を設定することもできます。ただし、request 関数の timeout オプションの方がより高レベルで扱いやすいことが多いです。

  • request 関数の timeout オプション
    http.request()https.request() に渡すオプションオブジェクトに timeout プロパティを設定することで、リクエスト全体のタイムアウト時間をミリ秒単位で指定できます。これには、接続の確立、データの送信、サーバーからの応答の受信にかかる時間が含まれます。

    const http = require('http');
    
    const options = {
      hostname: 'example.com',
      port: 80,
      path: '/',
      method: 'GET',
      timeout: 5000 // リクエスト全体のタイムアウトを5秒に設定
    };
    
    const req = http.request(options, (res) => {
      console.log(`ステータスコード: ${res.statusCode}`);
      res.on('data', (chunk) => {
        console.log(`データ: ${chunk}`);
      });
      res.on('end', () => {
        console.log('データ受信完了');
      });
    });
    
    req.on('error', (err) => {
      console.error(`リクエストエラー: ${err.message}`);
    });
    
    req.on('timeout', () => {
      console.log('リクエストがタイムアウトしました。');
      req.abort(); // リクエストを中止します
    });
    
    req.end();
    

Promiseベースのラッパーライブラリの利用

node-fetchaxios のようなPromiseベースのHTTPクライアントライブラリは、タイムアウト設定をより簡潔に行うためのオプションを提供しています。

  • axios の timeout オプション

    const axios = require('axios');
    
    axios.get('http://example.com', { timeout: 5000 }) // タイムアウトを5秒に設定
      .then(response => {
        console.log(response.data);
      })
      .catch(error => {
        console.error(`エラー: ${error.message}`);
      });
    
  • node-fetch の timeout オプション

    const fetch = require('node-fetch');
    
    (async () => {
      try {
        const response = await fetch('http://example.com', { timeout: 5000 }); // タイムアウトを5秒に設定
        const text = await response.text();
        console.log(text);
      } catch (error) {
        console.error(`エラー: ${error.message}`);
      }
    })();
    

これらのライブラリは、タイムアウト処理だけでなく、リダイレクトの処理、エラーハンドリング、レスポンスの解析なども容易に行えるため、HTTP通信を扱う際には非常に便利です。

自作のタイムアウト処理の実装

より複雑なシナリオや、特定の要件に合わせてタイムアウト処理をカスタマイズしたい場合は、setTimeout() とイベントリスナーを組み合わせて、独自のタイムアウト機構を実装することも可能です。

const net = require('net');

const socket = net.connect({ port: 3000 }, () => {
  console.log('サーバーに接続しました。');

  let timeoutId = setTimeout(() => {
    console.log('一定時間応答がなかったため、タイムアウトしました。');
    socket.destroy();
  }, 10000); // 10秒のタイムアウト

  socket.on('data', (data) => {
    console.log(`受信: ${data.toString()}`);
    clearTimeout(timeoutId); // データを受信したらタイマーをクリア
    timeoutId = setTimeout(() => {
      console.log('次のデータが一定時間来なかったため、タイムアウトしました。');
      socket.destroy();
    }, 5000); // 次のデータのタイムアウトを5秒に設定
  });

  socket.on('end', () => {
    console.log('切断されました。');
    clearTimeout(timeoutId);
  });

  socket.on('error', (err) => {
    console.error(`ソケットエラー: ${err}`);
    clearTimeout(timeoutId);
  });

  socket.write('データを送信します。\n');
});

この例では、接続確立時とデータ受信後に setTimeout() を設定し、データを受信するたびにタイマーをクリアしています。これにより、「一定時間データが到着しない場合にタイムアウトする」というより細やかな制御が可能になります。

ストリーム API の pipe とタイムアウト

ストリームを扱う場合、pipe メソッドと setTimeout() を組み合わせることで、データ転送が一定時間停滞した場合にタイムアウトさせることができます。ただし、ストリーム全体としてのタイムアウトを直接設定する機能は標準では提供されていないため、必要に応じて上記の自作のタイムアウト処理などを応用する必要があります。