パフォーマンス改善に繋がる?Node.js socket.localFamily の理解

2025-05-01

socket.localFamily は、Node.js の net.Socket オブジェクトのプロパティの一つで、ローカルソケットのアドレスファミリを示します。簡単に言うと、「この接続の自分の側のネットワークアドレスの種類は何ですか?」という情報を保持しています。

具体的には、以下のいずれかの文字列が格納されます。

  • "Unix": ローカルソケットが Unix ドメインソケット(ファイルパスで識別されるソケット)を使用している場合に設定されます。Windows 環境では通常見られません。
  • "IPv6": ローカルソケットが IPv6 アドレス(例: ::1, 2001:0db8:85a3:0000:0000:8a2e:0370:7334) を使用している場合に設定されます。
  • "IPv4": ローカルソケットが IPv4 アドレス(例: 127.0.0.1, 192.168.1.10) を使用している場合に設定されます。

このプロパティは読み取り専用 (read-only) です。 つまり、プログラム中でこの値を変更することはできません。接続が確立された時点で、Node.js によって自動的に適切な値が設定されます。

どのような時に socket.localFamily が役立つのでしょうか?

  • 情報表示
    ネットワーク関連の情報をユーザーに表示する際に、ローカルアドレスのファミリを示すことができます。
  • 条件分岐
    接続の種類に応じて処理を分けたい場合に利用できます。例えば、IPv6 アドレスで接続された場合に特定の設定を適用するなどといった処理が考えられます。
  • ログ出力やデバッグ
    接続の種類(IPv4 か IPv6 か Unix ドメインソケットか)をログに出力したり、デバッグ時に確認したりする際に役立ちます。

簡単な例

const net = require('net');

const server = net.createServer((socket) => {
  console.log('クライアントが接続しました。');
  console.log(`ローカルアドレスファミリ: ${socket.localFamily}`);
  socket.end('接続ありがとうございます!\n');
});

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

この例では、クライアントが接続するたびに、サーバー側のソケットの localFamily プロパティの値がコンソールに出力されます。この場合、server.listen()'127.0.0.1' を指定しているため、通常は "IPv4" と表示されます。

もし IPv6 アドレスでリッスンするように変更した場合 (server.listen(3000, '::1', ...) など)、socket.localFamily"IPv6" になります。



socket.localFamily 自体は読み取り専用のプロパティであるため、直接的にこのプロパティが原因でエラーが発生することはほとんどありません。 しかし、このプロパティの値が示すローカルソケットのアドレスファミリに関連して、ネットワーク接続の問題が発生することがあります。

以下に、socket.localFamily の値を意識する際に遭遇しやすい一般的なエラーと、そのトラブルシューティングについて説明します。

想定外のアドレスファミリ

  • トラブルシューティング
    • サーバー側のリッスンアドレスの確認
      net.createServer().listen() で指定しているホスト名や IP アドレスを確認してください。IPv4 のみでリッスンしたい場合は、'0.0.0.0' や特定の IPv4 アドレスを指定します。IPv6 のみでリッスンしたい場合は '::' や特定の IPv6 アドレスを指定します。
    • クライアント側の接続設定の確認
      net.connect()net.createConnection() で指定しているホスト名や IP アドレスを確認してください。特定のファミリでの接続を強制することは難しいですが、意図しないアドレスへの接続になっていないか確認します。
    • Unix ドメインソケットの使用意図の確認
      Unix ドメインソケットを使用する意図がない場合は、net.createServer()net.connect() の最初の引数がファイルパスになっていないか確認してください。
  • 原因
    • サーバーの設定
      サーバーが IPv6 アドレスでもリッスンするように設定されている場合、クライアントからの接続が IPv6 で確立されることがあります。
    • クライアントの設定
      クライアント側が IPv6 を優先して使用するように設定されている場合、IPv6 での接続を試みる可能性があります。
    • Unix ドメインソケット
      意図せず Unix ドメインソケットを使用する設定になっている(通常は net.createServer()net.connect() の最初の引数にファイルパスを指定した場合)。
  • 現象
    期待していたアドレスファミリ(例えば IPv4 のみ)とは異なる値 ("IPv6""Unix") が socket.localFamily に入っている。

