Node.js socket.bytesWrittenでネットワーク送信量を監視・分析

2025-05-16

socket.bytesWritten は、Node.js の net.Socket クラスや tls.TLSSocket クラスのインスタンスが持つ読み取り専用のプロパティです。

このプロパティは、ソケットに書き込まれたバイト数を数値で示します。具体的には、socket.write() メソッドや socket.end() メソッドなどを使用してソケットに送信されたデータの合計バイト数を追跡し、その値を保持しています。

重要なポイント

  • 送信完了とは限らない
    socket.write() が成功したとしても、データがネットワークの相手先に完全に届いたことを保証するものではありません。socket.bytesWritten は、ローカルの Node.js プロセスからソケットの送信バッファにデータが書き込まれた時点でのバイト数を表します。
  • 累積値
    アプリケーションがソケットのライフサイクル全体にわたってデータを書き込むたびに、この値は増加していきます。
  • 読み取り専用
    このプロパティの値を直接変更することはできません。Node.js 内部で自動的に更新されます。
  • 書き込まれたバイト数
    これは、実際にネットワークに送信するためにバッファリングされたデータのバイト数です。

どのような場合に役立つか

  • 進捗状況の表示 (限定的)
    大量のデータを送信する処理において、おおよその進捗状況をユーザーに示唆するのに利用できる場合があります(ただし、受信側の状況は考慮されません)。
  • ログ記録や監視
    送信されたデータ量をログに記録したり、監視システムでモニタリングしたりするのに利用できます。
  • パフォーマンス分析
    大量のデータを送信するアプリケーションのパフォーマンスを分析する際に、送信速度や効率を評価する手がかりになります。
  • 送信量の監視
    アプリケーションがどれくらいのデータをネットワークに送信したかを把握するのに役立ちます。

簡単な例

const net = require('net');

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

  const message = 'こんにちは、クライアント!';
  socket.write(message, () => {
    console.log(`"${message}" を送信しました。書き込まれたバイト数: ${socket.bytesWritten}`);
  });

  socket.on('end', () => {
    console.log('クライアントが切断しました。合計書き込みバイト数: ${socket.bytesWritten}');
  });
});

server.listen(3000, () => {
  console.log('サーバーはポート 3000 でリッスンしています。');
});

この例では、サーバーがクライアントにメッセージを送信した後、socket.bytesWritten の値を出力しています。クライアントが切断した際にも、その時点までの合計書き込みバイト数が出力されます。



  1. 送信完了と bytesWritten の混同

    • 誤解
      socket.write() が成功し、socket.bytesWritten が増加したら、データは相手先に完全に送信されたと考える。
    • 実際
      socket.bytesWritten は、データが Node.js プロセスのソケットの送信バッファに書き込まれた時点でのバイト数を示します。ネットワークの遅延、輻輳、相手先の受信状況などにより、データがすぐに相手先に届くとは限りません。
    • トラブルシューティング
      • 送信の完了や成功を確認するには、socket.write() のコールバック関数や 'drain' イベントを利用します。'drain' イベントは、送信バッファが空になり、さらにデータを書き込める状態になったときに発生します。
      • 信頼性の高いデータ送信が必要な場合は、TCP の特性(順序保証、再送制御など)に依存するだけでなく、アプリケーションレベルでの確認応答や再送メカニズムの実装を検討する必要があるかもしれません。
  2. 大きなデータを一度に書き込む場合

    • 状況
      非常に大きなデータを socket.write() で一度に送信しようとすると、内部バッファが一杯になり、socket.write()false を返すことがあります。このとき、socket.bytesWritten はそれまでにバッファに書き込まれたバイト数を反映しますが、すべてのデータが書き込まれたわけではありません。
    • トラブルシューティング
      • socket.write() の戻り値を確認し、false が返ってきた場合は 'drain' イベントを監視し、イベント発生後に残りのデータを書き込むようにします。
      • データを小さなチャンクに分割して順次送信することを検討します。Stream API を利用すると、この処理を効率的に行うことができます。
  3. ソケットがクローズされた後の bytesWritten の値

    • 状況
      ソケットがすでにクローズされた後に socket.bytesWritten を参照した場合、その時点までの累積書き込みバイト数が返されますが、それ以降の書き込みは行われません。
    • トラブルシューティング
      • ソケットの状態を常に把握し、クローズされたソケットに対して書き込み操作を行わないようにします。socket.destroyed プロパティでソケットが破棄されたかどうかを確認できます。
      • エラーハンドリングを適切に行い、ソケットのエラーやクローズイベントを捕捉して、後続の処理に影響が出ないようにします。
  4. TLS/SSL ソケットの場合のオーバーヘッド

    • 状況
      tls.TLSSocket を使用している場合、socket.bytesWritten はアプリケーションレベルのデータだけでなく、TLS/SSL プロトコルによる暗号化やハンドシェイクなどのオーバーヘッドも含んだバイト数を反映する可能性があります。
    • トラブルシューティング
      • TLS/SSL のオーバーヘッドを考慮して、アプリケーションのデータ送信量を評価する必要があります。
      • ネットワークのパフォーマンス分析を行う際には、TLS/SSL の影響も視野に入れることが重要です。
  5. ストリームとの連携における注意点

    • 状況
      pipe() メソッドなどでストリームをソケットに接続している場合、socket.bytesWritten はパイプされたストリームからソケットに書き込まれたデータの合計バイト数を反映します。ストリームの読み取り速度やバッファリングの影響を受ける可能性があります。
    • トラブルシューティング
      • ストリームの状態やイベント('end', 'error' など)を監視し、データが適切にソケットに書き込まれているかを確認します。
      • 必要に応じて、ストリームのバッファサイズや書き込み速度を調整することを検討します。


