ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

海风域名查询工具v1.0:纯PHP轻量模块,无需API快速集成

海风域名查询工具v1.0:纯PHP轻量模块,无需API快速集成

简介:海风域名查询工具v1.0是一款基于PHP开发的轻量级域名查询系统,面向需要在Linux主机上快速搭建域名信息查询服务的站长与开发者,尤其适合具备基础PHP环境配置能力的技术人员使用。资源包为rar压缩格式,整体约372KB,体积小巧便于部署与迁移。安装前需将sitemap.xml、inc/config.php、install/data.sql等文件权限设置为777,随后运行/install/index.php完成安装,后台入口为/acp/,默认账号与密码均为admin,便于快速上手调试。该工具可帮助读者获得一套可直接运行的域名查询脚本,省去从零开发的时间成本,同时通过后台管理界面灵活维护查询数据与站点配置,适合用于个人站点功能扩展或学习PHP项目部署流程。目前已有118人学习关注,可作为入门级PHP建站与域名工具实践的参考素材。

1. 海风域名查询工具 v1.0:一个能塞进任何 PHP 项目的轻量查询模块

手里有个 PHP 老项目,后台需要加一个「查域名是否被注册」的小功能,你会怎么做?调第三方 API 要配额、要密钥,还怕哪天服务停了;自己写 whois 查询又得处理各种后缀的返回格式,烦得很。海风域名查询工具 v1.0 就是冲着这个场景来的——它是一套纯 PHP 实现的域名查询源码,不依赖外部接口,丢进现有项目就能跑。适合谁?手上维护着 PHP 站点、需要批量或单次查询域名状态的后端开发者,以及想拿一个完整小项目练手 PHP 网络编程的新手。整套代码结构不复杂,但把「查询—解析—返回」这条链路走通了,拿来当二次开发的底座正合适。

2. 技术选型与目录结构:为什么用原生 PHP 而不是框架

2.1 选型逻辑:轻量优先,拒绝框架绑架

这个项目最核心的选型判断是:不引入任何框架。原因很直接——域名查询本身是一个工具型功能,它需要的是「能被嵌入」,而不是「自成体系」。如果用了某个 MVC 框架,使用者就得先装框架、配路由、改入口文件,接入成本反而比功能本身还高。原生 PHP 的好处是,你可以在任何入口文件里require一下就开始调用,不需要改项目的整体架构。

从技术栈上看,这套源码依赖的东西很少:

依赖项用途是否必须
PHP 7.0+运行环境必须
cURL 扩展发起 whois / HTTP 查询必须
sockets 扩展部分后缀走 socket 直连可选
mbstring 扩展处理多编码返回内容推荐

常见做法是,在入口文件里先做一个环境检查,把缺失的扩展直接报出来,避免使用者跑起来才发现某个函数不存在。我一般会在项目根目录放一个check_env.php,浏览器访问一下就能看到当前环境是否满足要求。

2.2 目录结构:每个文件干什么

源码包的目录结构大致是这样的:

haifeng-domain/ ├── index.php # 演示入口,可直接访问测试 ├── config.php # 全局配置:超时、后缀列表、缓存开关 ├── core/ │ ├── Query.php # 查询调度器,根据后缀选择查询方式 │ ├── WhoisQuery.php # whois 协议查询实现 │ ├── HttpQuery.php # HTTP 接口查询实现(备用通道) │ └── Parser.php # 返回内容解析器,提取关键字段 ├── lib/ │ ├── Cache.php # 文件缓存,避免重复查询 │ └── Helper.php # 公共函数:后缀提取、编码转换 └── data/ └── cache/ # 缓存文件存放目录,需可写

这个结构里,core/Query.php是调度中心,它不关心具体怎么查,只负责根据域名后缀决定走 whois 还是 HTTP。Parser.php是另一个关键点——不同后缀的 whois 返回格式差异很大,解析器要做的是从一堆文本里把「注册状态」「到期时间」「注册商」这几个字段抠出来。

2.3 配置项怎么改

config.php里几个参数值得单独说:

<?php return [ // 查询超时时间,单位秒。whois 服务器响应慢时可适当调大 'timeout' => 10, // 是否开启文件缓存。批量查询同一域名时建议开启 'cache_enable' => true, // 缓存有效期,单位秒。默认 1 小时 'cache_ttl' => 3600, // 支持查询的后缀列表,不在列表内的后缀会直接返回不支持 'suffix_list' => ['com', 'net', 'cn', 'org', 'io', 'com.cn'], // whois 服务器映射,可自行补充 'whois_server' => [ 'com' => 'whois.verisign-grs.com', 'cn' => 'whois.cnnic.cn', 'io' => 'whois.nic.io', ], ];

timeout这个值别设太小,有些 whois 服务器在高峰期响应会超过 5 秒,设成 10 秒是比较稳妥的。cache_enable在批量查询场景下一定要开,否则同一个域名查两次就是两次网络请求,既慢又容易被限流。suffix_list决定了哪些后缀会被处理,不在列表里的直接返回「不支持」,避免程序去查一个根本没有 whois 服务器的后缀。

3. 核心查询链路:从域名输入到结果返回

3.1 查询调度器:一个入口分流两种通道

Query.php是整个项目的调度入口,它的逻辑不复杂,但决定了后续走哪条路:

<?php class Query { private $config; public function __construct($config) { $this->config = $config; } /** * 查询域名状态 * @param string $domain 完整域名,如 example.com * @return array 包含 status/expire/registrar 的数组 */ public function check($domain) { // 提取后缀,判断是否在支持列表内 $suffix = Helper::getSuffix($domain); if (!in_array($suffix, $this->config['suffix_list'])) { return ['status' => 'unsupported', 'msg' => '后缀暂不支持']; } // 缓存命中则直接返回,不走网络 if ($this->config['cache_enable']) { $cached = Cache::get($domain); if ($cached !== false) { return $cached; } } // 根据后缀选择查询通道 if (isset($this->config['whois_server'][$suffix])) { $query = new WhoisQuery($this->config); } else { $query = new HttpQuery($this->config); } $raw = $query->fetch($domain); $result = Parser::parse($raw, $suffix); // 写入缓存 if ($this->config['cache_enable']) { Cache::set($domain, $result, $this->config['cache_ttl']); } return $result; } }

这段代码里几个关键点:Helper::getSuffix()负责从完整域名里把后缀抠出来,比如example.com.cn要返回com.cn而不是cn,这里需要做一次后缀匹配。缓存判断放在网络请求之前,命中就直接返回,这是批量查询场景下最省时间的优化。通道选择依据是whois_server映射表里有没有这个后缀,有就走 whois,没有就走 HTTP 备用通道。

3.2 whois 查询实现:socket 直连与超时控制

WhoisQuery.php负责和 whois 服务器建立连接并读取返回内容:

<?php class WhoisQuery { private $config; public function __construct($config) { $this->config = $config; } /** * 向 whois 服务器发起查询 * @param string $domain 完整域名 * @return string 原始返回文本 */ public function fetch($domain) { $suffix = Helper::getSuffix($domain); $server = $this->config['whois_server'][$suffix]; // 建立 socket 连接,超时时间从配置读取 $fp = @fsockopen($server, 43, $errno, $errstr, $this->config['timeout']); if (!$fp) { return ''; // 连接失败返回空,由上层决定如何处理 } // 设置读取超时,避免服务器不返回时一直卡住 stream_set_timeout($fp, $this->config['timeout']); // whois 协议规定查询内容以换行结尾 fwrite($fp, $domain . "\r\n"); $result = ''; while (!feof($fp)) { $result .= fgets($fp, 1024); } fclose($fp); return $result; } }

fsockopen的第五个参数是连接超时,这里直接用了配置里的timeout。stream_set_timeout是读取超时,两个都要设,否则连接上了但服务器不返回数据,程序会一直卡在fgets上。whois 协议默认走 43 端口,查询内容以\r\n结尾,这是协议规定,不能省。返回的原始文本不做任何处理,直接交给Parser去解析,这样职责分离得更干净。

3.3 解析器:从一堆文本里抠出关键字段

Parser.php是这个项目里最需要耐心写的部分,因为不同后缀的 whois 返回格式差异很大:

<?php class Parser { /** * 解析 whois 原始返回 * @param string $raw 原始文本 * @param string $suffix 域名后缀 * @return array 结构化结果 */ public static function parse($raw, $suffix) { $result = [ 'status' => 'unknown', 'expire' => '', 'registrar' => '', ]; if (empty($raw)) { $result['status'] = 'error'; return $result; } // 统一转小写,避免大小写导致匹配失败 $text = strtolower($raw); // 判断是否已注册:不同后缀的关键词不一样 if (strpos($text, 'no match') !== false || strpos($text, 'not found') !== false || strpos($text, 'no data found') !== false) { $result['status'] = 'available'; return $result; } // 已注册,提取到期时间和注册商 if (preg_match('/expir(y|ation) date:\s*(.+)/i', $raw, $m)) { $result['expire'] = trim($m[2]); } if (preg_match('/registrar:\s*(.+)/i', $raw, $m)) { $result['registrar'] = trim($m[1]); } $result['status'] = 'registered'; return $result; } }

解析逻辑分两步:先判断是否可注册,再提取已注册域名的附加信息。判断可注册的关键词用了no match、not found、no data found三个,覆盖了大部分 whois 服务器的返回习惯。到期时间和注册商用正则提取,expir(y|ation) date这个写法是为了兼容expiry date和expiration date两种写法。注意这里没有做编码转换,如果返回内容不是 UTF-8,需要在Helper里先转码再解析。

4. 避坑与常见问题:跑起来之后容易卡在哪

4.1 查询超时但没有任何报错

现象是页面一直转圈,最后返回空白或者超时提示,但日志里看不到具体错误。原因通常是fsockopen连接失败后返回了空字符串,上层没有区分「连接失败」和「域名可注册」两种情况。解决方式是在WhoisQuery::fetch()里把连接失败的状态单独标记出来,比如返回false而不是空字符串,然后在Query::check()里判断$raw === false时直接返回status => error,不要继续走解析流程。

4.2 某些后缀查询结果始终是 unknown

现象是.cn或.io域名查出来状态永远是unknown。原因是这些后缀的 whois 返回格式和.com不一样,Parser里的关键词没匹配上。解决方式是先把原始返回打印出来看一眼,确认它的「可注册」提示词是什么,然后往Parser的判断条件里补。我一般会在Parser里加一个调试开关,打开后把原始文本写到日志文件,方便对照。

4.3 批量查询时被 whois 服务器限流

现象是查了十几个域名之后,后续全部返回空或超时。原因是 whois 服务器对同一 IP 的请求频率有限制,短时间大量请求会被临时封禁。解决方式有两个:一是开启缓存,同一个域名短时间内不重复查;二是在批量查询的循环里加sleep(1),每次查询间隔一秒。如果量确实大,建议把查询任务拆到多个时间段执行,不要一次性跑完。

4.4 缓存文件越来越多,磁盘占用上涨

现象是data/cache/目录下文件数量持续增长,几个月后占了几百 MB。原因是缓存文件只写不删,过期文件没有被清理。解决方式是加一个清理逻辑,在每次写入缓存时以一定概率触发过期文件扫描,把修改时间超过cache_ttl的文件删掉。或者更简单一点,用 cron 定时任务每天清理一次。

4.5 中文域名查询返回乱码

现象是查询中文域名时,返回内容里出现乱码。原因是中文域名需要先转成 punycode 再查询,直接传中文给 whois 服务器会返回错误内容。解决方式是在Query::check()入口处加一个判断,如果域名包含非 ASCII 字符,先用idn_to_ascii()转成 punycode 再走后续流程。这个函数需要intl扩展支持,环境检查里要加上。

5. 二次开发:把查询模块改造成自己的工具

5.1 扩展后缀支持:加一个后缀要改哪几个地方

假设你要加.xyz后缀的支持,需要动三个地方。第一,在config.php的suffix_list里加上'xyz'。第二,在whois_server映射里加上'xyz' => 'whois.nic.xyz'。第三,如果.xyz的返回格式和现有解析逻辑不兼容,需要在Parser::parse()里补充对应的关键词判断。改完之后用index.php测试一下,确认返回状态正确。

5.2 接入自己的项目:三种调用方式

最直接的调用方式是在你的入口文件里引入核心类:

<?php require_once 'haifeng-domain/core/Query.php'; require_once 'haifeng-domain/core/WhoisQuery.php'; require_once 'haifeng-domain/core/HttpQuery.php'; require_once 'haifeng-domain/core/Parser.php'; require_once 'haifeng-domain/lib/Cache.php'; require_once 'haifeng-domain/lib/Helper.php'; $config = require 'haifeng-domain/config.php'; $query = new Query($config); $result = $query->check('example.com'); // 输出:['status' => 'registered', 'expire' => '2025-...', 'registrar' => '...'] print_r($result);

如果你用的是 Composer,可以把core/和lib/目录加到autoload的classmap里,省去手动require。另一种方式是把整个工具包作为一个独立目录放进项目,通过命名空间隔离,避免类名冲突。我一般会在项目里建一个tools/目录专门放这类小工具,每个工具一个子目录,互不干扰。

5.3 改造为 API 接口:返回 JSON 而不是数组

如果你想把查询功能暴露成 HTTP 接口,可以在index.php基础上改:

<?php header('Content-Type: application/json; charset=utf-8'); $domain = isset($_GET['domain']) ? trim($_GET['domain']) : ''; if (empty($domain)) { echo json_encode(['code' => 400, 'msg' => '缺少 domain 参数']); exit; } // 简单校验域名格式,避免传入非法字符 if (!preg_match('/^[a-z0-9\-\.]+\.[a-z]{2,}$/i', $domain)) { echo json_encode(['code' => 400, 'msg' => '域名格式不正确']); exit; } $config = require 'config.php'; $query = new Query($config); $result = $query->check($domain); echo json_encode(['code' => 0, 'data' => $result]);

这个接口改造的关键点是加了参数校验,避免用户传入奇怪的内容导致查询异常。返回格式统一成code + data的结构,方便前端处理。如果你要对外提供服务,建议再加一层频率限制,比如同一个 IP 每分钟最多查 10 次,防止被滥用。

5.4 一个我踩过的坑:缓存 key 的命名

最开始写缓存的时候,我直接用域名做文件名,比如example.com.cache。跑了一段时间发现有些域名带特殊字符,比如xn--开头的 punycode 域名,文件名里带-没问题,但有些后缀带点号,比如example.com.cn,文件名里有两个点,在某些文件系统上会有问题。后来改成用md5($domain)做文件名,彻底避开了特殊字符的问题。从那以后我每次写文件缓存都强制走一遍md5命名,不再直接用原始字符串做文件名。希望这个习惯能帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进