アドレスファミリによる処理の不整合

  • トラブルシューティング
    • 条件分岐の再確認
      socket.localFamily の値に基づく条件分岐のロジックを丁寧に確認し、すべての可能性を考慮しているか確認してください。
    • ライブラリの互換性確認
      使用しているライブラリが IPv6 や Unix ドメインソケットをサポートしているか、ドキュメントなどを確認してください。
  • 原因
    • 条件分岐の記述ミス
      if (socket.localFamily === 'IPv4') { ... } else if (socket.localFamily === 'IPv6') { ... } のような条件分岐で、タイプミスがあったり、考慮漏れのアドレスファミリ(例えば "Unix") があったりする。
    • ライブラリの依存性
      使用しているネットワーク関連のライブラリが特定のアドレスファミリのみをサポートしている場合、異なるファミリでの接続でエラーが発生する可能性がある。
  • 現象
    socket.localFamily の値に基づいて処理を分岐させている箇所で、想定外のアドレスファミリが来たために意図しない動作をする。

環境による挙動の違い

  • トラブルシューティング
    • 本番環境のネットワーク設定の確認
      本番環境のネットワークインターフェースの設定やルーティングなどを確認し、IPv6 が有効になっているかなどを把握してください。
    • IP アドレスでの直接指定
      ホスト名ではなく、IP アドレス(IPv4 または IPv6)を直接指定して接続を試すことで、挙動を安定させることができます。
    • 環境変数の利用
      環境変数などを用いて、接続先のアドレスファミリやホスト名を環境ごとに切り替えるように設計することも有効です。
  • 原因
    • ネットワーク構成の違い
      開発環境と本番環境でネットワークの設定が異なり、IPv6 が有効になっているかどうかが違う。
    • DNS 解決の違い
      ホスト名を指定して接続する場合、DNS サーバーの応答によって解決される IP アドレスのファミリが異なることがある。
  • 現象
    開発環境では IPv4 で動作していたコードが、本番環境では IPv6 で接続されるなど、環境によって socket.localFamily の値が変わる。
  • ネットワークプログラミングにおいては、IPv4 と IPv6 の両方をサポートすることを考慮した設計が推奨される場合があります。
  • socket.localFamily はあくまでローカルソケットのアドレスファミリを示すものであり、リモートソケットのアドレスファミリを示す socket.remoteFamily とは異なります。 混同しないように注意してください。


例1: サーバー側で接続元のアドレスファミリをログ出力する

この例では、クライアントからの接続を受け付け、接続されたソケットのローカルアドレスファミリをログに出力する簡単なサーバーを作成します。

const net = require('net');

const server = net.createServer((socket) => {
  console.log('クライアントが接続しました。');
  console.log(`ローカルアドレスファミリ: ${socket.localFamily}`);
  socket.end('接続ありがとうございます!\n');
});

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

// IPv6 アドレスでもリッスン (必要であれば)
// server.listen(3001, '::1', () => {
//   console.log('IPv6 サーバーがポート 3001 でリッスンを開始しました。');
// });

このコードを実行し、127.0.0.1:3000 にクライアントから接続すると、コンソールには "ローカルアドレスファミリ: IPv4" と表示されます。もし IPv6 でリッスンするサーバー (::1:3001) に IPv6 をサポートしたクライアントから接続した場合、socket.localFamily"IPv6" となります。

例2: クライアント側で接続したソケットのローカルアドレスファミリを確認する

この例では、サーバーに接続するクライアントを作成し、接続が確立されたソケットのローカルアドレスファミリをコンソールに出力します。

const net = require('net');

const client = net.connect({ port: 3000, host: '127.0.0.1' }, () => {
  console.log('サーバーに接続しました。');
  console.log(`ローカルアドレスファミリ: ${client.localFamily}`);
  client.end();
});

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

client.on('error', (err) => {
  console.error('接続エラー:', err);
});

このクライアントを実行すると、サーバー (127.0.0.1:3000) に接続後、コンソールに "ローカルアドレスファミリ: IPv4" と表示されます。サーバーが IPv6 アドレスで動作している場合、クライアントが IPv6 で接続できれば "IPv6" となる可能性があります。

例3: アドレスファミリに基づいて処理を分岐する (サーバー側)

この例では、接続されたクライアントのローカルアドレスファミリに基づいて、異なるメッセージを送信するサーバーを作成します。

