WebSocket、HTTP… Node.js で socket.write() 以外を使う場面

2025-05-01

socket.write() は、Node.jsの net モジュールや tls モジュールによって作成された Socket オブジェクトが持つメソッドの一つです。このメソッドは、ソケットを通じてデータを送信するために使用されます。

より具体的に言うと、以下のようになります。



一般的なエラーと原因

    • 原因
      ソケットがすでに閉じられた後 (socket.end() が呼び出された後、または相手側が接続を閉じた後) に socket.write() を呼び出そうとした場合に発生します。
    • トラブルシューティング
      • ソケットの状態を常に確認し、socket.writable プロパティが true であることを確認してから write() を呼び出すようにします。
      • ソケットのライフサイクルを適切に管理し、不要になったソケットは明示的に閉じるようにします。
      • エラーイベント ('error') や 'close' イベントを監視し、ソケットが閉じられた後の処理を適切に行うようにします。
  1. TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received type object (引数 "chunk" は string、Buffer、または Uint8Array 型でなければなりません。object 型を受け取りました)

    • 原因
      socket.write() の最初の引数である chunk に、文字列、Buffer オブジェクト、または Uint8Array オブジェクト以外の型 (例えば、オブジェクトや数値など) を渡した場合に発生します。
    • トラブルシューティング
      • 送信したいデータが正しい型であることを確認します。オブジェクトを送信したい場合は、JSON.stringify() などを使用して文字列に変換してから送信する必要があります。
      • Buffer オブジェクトを扱う場合は、Buffer.from() などを適切に使用して作成します。
  2. ネットワーク関連のエラー (例: ECONNRESET, EPIPE, ETIMEDOUT)

    • 原因
      ネットワーク接続の問題 (相手側が突然接続を閉じた、ネットワークが不安定、タイムアウトなど) によって、データの送信が中断された場合に発生します。これらのエラーは、socket オブジェクトの 'error' イベントで捕捉できます。
    • トラブルシューティング
      • エラーイベントハンドラ (socket.on('error', (err) => { ... })) を適切に実装し、エラーの内容をログ出力するなどして原因を調査します。
      • ネットワーク環境を確認し、接続先のサーバーが正常に動作しているかを確認します。
      • 必要に応じて、再接続のロジックを実装することを検討します (ただし、無限ループにならないように注意が必要です)。
      • タイムアウト関連のエラーの場合は、ソケットのタイムアウト設定 (socket.setTimeout()) を見直すことも有効です。
  3. エンコーディングの問題

    • 原因
      socket.write() で指定したエンコーディングと、相手側が期待するエンコーディングが異なる場合に、文字化けなどの問題が発生することがあります。
    • トラブルシューティング
      • 送信側と受信側でデータのエンコーディングを一致させるようにします。通常は UTF-8 を使用することが推奨されます。
      • エンコーディングを指定しない場合、デフォルトは 'utf8' です。必要に応じて明示的にエンコーディングを指定します。
  4. バックプレッシャー (Backpressure)

    • 原因
      送信側の速度が受信側の処理速度よりも速い場合に、送信バッファが溢れる可能性があります。これは直接的なエラーとして現れることは少ないですが、パフォーマンスの低下やデータの損失につながる可能性があります。
    • トラブルシューティング
      • socket.write()false を返した場合、内部バッファが一杯であることを示しています。この場合、'drain' イベントが発生するまでデータの書き込みを一時的に停止し、受信側の処理が追いつくのを待つようにします。
      • 適切なフロー制御のメカニズムを実装することを検討します。

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

  • ドキュメントの参照
    Node.js の公式ドキュメントの net モジュールや tls モジュールに関する記述を再度確認することも重要です。
  • シンプルなテスト
    問題を切り分けるために、最小限のコードでソケットの送受信をテストしてみるのも有効です。
  • ネットワーク監視ツール
    tcpdump や Wireshark などのネットワーク監視ツールを使用すると、実際に送受信されているデータやネットワークの状態を確認できます。
  • ログ出力
    socket.write() で送信するデータや、ソケットの状態 (接続状態、書き込み可能かどうかなど) をログに出力することで、問題の原因を特定しやすくなります。
  • エラーイベントの監視
    socket.on('error', (err) => { ... }) を使用して、ソケットで発生したエラーを捕捉し、ログ出力や適切なエラー処理を行います。エラーオブジェクト (err) には、エラーの種類や詳細な情報が含まれていることが多いです。


例1: シンプルなテキストデータの送信 (クライアント側)

この例では、クライアントがサーバーに接続し、簡単なテキストメッセージを送信します。

const net = require('net');

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

  // 文字列データを UTF-8 エンコーディングで送信
  client.write('こんにちは、サーバー!\r\n');

  // 送信完了時のコールバック関数
  client.write('これは2回目の送信です。\r\n', () => {
    console.log('2回目のデータを送信しました。');
    client.end(); // データ送信完了後、接続を閉じます
  });
});

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

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

解説

  • 'error' イベント: 接続中にエラーが発生した場合に発生します。
  • 'end' イベント: サーバー側から接続が閉じられたときに発生します。
  • client.end(): これ以上データを送信しないことをサーバーに通知し、接続を閉じます。
  • コールバック関数: client.write() の第三引数に指定された関数は、データがソケットの内部バッファからネットワークにフラッシュされた後に実行されます。
  • client.write('...'): サーバーに送信するテキストデータを指定します。デフォルトでは UTF-8 エンコーディングが使用されます。
  • net.connect(): 指定されたホストとポートに接続を試み、接続が成功するとコールバック関数が実行されます。

例2: Buffer オブジェクトの送信 (クライアント側)

この例では、文字列を Buffer オブジェクトに変換して送信します。バイナリデータを送信する場合などに使用します。

const net = require('net');

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

  const message = 'これはBufferデータです。';
  const buffer = Buffer.from(message, 'utf8'); // 文字列を UTF-8 エンコーディングの Buffer に変換
  client.write(buffer);
  client.end();
});

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

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

解説

  • client.write(buffer): 作成した Buffer オブジェクトをサーバーに送信します。
  • Buffer.from(message, 'utf8'): 指定された文字列を指定されたエンコーディング (ここでは 'utf8') で Buffer オブジェクトに変換します。

例3: サーバー側でのデータ受信と応答 (サーバー側)

クライアントから送信されたデータを受信し、応答を返すサーバー側のコードです。

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

  socket.on('data', (data) => {
    console.log(`受信データ: ${data.toString()}`); // 受信したデータを文字列に変換して表示
    socket.write(`「${data.toString().trim()}」を受け取りました。\r\n`); // 受信した内容をエコーバック
  });

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

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

server.listen(8080, () => {
  console.log('サーバーがポート 8080 でリッスンを開始しました。');
});

解説

  • server.listen(8080, () => { ... }): サーバーを指定されたポートでリッスン状態にします。
  • socket.on('end', () => { ... }): クライアントが接続を閉じたときに発生します。
  • socket.write('...'): クライアントに応答を送信します。
  • socket.on('data', (data) => { ... }): クライアントからデータが送信されるたびに発生するイベントです。data は Buffer オブジェクトとして渡されます。toString() メソッドで文字列に変換できます。
  • net.createServer((socket) => { ... }): 新しいクライアントからの接続を受け付けるたびに、新しい socket オブジェクトが作成され、コールバック関数が実行されます。

例4: バックプレッシャーの処理 (クライアント側)

大量のデータを送信する際に、送信速度が受信側の処理速度よりも速い場合に発生するバックプレッシャーを処理する例です。

const net = require('net');

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

  const largeData = 'A'.repeat(1024 * 1024); // 1MB のデータ
  let i = 0;

  function sendData() {
    const canWrite = client.write(`データ ${i}: ${largeData.substring(i * 1024, (i + 1) * 1024)}\n`);
    i++;
    if (!canWrite) {
      // バッファが一杯の場合、'drain' イベントを待つ
      client.once('drain', sendData);
    } else if (i < 1000) { // 1000回送信
      // まだ送信するデータがある場合は再帰的に呼び出す
      sendData();
    } else {
      client.end('送信完了');
    }
  }

  sendData();
});

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

client.on('error', (err) => {
  console.error(`クライアントエラー: ${err}`);
});
  • このようにすることで、送信速度を受信側の処理能力に合わせ、データの損失を防ぐことができます。
  • client.once('drain', sendData): 'drain' イベントは、バッファが空になり、再び書き込みが可能になったときに発生します。このイベントのリスナーとして sendData 関数を登録し、書き込みが可能になったら次のデータを送信します。
  • client.write()false を返した場合、内部送信バッファが一杯であることを意味します。


socket.pipe() を使用したストリーム処理

socket オブジェクトは ReadableStream および WritableStream のインターフェースを実装しています。そのため、他のストリームと pipe() メソッドを使って接続し、データの流れをより宣言的に記述できます。

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

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

  const fileStream = fs.createReadStream('large_file.txt');

  // ファイルの内容をソケットにパイプ処理で送信
  fileStream.pipe(client);

  fileStream.on('end', () => {
    console.log('ファイルの送信が完了しました。');
    client.end();
  });

  fileStream.on('error', (err) => {
    console.error(`ファイル読み込みエラー: ${err}`);
    client.end();
  });
});

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

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

