简介这是一份可直接部署的WHOIS域名信息查询源码面向站长、网络管理员及PHP初学者用于快速搭建域名注册信息查询页面满足日常了解域名归属、Nameserver详情等需求。源码包共19个文件、约290KB主要包含PHP业务逻辑、HTML页面模板、CSS样式、JavaScript交互脚本以及woff、ttf、eot、svg格式的字体图标和png、gif图片素材目录结构清晰便于按需修改与二次开发。已有278人学习下载适合作为域名查询功能的参考实现。资源提供完整的查询流程覆盖从发起查询、服务端处理、结果展示到错误兜底的各个环节并附带logo、等待动画及美化说明页开箱即用。通过学习这份源码既能掌握WHOIS协议的基本调用方式也能理解PHP后端参数处理与前端响应式布局的结合思路直接部署到服务器即可为个人网站增加实用的域名信息查询工具。1. 域名信息查询同款WHOIS源码.zip先想明白你拿到的是协议客户端还是网页壳很多人下载到“域名信息查询同款 WHOIS 源码.zip”以后第一件事就是解压、传站、填个域名试一下结果换一个后缀就报错或者卡在“正在查询”半天。问题不在 PHP 版本也不在服务器配置而是大部分人对 WHOIS 有一个错觉以为它是网页、是接口、是某个数据库。真正的 WHOIS 查询走的是几十年前留下的 TCP 43 端口文本协议你得先连上一台 whois 服务器发一行域名然后等它把纯文本甩回来。这篇笔记把协议、源码、部署和踩坑串成一条线看完你既能判断手里 zip 的成色也能自己动手改出一套能用的域名信息查询服务。2. WHOIS 源码背后的协议常识43 端口、服务器分域与文本解析2.1 一次 WHOIS 查询网络层只发生了一件小事WHOIS 本质上不是“查数据库”也不是“请求一个 JSON 接口”。它是一条裸 TCP 连接客户端连到目标 whois 服务器的 43 端口把要查的域名加回车换行发过去然后一直读响应直到服务器关闭连接。所有号称“域名信息查询源码”的 zip不管它是 PHP 还是 Python核心都脱不开这个动作。我先用 Python 写一个最小实现方便你理解后面 PHP 源码里那些函数的来源。import socket def whois_raw(domain, serverwhois.iana.org, timeout10): client socket.create_connection((server, 43), timeouttimeout) client.sendall((domain \r\n).encode(ascii, errorsignore)) buf b while True: chunk client.recv(4096) if not chunk: break buf chunk client.close() return buf.decode(utf-8, errorsreplace)这个函数只做四件事建立 TCP 连接、发送查询、循环收数据、断开连接。参数里最容易忽略的是\r\n。WHOIS 协议按行交互很多老服务器只认“回车换行”你只发\n它就可能一直等你补回车。timeout参数必须给否则一条坏记录能把整个进程挂住.recv(4096)一次最多收 4KB至于响应多长只能靠循环读。WHOIS 没有“长度字段”服务器关闭连接就是响应结束的标记。市面上的 zip 源码基本就是把这个逻辑搬到 PHP 或 Python但有些版本会在“读取结束”的判断上翻车比如用fread($fp, 8192)只读一次就返回。响应一长数据就缺了一半。判断结束要依赖feof或流超时不能依赖字节长度。2.2 域名后缀决定查询服务器为什么同一套源码有的域名能查有的不能WHOIS 不是全世界一台服务器。查询.com和查询.cn背后是完全不同的注册局地址也完全不同。你的源码必须知道“这个后缀应该连哪台 whois 服务器”。常见的映射关系大概是这样域名后缀常见 whois 服务器特点.com / .netwhois.verisign-grs.com注册局统一查询字段比较稳定.orgwhois.pir.org字段偶尔有多值.cnwhois.cnnic.cn可能返回中文编码需要处理.iowhois.nic.io响应里经常带 referral 信息.xyzwhois.nic.xyzDomain Status 字段可能多次出现大多数“同款源码 zip”用的是内置映射表这也是为什么不同 zip 之间区别很大谁的servers.json全谁就能查更多后缀谁的表旧谁就得自己补。不依赖内置表也可以PHP 里常见做法是先查 IANA 的 whois 服务器让它告诉你这个后缀该去哪。用 Python 写就是def resolve_whois_server(tld, timeout10): raw whois_raw(tld, whois.iana.org, timeout) for line in raw.splitlines(): if whois:// in line: return line.split(whois://, 1)[1].strip() return 这里查询对象不写完整域名只写后缀。IANA 收到.com之后会返回一条类似whois://whois.verisign-grs.com的 referral 记录。注意whois://后面可能带端口或路径但不一定每台服务器都会返回正确内容。解析失败就退回内置表不能把异常直接抛给用户。这个函数只适合在启动时调用一次不适合每次查询都实时请求 IANA否则反而会多一次网络开销也更容易被限流。2.3 原始响应不是 JSON解析前先认清三种数据噪音WHOIS 返回的是纯文本结构大致是“一行字段名 冒号 一行值”但里面有大量噪音。% IANA WHOIS server % query: example.com Domain Name: EXAMPLE.COM Registry Domain ID: 1234_DOMAIN_COM-VRSN Registrar WHOIS Server: whois.example-registrar.com Registrar: Example Registrar, LLC Domain Status: clientTransferProhibited Creation Date: 1995-08-14T04:00:00Z先看%开头的行那是注释不该当字段解析#和开头也可能出现。再看Registrar WHOIS Server这是一个 referral 线索不是普通用户要看的字段。最后看Domain Status同一字段可能反复出现好几次解析时必须用数组合并不能只取最后一个。很多源码死在哪死在一个正则表达式只对单一后缀有效。换到.site或.top返回格式一变正则就匹配不上前端就显示空白。相比之下“按行拆 key: value”的兜底方案虽然朴素但至少不会把 WHOIS 原始信息丢掉。到第 4 章我会给一个能直接落地的解析函数。3. 拆开 zip 跑通最小 PHP 后端核心查询、映射表和缓存三件事3.1 常见目录布局以及我先看哪几个文件这类 WHOIS 源码包看多了以后你会发现结构大同小异。常见做法是一个 PHP 入口页面一个 AJAX 查询接口一个负责 socket 查询的库文件再加一个后缀映射表。project/ ├─ index.php # 入口页面渲染查询表单 ├─ query.php # 接收 domain 参数返回 JSON ├─ config.php # 超时、缓存时间等全局配置 ├─ lib/ │ └─ whois.php # 核心查询与解析函数 ├─ data/ │ └─ servers.json # 后缀到 whois 服务器的映射 └─ cache/ # 文件缓存目录运行后自动生成拿到 zip 先不急着传到服务器先打开config.php和data/servers.json看一眼。大部分功能问题都出在这两个文件要么超时设得太短要么映射表里根本没有你要查的后缀。如果 zip 里没有servers.json大概率是把映射写成了 PHP 数组或者写在lib/whois.php里。你补映射的时候要注意保持原有格式别同时用数组和 JSON 文件查错的时候会晕。3.2 最小可跑的查询函数用 fsockopen 而不是 file_get_contentsPHP 里查 WHOIS 最常见的错误写法是用file_get_contents(whois://...)。这个封装在部分环境里能通但它把协议细节都藏了出错时不好定位。我一般用fsockopen因为连接阶段的错误码和错误描述都拿得到。function whois_query($domain, $server, $timeout 10) { $fp fsockopen($server, 43, $errno, $errstr, $timeout); if (!$fp) { return conn_fail: . $errstr . ( . $errno . ); } stream_set_timeout($fp, $timeout); fwrite($fp, strtolower($domain) . \r\n); $raw ; while (!feof($fp)) { $raw . fgets($fp, 4096); } fclose($fp); return $raw; }函数里两个超时要分开看fsockopen的$timeout只管 TCP 连接阶段stream_set_timeout管的是后续读取阶段。有些同款源码只设置了连接超时连接成功却卡在读数据上照样把 PHP 进程挂住。strtolower($domain)不是必须但遇到某些服务器大小写敏感时会救你一命。fgets($fp, 4096)每次最多读 4096 字节按行拼接直到服务器关闭连接。如果返回字符串以conn_fail:开头说明是网络层问题和你要查的域名无关先不要怀疑业务代码。3.3 缓存与幂等公网部署必须先过这一关把查询接口直接暴露到公网等于给所有路过的人开了一个裸 socket 口子。只要有人不停刷域名你所在服务器的 IP 很快会被注册局的 whois 服务器限流接着所有查询全部超时。这种“血泪经验”我踩过不止一次。所以缓存不是可选项是必选项。WHOIS 信息变化很慢10 分钟以内的缓存完全够用。最简单的做法是文件缓存按“域名 服务器”组合做 md5 文件名。function whois_get($domain, $server, $cache_ttl 600) { $cache_dir __DIR__ . /cache/; if (!is_dir($cache_dir)) { mkdir($cache_dir, 0755, true); } $key md5(strtolower($domain) . | . $server); $file $cache_dir . $key . .json; if (is_file($file) (time() - filemtime($file) $cache_ttl)) { return json_decode(file_get_contents($file), true); } $raw whois_query($domain, $server); $data [ domain $domain, server $server, raw $raw, query_time time(), ]; file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX); return $data; }filemtime判断过期对 WHOIS 这种低频数据足够用。$cache_ttl 600指的是 600 秒如果你做的是公开查询工具可以再拉长到 3600。两个隐蔽的坑第一cache目录要确保 PHP 运行用户有写权限Nginx php-fpm 环境下常因为目录权限不对而报错第二缓存文件名不要直接用用户输入必须用md5做一层转换否则会遇到路径穿越问题。还有一个容易被忽略的场景查询不存在的域名时最好也缓存但 TTL 要短一些比如 60 秒不然用户刚注册完域名你这边还在缓存“查无此域名”。3.4 同款源码常见的“单跳”ReferralServer 被直接忽略WHOIS 查询经常会遇到“跳转”。比如你查一个域名当前 whois 服务器不掌握详细注册信息就会返回一条Registrar WHOIS Server: whois.xxx.com意思是“更详细的数据在另一台服务器上”。大多数 zip 里的源码不会做二次查询因为实现起来要维护一个链式跳转还要防止死循环。我一般建议直接查注册局服务器的数据而不是解析 referral 链。对普通用户来说能从注册局拿到域名状态、创建时间、过期时间已经够用。衣服不必一次扒干净否则页面速度会拖到不能接受。如果你的需求就是要把联系人字段也查全那就要写一个递归查询读到whois://或Registrar WHOIS Server之后再用目标服务器发起第二次连接。这个方案会明显增加响应时间而且不同注册商返回的 referral 格式不一样测试量不小。4. 让查询结果“能看”文本解析、前端轮询和接口限流4.1 先按行拆出 key-value再处理同名字段WHOIS 文本的解析不用写得很“聪明”。你不需要用正则去抓某个字段因为每个后缀的写法都不同。最稳妥的办法是按行拆除 key-value把原始信息尽量不留死角地结构化。function parse_whois_text($raw) { $lines preg_split(/\R/, $raw); $data []; foreach ($lines as $line) { $line trim($line); if ($line ) { continue; } $first $line[0]; if (in_array($first, [%, #, ])) { continue; } if (strpos($line, :) false) { continue; } [$key, $value] explode(:, $line, 2); $key strtolower(trim($key)); $value trim($value); if (isset($data[$key])) { if (!is_array($data[$key])) { $data[$key] [$data[$key]]; } $data[$key][] $value; } else { $data[$key] $value; } } return $data; }preg_split(/\R/)的好处是同时兼容\n和\r\n不用为 Windows 和 Linux 换行风格写两套逻辑。explode(:, $line, 2)的第三个参数很关键值里面如果还有冒号不会把字段拆断。同名字段转数组是必须的。例如“Domain Status”在一条 WHOIS 记录里可能出现多次如果你只保留最后一次前端就会漏掉“clientDeleteProhibited”这类状态。注释行过滤放在解析最前面避免把%内容当作正式字段展示给用户。4.2 前端表单异步请求、超时提示、结果回填WHOIS 查询速度不快一个完整查询少则两三秒多则十秒。前端如果用同步请求浏览器会一直转圈用户会以为页面卡死。我建议用fetch加AbortController做超时控制。document.querySelector(#whois-form).addEventListener(submit, async (e) { e.preventDefault(); const domain document.querySelector(#domain).value.trim(); const status document.querySelector(#status); const ctrl new AbortController(); const timer setTimeout(() ctrl.abort(), 12000); status.textContent 查询中通常需要 5 秒左右...; try { const resp await fetch(/query.php?domain encodeURIComponent(domain), { signal: ctrl.signal }); const data await resp.json(); renderWhoisTable(data.data || {}); status.textContent ; } catch (err) { if (err.name AbortError) { status.textContent 查询超时请稍后重试; } else { status.textContent 查询失败; } } finally { clearTimeout(timer); } });AbortController是浏览器标准的取消手段遇到 12 秒没响应就主动中断。我故意把前端超时设成 12 秒比后端 socket 超时的 10 秒长一点这样能给 PHP 留出连接和读取的时间也不会让用户一直死等。renderWhoisTable是你自己写的渲染函数按data.data里的 key-value 去生成表格。这里不展开但要注意一点fetch 返回的可能是 JSON也可能是 Nginx 的 502 错误页所以resp.json()要包在 try/catch 里否则会把 HTML 错误页当 JSON 解析。4.3 接口层限流IP 时间段计数别让查询接口裸奔公开接口必须做限流。WHOIS 查询不像普通 API 那样轻量一次查询要占用一个 socket还会消耗注册局对你 IP 的信任度。我一般会在query.php第一行就做一次按 IP 的计数。function ip_rate_limit($limit 30, $window 60) { $ip $_SERVER[REMOTE_ADDR] ?? unknown; $file __DIR__ . /rate/ . md5($ip) . .tmp; $now time(); $rec is_file($file) ? json_decode(file_get_contents($file), true) : []; if (!is_array($rec) || $rec[window_start] $window $now) { $rec [count 0, window_start $now]; } $rec[count]; file_put_contents($file, json_encode($rec), LOCK_EX); if ($rec[count] $limit) { http_response_code(429); exit(json_encode([error 请求太频繁请稍后再试])); } }$limit 30表示一个 IP 一分钟最多查 30 次普通用户完全够用脚本刷接口的基本会被挡住。文件缓存的方式适合单机部署多台服务器负载均衡时计数会不准但自助查询工具或内部工具不需要考虑这个精度。file_put_contents不是原子操作所以严格意义上会有一点计数误差。这个误差对限流来说可以接受重点是把“明显恶意”的连续请求挡在业务逻辑之外。4.4 字段展示原样显示不等于排版完成解析完成之后直接把 key-value 输出到页面上用户看到的是creation_date这种下划线字段名体验不太好。我习惯做一张字段映射表把常用字段转成中文名。原始 key小写后显示名备注domain_name域名一个查询里可能出现多行registrar注册商可能为空creation_date创建时间服务器格式不同保留原值expiry_date过期时间有的服务器叫 expiration_timedomain_status域名状态可能是数组显示规则只有一条字段存在就显示值不存在就是占位符。很多源码喜欢直接输出null或空字符串用户看到空白区域会以为查询坏了所以要在渲染层区分“字段不存在”和“值被隐藏”两种情况。5. WHOIS 源码包部署避坑zip 伪加密、出站 43 端口和隐私脱敏的排查记录5.1 解压时弹“输入密码”下载页却说免费先查是不是伪加密现象从网上拿到的 zip双击就弹密码框下载页面又没给密码不少人以为文件坏了。原因这类包里经常遇到“zip 伪加密”。所谓伪加密只是把 zip 文件头里的通用位标志位第 0 位置成了 1文件本身没有真正加密只是为了让你以为需要密码。解决先确认是不是真加密。直接改掉标志位把伪加密壳拆掉import struct def clear_fake_encryption(in_path, out_path): data bytearray(open(in_path, rb).read()) # 本地文件头 PK\x03\x04通用位标志在偏移 6 off 0 while True: off data.find(bPK\x03\x04, off) if off -1: break flag struct.unpack_from(H, data, off 6)[0] struct.pack_into(H, data, off 6, flag 0xFFFE) off 4 # 中央目录文件头 PK\x01\x02通用位标志在偏移 8 off 0 while True: off data.find(bPK\x01\x02, off) if off -1: break flag struct.unpack_from(H, data, off 8)[0] struct.pack_into(H, data, off 8, flag 0xFFFE) off 4 open(out_path, wb).write(data)这个脚本只清掉通用位标志的第 0 位对真加密文件无效。如果文件里的压缩数据确实被 ZipCrypto 或 AES 加密过清掉标志位之后解压照样报错甚至还会损坏 zip。它不是“密码移除工具”只适用于伪加密场景。下载源码包之前先留一个心眼压缩包来源不明的话解压后别急着上传到服务器先看看里面有没有不该出现的可执行文件。5.2 云服务器上查不动本地却秒回现象本机用nc或 whois 命令能查到数据部署到云服务器以后每次查询都超时日志里只有conn_fail。原因WHOIS 走的是 TCP 43 端口。部分云主机或机房的出站防火墙只会默认放行 80、443 等常用端口43 端口被安全策略挡在外面。这不是 PHP 代码的问题是网络层的问题。解决先测试目标端口通不通timeout 5 bash -c echo -e example.com\r\n /dev/tcp/whois.iana.org/43 echo port ok || echo port blocked命令能通再回去看代码命令不通就去安全组和出站防火墙里把目标地址的 TCP 43 放行。有些云面板默认不显示“出站防火墙”只让你看来访。如果找不到放行入口最省事的替代方案是改用 RDAP 走 443 端口做兜底第 6 章会说。5.3 隐私保护域名查回来一堆隐藏值不是解析失败现象查询结果里出现REDACTED_FOR_PRIVACY、Data 不可用、Privacy Protection这类值看起来像是源码解析错了。原因现在很多注册局和注册商会对个人信息做脱敏这是数据保护策略的一部分不是 WHOIS 协议本身出错。联系人姓名、邮箱、电话被隐藏是正常现象。解决别把这种情况当错误处理。前端要区分两个状态字段不存在显示“未提供”字段存在但值被隐藏显示“隐私保护”。如果把隐藏值和错误混在一起用户分不清是“域名信息真的没有”还是“你的查询工具坏了”。我在项目里通常把脱敏关键词收集成一个小数组REDACTED_FOR_PRIVACY、Privacy Protected、Data Not Available。解析后先检查一遍这些关键词命中就统一翻译成“对方已隐藏该字段”。5.4 查询 .cn 返回乱码JSON 输出变成“”现象查中文域名或.cn域名时页面出现大量乱码尤其是联系人和注册商字段。原因部分.cn的 whois 服务器返回内容是 GBK 或 GB2312 编码而源码里默认按 UTF-8 解析。PHP 的json_encode遇到非法 UTF-8 会直接返回false或者变成替换字符。解决在解析前做一次编码转换。$raw mb_convert_encoding($raw, UTF-8, GBK, UTF-8, ISO-8859-1);字符串里的编码来源列表是有讲究的。我一般先按GBK, UTF-8, ISO-8859-1的顺序去转换而不是直接用auto。auto在短文本和多字节混合时猜测准确率不高有时候会把中文猜成乱码。另外要注意转换要在parse_whois_text之前做不要在拼接 HTML 之后做否则字段已经被污染了。5.5 Nginx PHP-FPM 下出现 504响应时间比查询还长现象前端 AJAX 一直转圈最后 Nginx 返回 504 Gateway Timeout。原因WHOIS 查询是阻塞式 socket 操作一次慢查询可能要几十秒。Nginx 的proxy_read_timeout默认值不大PHP-FPM 也有执行时间限制谁先到点谁先断你就看到 504。解决两个方向。简单粗暴的做法是把 Nginx 的proxy_read_timeout调大到 60 秒同时把 PHP 的超时参数调大。但这条路走不长因为慢查询会占满 PHP-FPM 进程池用户一多照样全站卡死。我最终采用的方式是异步化用户提交查询后PHP 立即返回一个任务 ID后台脚本异步完成 WHOIS 查询前端再轮询结果。这个改动对同款源码来说不小但只要是公网服务阻塞式查询早晚会出问题。6. 进阶校验用 RDAP 兜底和多 TLD 回归清单打磨“同款源码”6.1 RDAP 兜底当 43 端口不通时还有一条 HTTPS 路RDAP 是 WHOIS 的现代替代协议它跑在 443 端口的 HTTPS 上返回的是标准 JSON。遇到端口被封锁、whois 服务器不稳定、原始文本解析失败的时候RDAP 是一个很实用的兜底方案。用 Python 写一个最小 RDAP 查询import json from urllib.request import urlopen def rdap_lookup(domain, timeout10): url fhttps://rdap.org/domain/{domain} with urlopen(url, timeouttimeout) as resp: body json.load(resp) return body.get(events, [])rdap.org会根据域名后缀自动跳转到对应的 RDAP 服务器不需要你自己维护映射表。返回的 JSON 里有events、entities、nameservers等结构化字段解析起来比纯文本 WHOIS 舒服得多。我现在的习惯是在源码层做双通道先用 WHOIS socket 查询超时或返回conn_fail:时改用 RDAP。这样既保留了原有 zip 源码的查询逻辑又能在网络受限时继续服务用户。6.2 多 TLD 回归清单上线前只跑三条用例每次改完解析代码或服务器映射不要只测一个域名。我固定跑一版回归表格通过率稳定才敢上线。测试对象期望结果排查方向example.com返回注册商、创建时间无超时域名转小写、CRLF、43 端口连通性example.cn中文不乱码编码转换、映射表是否正确example.io返回注册局字段是否出现只返回 referral 响应的情况这三个后缀覆盖了最常见的三类问题标准格式、中文编码、referral 跳转。如果这三条都过再补一条连续刷新 20 次观察 IP 是否被限流被限流就把缓存 TTL 调大同时检查限流计数器是不是误伤了正常用户。我在生产环境会把缓存 TTL 从 600 秒调整到 3600 秒并把每次查询的domain、server、query_time写入日志。这样即使某一天查询全部失败也能倒推出是哪台 whois 服务器在哪个时间段开始不响应而不是对着页面干瞪眼。如果你从网上下到的 zip 里有多余的管理页面、示例页面上线前最好先清理一遍。这类页面往往没有经过安全加固暴露在公网就是在给服务器添麻烦。把这些习惯守住你手里的“同款 WHOIS 源码”就不再是个解压后黑匣子而是一套能长期跑、能说清边界、能快速定位问题的域名信息查询服务。希望帮到你。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站