const net = require('net');

const server = net.createServer((socket) => {
  console.log(`クライアント (${socket.remoteAddress}) が接続しました。ローカルアドレスファミリ: ${socket.localFamily}`);

  if (socket.localFamily === 'IPv6') {
    socket.write('IPv6 での接続ありがとうございます!\n');
  } else if (socket.localFamily === 'IPv4') {
    socket.write('IPv4 での接続ありがとうございます!\n');
  } else if (socket.localFamily === 'Unix') {
    socket.write('Unix ドメインソケットでの接続ありがとうございます!\n');
  } else {
    socket.write('不明なアドレスファミリでの接続ありがとうございます!\n');
  }

  socket.end();
});

server.listen(3000, '0.0.0.0', () => {
  console.log('サーバーがポート 3000 で全てのアドレスでリッスンを開始しました。');
});

このサーバーを実行し、IPv4 または IPv6 のアドレスで接続すると、socket.localFamily の値に応じて異なるメッセージがクライアントに送信されます。

例4: Unix ドメインソケットを使用した場合の localFamily (サーバー側)

Unix ドメインソケットを使用する例です(Windows では動作しません)。

const net = require('net');
const socketPath = '/tmp/my_unix_socket';

// 既存のソケットファイルが存在する場合は削除
try {
  fs.unlinkSync(socketPath);
} catch (err) {
  if (err.code !== 'ENOENT') {
    console.error('ソケットファイルの削除エラー:', err);
  }
}

const server = net.createServer((socket) => {
  console.log('クライアントが Unix ドメインソケット経由で接続しました。');
  console.log(`ローカルアドレスファミリ: ${socket.localFamily}`);
  socket.end('Unix ドメインソケット接続ありがとうございます!\n');
});

server.listen(socketPath, () => {
  console.log(`サーバーが Unix ドメインソケット ${socketPath} でリッスンを開始しました。`);
});

const fs = require('fs');

このサーバーは /tmp/my_unix_socket というパスで Unix ドメインソケットをリッスンします。接続してきたクライアントの socket.localFamily"Unix" となります。

例5: Unix ドメインソケットを使用した場合の localFamily (クライアント側)

対応するクライアントの例です(Windows では動作しません)。

const net = require('net');
const socketPath = '/tmp/my_unix_socket';

const client = net.connect({ path: socketPath }, () => {
  console.log('Unix ドメインソケットサーバーに接続しました。');
  console.log(`ローカルアドレスファミリ: ${client.localFamily}`);
  client.end();
});

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

client.on('error', (err) => {
  console.error('接続エラー:', err);
});

これらの例を通して、socket.localFamily が接続の種類に応じて "IPv4", "IPv6", "Unix" のいずれかの値を持つことが確認できます。また、この値を利用して、接続の種類に応じた処理を行うことも可能です。



socket.localAddress と net.isIP() を利用してアドレスファミリを推測する

socket.localAddress プロパティは、ローカルソケットがバインドされている IP アドレス(または Unix ドメインソケットのパス)を取得できます。この値と net.isIP() 関数を組み合わせることで、アドレスファミリを推測できます。

const net = require('net');

const server = net.createServer((socket) => {
  console.log(`ローカルアドレス: ${socket.localAddress}`);
  let family;
  if (net.isIP(socket.localAddress) === 4) {
    family = 'IPv4';
  } else if (net.isIP(socket.localAddress) === 6) {
    family = 'IPv6';
  } else {
    family = '不明'; // Unix ドメインソケットの場合は IP アドレスではない
  }
  console.log(`推測されたアドレスファミリ: ${family}`);
  socket.end('接続ありがとうございます!\n');
});

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

この方法では、net.isIP() 関数に socket.localAddress を渡すことで、IPv4 アドレスであれば 4、IPv6 アドレスであれば 6 が返ります。Unix ドメインソケットの場合は IP アドレスではないため、net.isIP()0 を返します。これにより、socket.localFamily と同様の情報を得ることができます。

socket.remoteAddress と net.isIP() を利用してリモート側のアドレスファミリを判断する

ローカルではなく、接続してきたリモートクライアントのアドレスファミリに基づいて処理を分けたい場合は、socket.remoteAddressnet.isIP() を使用します。

const net = require('net');

const server = net.createServer((socket) => {
  console.log(`リモートアドレス: ${socket.remoteAddress}`);
  let remoteFamily;
  if (net.isIP(socket.remoteAddress) === 4) {
    remoteFamily = 'IPv4';
  } else if (net.isIP(socket.remoteAddress) === 6) {
    remoteFamily = 'IPv6';
  } else {
    remoteFamily = '不明';
  }
  console.log(`リモートアドレスファミリ: ${remoteFamily}`);

  if (remoteFamily === 'IPv6') {
    socket.write('IPv6 クライアントからの接続です。\n');
  } else {
    socket.write('IPv4 クライアントからの接続です。\n');
  }
  socket.end('接続を閉じます。\n');
});

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

この例では、接続してきたクライアントの IP アドレス (socket.remoteAddress) を net.isIP() でチェックし、リモートのアドレスファミリに基づいて異なるメッセージを送信しています。

接続時のオプションでアドレスファミリを制御する

net.connect()net.createServer().listen() などの関数に渡すオプションオブジェクトで、使用するアドレスファミリを明示的に指定することができます。これにより、socket.localFamily の値を間接的に制御できます。

  • server.listen() のホスト指定
    サーバー側でリッスンするホスト名を指定することで、特定のアドレスファミリでのみリッスンするように制限できます。例えば、'127.0.0.1' を指定すると IPv4 のみ、'::1' を指定すると IPv6 のみでリッスンします。

    const net = require('net');
    
    // IPv4 のみでリッスンするサーバー
    const serverIPv4 = net.createServer((socket) => {
      console.log(`IPv4 接続: ローカルアドレスファミリ: ${socket.localFamily}`);
      socket.end('IPv4 接続ありがとうございます。\n');
    });
    serverIPv4.listen(3000, '127.0.0.1', () => {
      console.log('IPv4 サーバーがポート 3000 でリッスンを開始しました。');
    });
    
    // IPv6 のみでリッスンするサーバー
    const serverIPv6 = net.createServer((socket) => {
      console.log(`IPv6 接続: ローカルアドレスファミリ: ${socket.localFamily}`);
      socket.end('IPv6 接続ありがとうございます。\n');
    });
    serverIPv6.listen(3001, '::1', () => {
      console.log('IPv6 サーバーがポート 3001 でリッスンを開始しました。');
    });
    
  • net.connect() の family オプション
    クライアント側で接続を確立する際に、使用する IP アドレスファミリ (4 または 6) を指定できます。

    const net = require('net');
    
    // IPv6 での接続を試みる
    const clientIPv6 = net.connect({ port: 3000, host: '::1', family: 6 }, () => {
      console.log(`IPv6 でサーバーに接続しました。ローカルアドレスファミリ: ${clientIPv6.localFamily}`);
      clientIPv6.end();
    });
    
    clientIPv6.on('error', (err) => {
      console.error('IPv6 接続エラー:', err);
    });
    
    // IPv4 での接続を試みる
    const clientIPv4 = net.connect({ port: 3000, host: '127.0.0.1', family: 4 }, () => {
      console.log(`IPv4 でサーバーに接続しました。ローカルアドレスファミリ: ${clientIPv4.localFamily}`);
      clientIPv4.end();
    });
    
    clientIPv4.on('error', (err) => {
      console.error('IPv4 接続エラー:', err);
    });
    

環境変数などを利用した設定

アプリケーションの設定ファイルや環境変数などを用いて、使用するアドレスファミリに関する設定を行い、それに基づいて接続処理を切り替えることもできます。これにより、コードを変更せずにデプロイ環境に合わせて動作を変えることができます。

const net = require('net');

const addressFamilyPreference = process.env.ADDRESS_FAMILY || 'ipv4'; // 環境変数で設定

let connectOptions = { port: 3000 };
if (addressFamilyPreference.toLowerCase() === 'ipv6') {
  connectOptions.host = '::1';
  connectOptions.family = 6;
} else {
  connectOptions.host = '127.0.0.1';
  connectOptions.family = 4;
}

const client = net.connect(connectOptions, () => {
  console.log(`サーバーに接続しました (ファミリ: ${client.localFamily})。`);
  client.end();
});

client.on('error', (err) => {
  console.error('接続エラー:', err);
});