首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP WHOIS 查詢類增強版:支持遞歸查詢國際/國內域名服務器

     編輯:PHP 綜合知識

項目背景與改進點

標準 WHOIS 協議(RFC 3912)基於 TCP 43 端口,查詢域名註冊信息。原版類僅查詢 whois.internic.net,對於非 .com/.net 域名(如 .org、.cn、.io 等)返回信息極少。本增強版實現兩大改進:

  1. 代碼精簡:移除冗餘變量定義,邏輯更清晰。
  2. 遞歸查詢機制:解析 INTERNIC 返回信息中的 Whois Server: 字段,自動向該權威服務器發起二次查詢,獲取完整註冊詳情(如 Yahoo 在 whois.networksolutions.com 的詳細記錄)。

類結構設計

class whois {
  // 配置屬性
  public $use_cache = 1;
  public $FROM_CACHE = 0;
  public $cache_dir = "./";
  public $port = 43;
  public $MAXLEN = 100;
  public $MAX_RETRIES = 0;      // 連接失敗重試次數
  public $SLEEP_VAL = 1;        // 重試間隔秒數
  public $RETRY = 0;
  public $FOUND = 0;            // 查詢結果標記
  public $ERROR = 0;            // 錯誤計數
  public $DATA_MIN = 8;         // 最小有效數據字節數
  public $DATA_COUNT = 0;
  
  // 運行時狀態
  public $WHOIS_SERVER;         // 當前查詢服務器
  public $NEW_WHOIS;            // 解析出的權威服務器
  public $FURTHER_INFO = 0;     // 是否發現權威服務器
  
  // 核心方法:connect()、rawlookup()、cnrawlookup()
}

核心方法解析

1. Socket 連接與重試邏輯

function connect($server) {
  $this->RETRY = 0;
  while ($this->RETRY <= $this->MAX_RETRIES) {
    $ptr = fsockopen($server, $this->port);
    if ($ptr > 0) {
      $this->ERROR = 0;
      return $ptr;
    } else {
      $this->ERROR++;
      $this->RETRY++;
      sleep($this->SLEEP_VAL);
    }
  }
  return false; // 隱式返回
}

簡單的阻塞式連接重試,生產環境建議設置 MAX_RETRIES=3、SLEEP_VAL=2,並加入連接超時 stream_set_timeout($ptr, 10)。

2. 國際域名查詢與權威服務器發現

function rawlookup($query, $server) {
  if (!$query) return "";
  $ptr = $this->connect($server);
  if (!$ptr) { $this->ERROR++; return false; }
  
  if (!ereg($query, "\n$")) $query .= "\n"; // 確保以換行結尾
  fputs($ptr, $query);
  
  $i = 0; $this->FOUND = 1;
  while (!feof($ptr)) {
    $array[$i] = fgets($ptr, $this->MAXLEN);
    $this->DATA_COUNT += strlen(chop($array[$i]));
    
    // 判斷查詢失敗標記
    if (eregi("No match for", $array[$i]) || eregi("No entries found", $array[$i])) {
      $this->FOUND = 0;
    } elseif (eregi("WHOIS database is down", $array[$i]) || eregi("Please wait a while", $array[$i])) {
      $this->ERROR++; $this->FOUND = 0; break;
    }
    
    // 關鍵:解析權威 WHOIS 服務器
    if (eregi("Whois Server:", $array[$i])) {
      // 取行尾 13 字符(假設格式固定),實際建議用正則提取主機名
      $this->NEW_WHOIS = trim(substr(trim($array[$i]), (strlen(trim($array[$i])) - 13) * (-1)));
      $this->FURTHER_INFO = 1;
    }
    $i++;
  }
  fclose($ptr);
  
  if ($this->DATA_COUNT > $this->DATA_MIN) return $array;
  else { $this->ERROR++; return false; }
}

關鍵點:eregi("Whois Server:") 捕獲類似 Whois Server: whois.networksolutions.com 的行,提取主機名用於二次查詢。

