【Node.js】dnsPromises.resolveSrv() の代替手段と使い分け:コールバック vs ライブラリ

2025-05-16

SRV レコードは、特定のサービスがどのホストのどのポートで動作しているかといった情報を提供するために DNS サーバーに登録されるレコードです。例えば、メールサーバーや XMPP サーバーなどの場所を見つけるために利用されます。

この関数は非同期処理を行い、Promise オブジェクトを返します。Promise が解決されると、SRV レコードの情報を含むオブジェクトの配列が渡されます。各オブジェクトは以下のプロパティを持ちます。

  • name: サービスを提供しているホスト名です。
  • port: サービスが動作しているポート番号です。
  • weight: 同じ優先度を持つレコード間の重みを示す整数値です。値が大きいほど選択される可能性が高くなります。
  • priority: レコードの優先度を示す整数値です。値が小さいほど優先度が高いことを意味します。

基本的な使い方

const dns = require('node:dns').promises;

async function resolveSrvRecord(hostname) {
  try {
    const records = await dns.resolveSrv(hostname);
    console.log(`SRV レコード (${hostname}):`, records);
  } catch (err) {
    console.error('SRV レコードの解決に失敗しました:', err);
  }
}

// 例:"_xmpp-client._tcp.example.com" の SRV レコードを問い合わせる
resolveSrvRecord('_xmpp-client._tcp.example.com');

上記の例では、resolveSrvRecord という非同期関数を定義し、dns.resolveSrv() を使って _xmpp-client._tcp.example.com というホスト名の SRV レコードを問い合わせています。await キーワードを使って Promise の完了を待ち、成功した場合は取得した SRV レコードをコンソールに出力し、失敗した場合はエラーメッセージを出力します。

  • SRV レコードの形式は、サービス名 (_xmpp-client)、プロトコル (_tcp_udp)、そしてドメイン名 (example.com) を組み合わせたものが一般的です。
  • 指定されたホスト名に SRV レコードが存在しない場合、Promise はエラーで reject されます。
  • dnsPromises.resolveSrv() は非同期処理であるため、.then() メソッドや async/await 構文を使って結果を処理する必要があります。


一般的なエラー

    • 原因
      指定したホスト名が存在しない、または DNS サーバーがそのホスト名を解決できない場合に発生します。タイプミスや、ドメイン名の有効期限切れ、DNS サーバーの障害などが考えられます。
    • トラブルシューティング
      • 指定したホスト名が正しいかどうか再度確認してください。
      • ping コマンドや nslookup コマンドなどを使って、そのホスト名が正しく解決できるか手動で確認してみてください。
      • ネットワーク接続に問題がないか確認してください。
      • 使用している DNS サーバーが正常に動作しているか確認してください。
  1. Error: queryA ENODATA <hostname> (または類似のエラー): A レコードが見つからないエラー

    • 原因
      SRV レコードは存在するものの、その SRV レコードが指すホスト名に対応する A レコード(IPv4 アドレス)または AAAA レコード(IPv6 アドレス)が存在しない場合に発生することがあります。
    • トラブルシューティング
      • SRV レコードが指しているホスト名に対して、A レコードまたは AAAA レコードが DNS に登録されているか確認してください。
      • nslookup -type=A <srv_target_hostname>nslookup -type=AAAA <srv_target_hostname> のようにして確認できます。
  2. Error: querySRV ENODATA <hostname> (または類似のエラー): SRV レコードが見つからないエラー

    • 原因
      指定したホスト名に SRV レコードが登録されていない場合に発生します。
    • トラブルシューティング
      • DNS サーバーに SRV レコードが正しく登録されているか確認してください。
      • nslookup -type=SRV <hostname> コマンドを使って、SRV レコードが存在するかどうかを確認できます。ホスト名は通常 _サービス名._プロトコル.ドメイン名 の形式です(例: _xmpp-client._tcp.example.com)。

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

  • DNS サーバーの確認
    使用している DNS サーバーのアドレスが正しいか、またそのサーバーが正常に動作しているかを確認してください。
  • タイムアウトの設定
    DNS クエリが長時間応答しない場合に備えて、タイムアウトを設定することを検討してください。Node.js の dns モジュール自体にはタイムアウトの直接的な設定はありませんが、必要であればより高度な DNS クライアントライブラリの利用を検討するのも一つの手段です。
  • Promise のエラーハンドリング
    try...catch ブロックや Promise の .catch() メソッドを使って、エラーを適切に処理するようにしてください。エラーが発生した場合に、適切なログ出力やフォールバック処理を行うことが重要です。
  • DNS ユーティリティの使用
    nslookupdig などの DNS lookup ツールを使って、DNS レコードを手動で確認することは非常に有効です。これらのツールで期待されるレコードが存在するかどうか、またその内容が正しいかを確認できます。
  • ネットワーク接続の確認
    ping コマンドなどで、DNS サーバーや問い合わせようとしているホストにネットワーク的に到達可能か確認してください。
  • エラーメッセージをよく読む
    エラーメッセージには、問題の原因の手がかりとなる情報が含まれています。


基本的な SRV レコードの解決

この例では、指定されたホスト名の SRV レコードを解決し、その結果をコンソールに出力します。

const dns = require('node:dns').promises;

async function resolveAndLogSrv(hostname) {
  try {
    const records = await dns.resolveSrv(hostname);
    console.log(`SRV レコード (${hostname}):`);
    records.forEach(record => {
      console.log(`  優先度: ${record.priority}, 重み: ${record.weight}, ポート: ${record.port}, ホスト: ${record.name}`);
    });
  } catch (err) {
    console.error(`SRV レコード (${hostname}) の解決に失敗しました:`, err);
  }
}

// 例:"_sip._tcp.example.com" の SRV レコードを問い合わせる
resolveAndLogSrv('_sip._tcp.example.com');

// 例:"_xmpp-server._tcp.example.com" の SRV レコードを問い合わせる
resolveAndLogSrv('_xmpp-server._tcp.example.com');

このコードでは、resolveAndLogSrv 関数がホスト名を受け取り、dns.resolveSrv() を使って SRV レコードを取得します。成功した場合、取得した各 SRV レコードの priority(優先度)、weight(重み)、port(ポート番号)、name(ホスト名)をコンソールに出力します。失敗した場合は、エラーメッセージを出力します。

特定の優先度を持つサーバーへの接続

この例では、取得した SRV レコードを優先度でソートし、最も優先度の高いサーバーの情報を使って接続を試みる(実際には接続処理は省略しています)シナリオを示しています。

const dns = require('node:dns').promises;

async function connectToHighestPriorityServer(hostname) {
  try {
    const records = await dns.resolveSrv(hostname);
    if (records.length > 0) {
      // 優先度でソート (昇順)
      records.sort((a, b) => a.priority - b.priority);
      const bestServer = records[0];
      console.log(`最も優先度の高いサーバー (${hostname}):`);
      console.log(`  ホスト: ${bestServer.name}, ポート: ${bestServer.port}`);
      // ここで bestServer.name と bestServer.port を使って接続処理を行う
      console.log('最も優先度の高いサーバーへの接続を試みます...');
    } else {
      console.log(`SRV レコード (${hostname}) は見つかりませんでした。`);
    }
  } catch (err) {
    console.error(`SRV レコード (${hostname}) の解決に失敗しました:`, err);
  }
}

// 例:"_http._tcp.api.example.com" の SRV レコードを問い合わせる
connectToHighestPriorityServer('_http._tcp.api.example.com');

このコードでは、取得した SRV レコードを priority プロパティに基づいて昇順にソートしています。配列の最初の要素が最も優先度の高いサーバーの情報となるため、それを取り出してホスト名とポート番号を表示し、接続を試みるというメッセージを出力しています。

同じ優先度のサーバーを重みに基づいて選択

この例では、複数のサーバーが同じ優先度を持つ場合に、重みに基づいてランダムにサーバーを選択するロジックを示しています。

const dns = require('node:dns').promises;

async function selectServerByWeight(hostname) {
  try {
    const records = await dns.resolveSrv(hostname);
    if (records.length > 0) {
      // 優先度でソート
      records.sort((a, b) => a.priority - b.priority);

      // 最も高い優先度のレコードのみを抽出
      const highestPriority = records[0].priority;
      const samePriorityRecords = records.filter(record => record.priority === highestPriority);

      if (samePriorityRecords.length === 1) {
        const selectedServer = samePriorityRecords[0];
        console.log(`選択されたサーバー (${hostname}, 優先度: ${selectedServer.priority}):`);
        console.log(`  ホスト: ${selectedServer.name}, ポート: ${selectedServer.port}`);
      } else {
        // 重みに基づいて選択
        let totalWeight = samePriorityRecords.reduce((sum, record) => sum + record.weight, 0);
        let randomNumber = Math.random() * totalWeight;
        let cumulativeWeight = 0;
        let selectedServer = null;

        for (const record of samePriorityRecords) {
          cumulativeWeight += record.weight;
          if (randomNumber < cumulativeWeight) {
            selectedServer = record;
            break;
          }
        }

        if (selectedServer) {
          console.log(`選択されたサーバー (${hostname}, 優先度: ${selectedServer.priority}, 重み: ${selectedServer.weight}):`);
          console.log(`  ホスト: ${selectedServer.name}, ポート: ${selectedServer.port}`);
        } else {
          console.log(`同じ優先度のサーバーが見つかりませんでした (${hostname})。`);
        }
      }
    } else {
      console.log(`SRV レコード (${hostname}) は見つかりませんでした。`);
    }
  } catch (err) {
    console.error(`SRV レコード (${hostname}) の解決に失敗しました:`, err);
  }
}