解説

  • ストリームの end イベントや error イベントを監視することで、ファイル送信の完了やエラーを処理できます。
  • fileStream.pipe(client): fileStream から読み取られたデータを自動的に client (ソケットの WritableStream) に書き込みます。これにより、socket.write() を明示的に呼び出す必要がなくなります。
  • fs.createReadStream('large_file.txt'): ファイルの内容を読み取る ReadableStream を作成します。

利点

  • バックプレッシャーを自動的に処理します (書き込み速度が読み取り速度より速い場合、自動的に一時停止します)。
  • 大量のデータを効率的に処理できます (メモリに一度にロードする必要がありません)。
  • より簡潔で可読性の高いコードになります。

より高レベルなライブラリの使用

Node.jsのエコシステムには、ネットワーク通信をより簡単に行うための多くの高レベルなライブラリが存在します。

  • axios (HTTP クライアント)
    HTTP リクエストをより簡単に送信できる Promise ベースの HTTP クライアントライブラリです。内部で http/https モジュールを使用しており、socket.write() を直接操作する必要はありません。

  • socket.io
    リアルタイムの双方向通信を抽象化するライブラリです。WebSocket をベースに、フォールバックメカニズムや便利なAPIを提供します。socket.write() を直接使用することはありません。

  • ws (WebSocket ライブラリ)
    WebSocket プロトコルを扱うためのライブラリです。双方向通信を容易に実装できます。socket.write() の代わりに、WebSocket オブジェクトの send() メソッドを使用します。

    const WebSocket = require('ws');
    const ws = new WebSocket('ws://localhost:8080');
    
    ws.on('open', () => {
      console.log('WebSocket 接続が開きました。');
      ws.send('WebSocket 経由で送信されたデータ'); // 内部的にソケットへの書き込みが行われる
    });
    
    ws.on('message', (message) => {
      console.log(`受信メッセージ: ${message}`);
    });
    
    ws.on('close', () => {
      console.log('WebSocket 接続が閉じられました。');
    });
    
    ws.on('error', (error) => {
      console.error(`WebSocket エラー: ${error}`);
    });
    
  • http および https モジュール
    HTTP および HTTPS プロトコルを扱うための高レベルなインターフェースを提供します。Webサーバーやクライアントを実装する際に socket.write() を直接使用するよりも便利です。

    const http = require('http');
    
    const options = {
      hostname: 'example.com',
      port: 80,
      path: '/',
      method: 'GET'
    };
    
    const req = http.request(options, (res) => {
      console.log(`ステータスコード: ${res.statusCode}`);
      res.on('data', (chunk) => {
        console.log(`ボディ: ${chunk.toString()}`);
      });
    });
    
    req.on('error', (error) => {
      console.error(`リクエストエラー: ${error}`);
    });
    
    req.end(); // リクエストを送信 (内部的にソケットへの書き込みが行われる)
    

利点

  • 開発効率が向上します。
  • エラー処理やプロトコルの詳細を意識せずに、より高レベルな操作に集中できます。
  • 特定のプロトコルやユースケースに合わせて最適化されています。

dgram モジュール (UDP 通信の場合)

UDP (User Datagram Protocol) を使用する場合は、net.Socket ではなく dgram.Socket を使用します。データの送信には socket.send() メソッドを使用します。write() メソッドは UDP ソケットには存在しません。

const dgram = require('dgram');
const client = dgram.createSocket('udp4');
const message = Buffer.from('UDP メッセージ');
const port = 41234;
const host = 'localhost';

client.send(message, 0, message.length, port, host, (err) => {
  if (err) {
    console.error(`UDP 送信エラー: ${err}`);
    client.close();
  } else {
    console.log(`UDP メッセージを ${host}:${port} に送信しました。`);
    client.close();
  }
});

解説

  • client.send(message, offset, length, port, address, callback): 指定されたホストとポートに UDP データグラムを送信します。
  • dgram.createSocket('udp4'): IPv4 の UDP ソケットを作成します。

利点

  • ブロードキャストやマルチキャストなどの機能を利用できます。
  • 高速でオーバーヘッドが少ない通信が可能です (ただし、信頼性や順序性は保証されません)。

socket.write() は TCP ソケットにおける基本的なデータ送信手段ですが、より複雑なアプリケーションや特定のプロトコルを使用する場合には、socket.pipe() によるストリーム処理や、http/httpswssocket.ioaxios などの高レベルなライブラリを利用することで、より効率的で保守性の高いコードを書くことができます。また、UDP 通信を行う場合は dgram モジュールを使用し、socket.send() メソッドを利用します。