3. 國內域名查詢

function cnrawlookup($query, $server) {
  // 邏輯與 rawlookup 相同,但無權威服務器解析分支
  // 直接查詢 whois.cnnic.net.cn
}

使用示例:自動選擇服務器並遞歸查詢

$myWHOIS = new whois();
$thisname = $servername . $domainname; // 如 "yahoo.com"

// 根據後綴選擇初始服務器
if (ereg("\.cn$", $thisname)) {
  $myWHOIS->WHOIS_SERVER = "whois.cnnic.net.cn";
  $array = $myWHOIS->cnrawlookup($thisname, $myWHOIS->WHOIS_SERVER);
} else {
  $myWHOIS->WHOIS_SERVER = "whois.internic.net";
  $array = $myWHOIS->rawlookup($thisname, $myWHOIS->WHOIS_SERVER);
}

// 輸出首輪結果
echo $thisname . "\n";
for ($x = 0; $x < count($array); $x++) {
  echo "$x {$array[$x]}";
}

// 國際域名且發現權威服務器 → 發起二次查詢
if (!ereg("\.cn$", $thisname) && $myWHOIS->FURTHER_INFO) {
  echo "\nFurther information\n";
  $array_further = $myWHOIS->rawlookup($thisname, $myWHOIS->NEW_WHOIS);
  for ($x = 0; $x < count($array_further); $x++) {
    echo "$x {$array_further[$x]}";
  }
}

代碼中的歷史問題與現代化建議

問題影響現代替代
ereg/eregi 已棄用 (PHP 5.3+) / 移除 (PHP 7+)代碼無法運行preg_match (PCRE),大小寫不敏感用 /i 修飾符
無連接超時控制掛起風險stream_set_timeout($ptr, 10) 或 stream_context_create(['socket'=>['bindto'=>'', 'timeout'=>10]])
緩存機制未實現$use_cache 僅佔位實現文件/Redis 緩存,Key 為域名,TTL 1 天
解析 Whois Server 位置硬編碼格式變動即失效preg_match('/Whois Server:\s*(\S+)/i', $line, $m) 提取
同步阻塞查詢高併發下性能差Swoole/Workerman 協程 + 連接池,或異步隊列
無 IPv6 支持部分新服務器不可達fsockopen 自動支持雙棧,或顯式 tcp://[::1]:43

現代化重構版(PHP 8.1+、PSR-12、異步就緒)

<?php
declare(strict_types=1);

namespace App\Whois;

use RuntimeException;

class WhoisClient
{
    private int $port = 43;
    private int $timeout = 10;
    private int $maxRetries = 3;
    private int $retryDelay = 1;
    private ?string $cacheDir = null;
    private int $cacheTtl = 86400; // 1 day

    public function __construct(?string $cacheDir = null) {
        $this->cacheDir = $cacheDir;
    }

    public function query(string $domain): WhoisResult {
        $domain = strtolower(trim($domain));
        
        // 緩存檢查
        if ($this->cacheDir) {
            $cacheFile = $this->cacheDir . '/' . md5($domain) . '.json';
            if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $this->cacheTtl) {
                return WhoisResult::fromJson(file_get_contents($cacheFile));
            }
        }

        $server = $this->selectServer($domain);
        $rawData = $this->fetchWithRecursion($domain, $server);
        
        $result = new WhoisResult($domain, $rawData);
        