// 例:"_minecraft._tcp.example.com" の SRV レコードを問い合わせる
selectServerByWeight('_minecraft._tcp.example.com');


コールバックベースの dns.resolveSrv()

Promise ベースの dnsPromises.resolveSrv() の代わりに、従来のコールバック関数を受け取る dns.resolveSrv() を使用することができます。

const dns = require('node:dns');

dns.resolveSrv('_sip._tcp.example.com', (err, records) => {
  if (err) {
    console.error('SRV レコードの解決に失敗しました:', err);
    return;
  }
  console.log('SRV レコード (_sip._tcp.example.com):');
  records.forEach(record => {
    console.log(`  優先度: ${record.priority}, 重み: ${record.weight}, ポート: ${record.port}, ホスト: ${record.name}`);
  });
});

console.log('SRV レコードの問い合わせを開始しました...');

この例では、dns.resolveSrv() の第二引数としてコールバック関数を渡しています。DNS クエリが完了すると、エラーオブジェクト(err)と SRV レコードの配列(records)がこのコールバック関数に渡されます。エラーが発生した場合は err オブジェクトが truthy な値になり、成功した場合は records に結果が含まれます。

利点

  • 古い Node.js のバージョンでも利用可能です。
  • Promise を使用しないため、よりシンプルなコードに見える場合があります(特に Promise に慣れていない場合)。

欠点

  • エラーハンドリングが Promise よりも煩雑になることがあります。
  • コールバック地獄(ネストが深くなる)に陥りやすいです。

他の DNS クライアントライブラリの利用

Node.js の標準 dns モジュール以外にも、より高度な機能や柔軟性を提供するサードパーティの DNS クライアントライブラリが存在します。これらのライブラリは、SRV レコードの解決を含む様々な DNS クエリをサポートしており、Promise ベースの API を提供しているものも多いです。

  • resolve-dns
    シンプルな DNS 解決ライブラリで、Promise ベースの API を提供しています。SRV レコードの解決もサポートしています。
  • dnspromise
    dns モジュールの Promise API をラップしたシンプルなライブラリですが、より使いやすいインターフェースを提供している場合があります。
  • node-dns
    比較的低レベルな DNS クライアントで、様々なレコードタイプや DNS プロトコルをサポートしています。SRV レコードのクエリも可能です。

これらのライブラリの利用方法はそれぞれ異なりますが、一般的には npm install <ライブラリ名> でインストールした後、ライブラリのドキュメントに従って SRV レコードをクエリします。

例 (node-dns を使用する場合 - 概念的なものです。API の詳細はライブラリのドキュメントを参照してください)

const dns = require('node-dns');

async function resolveSrvWithNodeDns(hostname) {
  try {
    const response = await new Promise((resolve, reject) => {
      const query = dns.Request({
        question: dns.Question({ name: hostname, type: 'SRV' }),
        server: { address: '8.8.8.8', port: 53, type: 'udp' }, // 例: Google Public DNS
        timeout: 1000,
      });

      query.send();

      query.on('timeout', () => reject(new Error('DNS query timed out')));
      query.on('message', (err, response) => {
        if (err) reject(err);
        else {
          const srvRecords = response.answer.filter(record => record.type === dns.consts.SRV);
          resolve(srvRecords.map(record => ({
            priority: record.priority,
            weight: record.weight,
            port: record.port,
            name: record.target,
          })));
        }
      });

      query.on('error', reject);
    });

    console.log(`SRV レコード (${hostname}):`, response);

  } catch (err) {
    console.error(`SRV レコード (${hostname}) の解決に失敗しました:`, err);
  }
}

resolveSrvWithNodeDns('_sip._tcp.example.com');

利点

  • Promise ベースの API を提供しているライブラリもあり、非同期処理を扱いやすくなります。
  • より多くの DNS レコードタイプや機能をサポートしていることがあります。
  • 標準の dns モジュールよりも詳細な設定や制御が可能な場合があります(例: 使用する DNS サーバーの指定、タイムアウト設定など)。

欠点

  • 標準モジュールよりも学習コストが高い場合があります。
  • 外部ライブラリへの依存性が増えます。

DNS ルックアップユーティリティの実行 (非推奨)

child_process モジュールなどを使用して、システムにインストールされている nslookupdig などの DNS ルックアップユーティリティを実行し、その出力を解析する方法も考えられます。しかし、この方法は移植性や信頼性の面で問題が多く、一般的には推奨されません。DNS ユーティリティの出力形式はシステムやバージョンによって異なる可能性があり、エラーハンドリングも複雑になります。