例1: シンプルなサーバーとクライアントでの送信バイト数の確認

この例では、サーバーがクライアントにメッセージを送信し、その際に socket.bytesWritten をログ出力します。クライアントが切断する際にも、合計の書き込みバイト数を確認します。

// server.js
const net = require('net');

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

  const message1 = '最初のメッセージです。';
  socket.write(message1, () => {
    console.log(`[サーバー] "${message1}" を送信しました。書き込まれたバイト数: ${socket.bytesWritten}`);
  });

  const message2 = '2番目のメッセージです。少し長めです。';
  socket.write(message2, () => {
    console.log(`[サーバー] "${message2}" を送信しました。書き込まれたバイト数: ${socket.bytesWritten}`);
  });

  socket.on('end', () => {
    console.log(`[サーバー] クライアントが切断しました。合計書き込みバイト数: ${socket.bytesWritten}`);
  });

  socket.on('error', (err) => {
    console.error('[サーバー] ソケットエラー:', err);
  });
});

server.listen(3000, () => {
  console.log('サーバーはポート 3000 でリッスンしています。');
});
// client.js
const net = require('net');

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

client.on('data', (data) => {
  console.log(`[クライアント] 受信したデータ: ${data.toString()}`);
});

client.on('end', () => {
  console.log('[クライアント] サーバーから切断されました。');
});

client.on('error', (err) => {
  console.error('[クライアント] ソケットエラー:', err);
});

実行方法

  1. node server.js を実行してサーバーを起動します。
  2. 別のターミナルで node client.js を実行してクライアントを接続します。

期待される出力 (サーバー側)

サーバーはポート 3000 でリッスンしています。
クライアントが接続しました: 127.0.0.1:XXXXX
[サーバー] "最初のメッセージです。" を送信しました。書き込まれたバイト数: XX
[サーバー] "2番目のメッセージです。少し長めです。" を送信しました。書き込まれたバイト数: YY
[サーバー] クライアントが切断しました。合計書き込みバイト数: ZZ

XX, YY, ZZ は、送信された文字列のバイト数に応じて変化します。socket.bytesWrittensocket.write() が呼ばれるたびに増加していくことが確認できます。

例2: drain イベントと bytesWritten の連携 (大きなデータの送信)

この例では、大きなデータを送信する際に socket.write()false を返す状況をシミュレートし、'drain' イベントが発生した後に残りのデータを送信する方法を示します。socket.bytesWritten がどのように変化するかも確認できます。

// server_large_data.js
const net = require('net');

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

  const largeData = Buffer.alloc(1024 * 1024, 'A'); // 1MB のデータ

  let sentBytes = 0;

  const sendData = () => {
    while (sentBytes < largeData.length) {
      const chunk = largeData.slice(sentBytes, sentBytes + 1024); // 1KB ずつ送信
      const canWrite = socket.write(chunk);
      sentBytes += chunk.length;
      console.log(`[サーバー] 今回送信したバイト数: ${chunk.length}, 現在の書き込みバイト数: ${socket.bytesWritten}`);
      if (!canWrite) {
        console.log('[サーバー] バックプレッシャーが発生しました。drain イベントを待ちます。');
        socket.once('drain', () => {
          console.log('[サーバー] drain イベントが発生しました。送信を再開します。');
          sendData();
        });
        return;
      }
    }
    socket.end('[サーバー] 全てのデータを送信しました。合計書き込みバイト数: ${socket.bytesWritten}');
  };

  sendData();

  socket.on('error', (err) => {
    console.error('[サーバー] ソケットエラー:', err);
  });
});

server.listen(3001, () => {
  console.log('サーバーはポート 3001 でリッスンしています。');
});
// client_large_data.js
const net = require('net');

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

client.on('data', (data) => {
  // 受信したデータを処理 (ここでは特に何もしません)
});

client.on('end', () => {
  console.log('[クライアント] サーバーから切断されました。');
});

client.on('error', (err) => {
  console.error('[クライアント] ソケットエラー:', err);
});

実行方法

  1. node server_large_data.js を実行してサーバーを起動します。
  2. 別のターミナルで node client_large_data.js を実行してクライアントを接続します。

期待される出力 (サーバー側)

サーバー側では、データを少しずつ送信する様子と、バックプレッシャーが発生した場合に 'drain' イベントを待ってから送信を再開する様子、そして socket.bytesWritten が増加していく様子がログ出力されます。

例3: HTTP サーバーでのリクエストとレスポンスにおける bytesWritten

HTTP サーバーのレスポンス送信においても socket.bytesWritten を確認できます。

// http_server_bytes_written.js
const http = require('http');

const server = http.createServer((req, res) => {
  const responseBody = '<html><body><h1>Hello, World!</h1><p>This is a test page.</p></body></html>';
  res.writeHead(200, { 'Content-Type': 'text/html', 'Content-Length': Buffer.byteLength(responseBody) });

  res.write(responseBody, () => {
    console.log(`[HTTP サーバー] 最初のチャンクを送信しました。書き込まれたバイト数: ${req.socket.bytesWritten}`);
  });

  res.end(() => {
    console.log(`[HTTP サーバー] レスポンスを完了しました。合計書き込みバイト数: ${req.socket.bytesWritten}`);
  });
});

server.listen(8080, () => {
  console.log('HTTP サーバーはポート 8080 でリッスンしています。');
});

実行方法

  1. node http_server_bytes_written.js を実行して HTTP サーバーを起動します。
  2. Web ブラウザで http://localhost:8080 にアクセスします。

期待される出力 (サーバー側)

HTTP サーバーはポート 8080 でリッスンしています。
[HTTP サーバー] 最初のチャンクを送信しました。書き込まれたバイト数: XX
[HTTP サーバー] レスポンスを完了しました。合計書き込みバイト数: YY

HTTP レスポンスの送信においても、リクエストのソケット (req.socket) の bytesWritten プロパティを通じて、書き込まれたバイト数を確認できることがわかります。



ストリーム API の利用 (stream モジュール)

Node.js の stream モジュールは、データのシーケンスを抽象化し、読み取り可能ストリーム (Readable) と書き込み可能ストリーム (Writable) を提供します。ソケットは双方向ストリーム (Duplex) であり、pipe() メソッドなどを利用してデータの流れをより宣言的に記述できます。

  • 送信バイト数の間接的な把握
    ストリーム API を直接利用する場合、socket.bytesWritten ほど直接的な送信バイト数の監視は難しくなりますが、ストリームのイベント ('drain', 'finish', 'error') や、必要に応じて Transform ストリームを挟むことで、データ処理の段階ごとのバイト数を監視できます。
  • 利点
    • データのチャンクごとの処理や変換が容易になります。
    • バックプレッシャーの管理が組み込まれており、socket.write() の戻り値を手動で確認する必要が減ります。
    • コードの可読性や保守性が向上します。


ファイルからソケットへパイプ処理で送信する

const fs = require('fs');
const net = require('net');

const server = net.createServer((socket) => {
  const readableStream = fs.createReadStream('large_file.txt');
  readableStream.pipe(socket);

  readableStream.on('end', () => {
    console.log('[サーバー] ファイルの送信が完了しました。');
    // socket.bytesWritten で合計送信バイト数を確認できます
    console.log('[サーバー] 合計書き込みバイト数:', socket.bytesWritten);
    socket.end();
  });

  readableStream.on('error', (err) => {
    console.error('[サーバー] ファイル読み込みエラー:', err);
    socket.destroy();
  });

  socket.on('error', (err) => {
    console.error('[サーバー] ソケットエラー:', err);
  });
});

server.listen(3002, () => {
  console.log('サーバーはポート 3002 でリッスンしています。');
});

この例では、pipe() を使用してファイルの内容をソケットに効率的に送信しています。socket.bytesWritten はパイプ処理後にも参照でき、合計の送信バイト数を確認できます。

高レベルなプロトコルライブラリの利用

HTTP、WebSocket、gRPC などの特定のプロトコルを扱う場合、Node.js にはそれぞれのプロトコルに特化した高レベルなライブラリが用意されています。これらのライブラリは、ソケットの低レベルな操作を抽象化し、よりアプリケーションロジックに集中できるようにします。

  • 送信バイト数の間接的な把握
    これらのライブラリでは、内部的にソケットを利用していますが、送信バイト数を直接公開していない場合があります。代わりに、リクエストやレスポンスの完了イベント、データ送信のコールバックなどを利用して、送信処理の完了や成功を確認できます。
  • 利点
    • プロトコルの詳細を意識せずに、データの送受信やセッション管理などが行えます。
    • エラーハンドリングやパフォーマンス最適化などがライブラリ側で考慮されている場合があります。

例 (HTTP サーバー)

const http = require('http');

const server = http.createServer((req, res) => {
  const responseBody = 'Hello from the HTTP server!';
  res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': Buffer.byteLength(responseBody) });
  res.end(responseBody, () => {
    // レスポンス送信完了時のコールバック
    console.log('[HTTP サーバー] レスポンスを送信しました。');
    // req.socket.bytesWritten で書き込みバイト数を確認できます
    console.log('[HTTP サーバー] 書き込みバイト数:', req.socket.bytesWritten);
  });
});

server.listen(8081, () => {
  console.log('HTTP サーバーはポート 8081 でリッスンしています。');
});

この例では、HTTP ライブラリの res.end() メソッドに渡すコールバック関数内で、リクエストのソケット (req.socket) の bytesWritten を確認しています。

カスタムロギングや監視メカニズムの導入

アプリケーションの特定の要件に合わせて、送信処理に関するカスタムなロギングや監視メカニズムを実装することも可能です。

  • 送信バイト数の明示的な記録
    socket.write() などの送信処理を行う箇所で、送信するデータのサイズを記録したり、送信完了時のコールバックで socket.bytesWritten の値をログに出力したりすることで、送信バイト数を追跡できます。
  • 利点
    • アプリケーション固有の情報を付加したログやメトリクスを収集できます。
    • 送信処理のパフォーマンス分析やデバッグに役立ちます。

例 (簡易的なロギング)

const net = require('net');

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

  const sendMessage = (message) => {
    const messageLength = Buffer.byteLength(message);
    socket.write(message, () => {
      console.log(`[サーバー] "${message}" (${messageLength} バイト) を送信しました。現在の書き込みバイト数: ${socket.bytesWritten}`);
    });
  };

  sendMessage('最初のログメッセージ');
  sendMessage('少し長い2番目のログメッセージ');

  socket.on('end', () => {
    console.log('[サーバー] クライアントが切断しました。合計書き込みバイト数:', socket.bytesWritten);
  });

  socket.on('error', (err) => {
    console.error('[サーバー] ソケットエラー:', err);
  });
});

server.listen(3003, () => {
  console.log('サーバーはポート 3003 でリッスンしています。');
});

この例では、送信するメッセージのバイト数を事前に計算してログに出力することで、送信量の情報をより具体的に把握しています。