        // 寫入緩存
        if ($this->cacheDir) {
            file_put_contents($cacheFile, $result->toJson());
        }
        return $result;
    }

    private function selectServer(string $domain): string {
        // 簡化:實際可維護 TLD->Server 映射表或查詢 IANA
        return str_ends_with($domain, '.cn') ? 'whois.cnnic.cn' : 'whois.iana.org';
    }

    private function fetchWithRecursion(string $domain, string $server, int $depth = 0): array {
        if ($depth > 3) return []; // 防無限遞歸
        $data = $this->fetchRaw($domain, $server);
        // 解析權威服務器
        foreach ($data as $line) {
            if (preg_match('/^(?:Whois Server|Registrar WHOIS Server|ReferralServer):\s*(\S+)/i', $line, $m)) {
                $authServer = trim($m[1]);
                if ($authServer !== $server) {
                    $data = array_merge($data, $this->fetchWithRecursion($domain, $authServer, $depth + 1));
                }
                break;
            }
        }
        return $data;
    }

    private function fetchRaw(string $query, string $server): array {
        $lastErr = null;
        for ($attempt = 0; $attempt <= $this->maxRetries; $attempt++) {
            $ctx = stream_context_create(['socket' => ['timeout' => $this->timeout]]);
            $fp = @stream_socket_client("tcp://{$server}:{$this->port}", $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT, $ctx);
            if ($fp) {
                stream_set_timeout($fp, $this->timeout);
                fwrite($fp, $query . "\r\n");
                $lines = [];
                while (!feof($fp)) {
                    $lines[] = fgets($fp, 4096);
                }
                fclose($fp);
                return $lines;
            }
            $lastErr = "{$errstr} ({$errno})";
            if ($attempt < $this->maxRetries) sleep($this->retryDelay);
        }
        throw new RuntimeException("WHOIS query failed for {$server}: {$lastErr}");
    }
}

class WhoisResult {
    public string $domain;
    public array $rawLines;
    public string $rawText;
    public array $parsed = [];

    public function __construct(string $domain, array $lines) {
        $this->domain = $domain;
        $this->rawLines = $lines;
        $this->rawText = implode('', $lines);
        $this->parseKeyFields();
    }

    private function parseKeyFields(): void {
        $patterns = [
            'registrar' => '/Registrar:\s*(.+)/i',
            'creation_date' => '/Creation Date:\s*(.+)/i',
            'expiry_date' => '/(?:Registry Expiry Date|Expiration Date):\s*(.+)/i',
            'name_servers' => '/Name Server:\s*(.+)/i',
            'status' => '/Status:\s*(.+)/i',
        ];
        foreach ($patterns as $key => $pattern) {
            if (preg_match_all($pattern, $this->rawText, $m)) {
                $this->parsed[$key] = $m[1];
            }
        }
    }

    public function toJson(): string {
        return json_encode([
            'domain' => $this->domain,
            'raw' => $this->rawText,
            'parsed' => $this->parsed,
            'queried_at' => time(),
        ], JSON_UNESCAPED_UNICODE);
    }

    public static function fromJson(string $json): self {
        $data = json_decode($json, true);
        $obj = new self($data['domain'], explode("\n", $data['raw']));
        $obj->parsed = $data['parsed'];
        return $obj;
    }
}

使用新版客戶端

$client = new WhoisClient(sys_get_temp_dir() . '/whois_cache');
try {
    $result = $client->query('google.com');
    echo "Domain: {$result->domain}\n";
    echo "Registrar: " . ($result->parsed['registrar'][0] ?? 'N/A') . "\n";
    echo "Created: " . ($result->parsed['creation_date'][0] ?? 'N/A') . "\n";
    echo "Expires: " . ($result->parsed['expiry_date'][0] ?? 'N/A') . "\n";
    echo "Name Servers:\n";
    foreach ($result->parsed['name_servers'] ?? [] as $ns) echo "  - $ns\n";
} catch (Exception $e) {
    echo "Error: {$e->getMessage()}\n";
}

總結

原版 WHOIS 類雖然實現了核心遞歸查詢思路,但代碼已嚴重過時(ereg、無超時、無緩存實現、同步阻塞)。現代重構應採用:PCRE 正則、流上下文超時控制、文件/Redis 緩存、結構化結果解析、異步就緒架構。本文提供的 WhoisClient 可直接用於生產環境,並易於擴展為批量查詢、API 服務或域名監控系統的核心組件。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved