很多时候我们只想要一个「够用、能部署、好维护」的状态页:告诉玩家服务器是否在线、当前在线人数、以及大概的延迟。
这个项目就是为此而生:后端用 PHP 直连 Minecraft Java Edition 服务器做一次 Server List Ping(1.7+) 探测,前端用 Vue 渲染一个简洁的状态卡片,并通过一个非常轻量的 JSON API 把数据串起来。
适用场景:你有一个(或少量)Minecraft Java 服务器,希望把状态展示在网页上;或者你想要一个能看懂、可改造的 Server List Ping 参考实现。
前端内容
- 在线/离线:探测成功视为在线
- 在线人数(当前/上限):从服务器返回的
players.online/players.max里读取 - 延迟(毫秒):通过一次 ping/pong 粗略估算 RTT
- 部署简单:只需要 PHP 7.0+,把
public/设成站点根目录即可
目录结构速览
config.php:Minecraft 服务器地址、端口、超时等配置public/index.php:前端页面(Vue)与交互逻辑(刷新、懒加载等)public/api/status.php:JSON API,封装并返回探测结果,带一个很小的服务端缓存src/MinecraftStatus.php:Server List Ping 的核心实现(TCP socket + VarInt 协议)
后端是怎么“问”Minecraft 服务器状态的?
Minecraft Java 的服务器列表信息,并不是一个 HTTP 接口,而是基于 TCP 的协议交互。这个项目在 src/MinecraftStatus.php 里实现了 Server List Ping(1.7+) 的基本流程:
- 通过
fsockopen($host, $port, ...)建立 TCP 连接,并设置超时 - 发送 Handshake 包
- 发送 Status Request 包
- 读取服务端返回的 JSON(包含版本、描述、玩家等信息)
- 可选:发送 Ping 并读取 Pong,用于估算延迟
其中最容易踩坑的是 VarInt 编码(变长整数)和「写入/读取必须一次读够」的流式 IO。代码里把这些细节封装成了几个小函数:
writeVarInt()/readVarIntFromStream():处理 VarInt 编码/解码writeAll():确保 socket 写完整readExactly():确保 socket 读够指定长度
协议要点(结合代码理解)
-
Handshake 包里会写入:
packet id = 0x00protocol version = -1(表示“任意版本”,用于尽量兼容)server address(字符串)server port(2 字节网络序)next state = 1(status)
-
状态请求包非常短:
length=1,packet id=0x00 -
服务端响应
packet id=0x00,正文是一个 JSON 字符串,典型结构里会有:players.online、players.maxversion.name等
这也是为什么 API 层可以直接从 data.players.* 里读人数。
API:一个端点搞定
接口只有一个:
GET /api/status.php
返回示例(字段含义见 README):
{
"ok": true,
"online": true,
"host": "example.com",
"port": 25565,
"playersOnline": 0,
"playersMax": 20,
"latencyMs": 42,
"error": null
}
为什么同时有 ok 和 online?
ok:代表这次探测是否“跑完并解析到有效响应”online:代表服务器是否在线(一般和ok同步)
当出现网络错误、读写失败、解析失败等,ok=false,同时在 error 里带上可读的原因,前端会把它展示成一条错误提示。
服务端小缓存:让刷新更友好
public/api/status.php 里实现了一个非常小的缓存策略:
- 默认
cacheTtlSec = 10秒(可以在config.php的minecraft配置下新增cacheTtlSec覆盖) - 缓存写到系统临时目录
sys_get_temp_dir()下的一个 JSON 文件 - 浏览器端缓存被强制禁用:响应头
Cache-Control: no-store
这意味着:
- 用户狂点“刷新”时,不会每次都触发一次真实的 TCP 探测
- 但浏览器不会缓存页面/接口,刷新行为仍然“即时可见”
如果你的服务器状态变化很频繁、或者你希望每次都真实探测,把 cacheTtlSec 设为 0 就行。
前端:懒加载 + 手动刷新
页面在 public/index.php 里完成(Vue 直接挂载在同一个文件中)。核心交互非常克制:
- 首屏不立刻请求:用
IntersectionObserver判断页面进入视口后再触发请求 - 若浏览器支持
requestIdleCallback,会在空闲时发起请求(同时设置了超时兜底) - 用户也可以点击“刷新”按钮显式拉取最新状态
- 请求使用
axios.get('./api/status.php', { params: { t: Date.now() } }),加一个时间戳参数避免被中间缓存误伤
这种设计的好处是:
- 打开页面不会立刻“打扰”后端
- 你可以把状态页嵌在其他页面里,只有真正滚到它附近时才探测
配置与本地运行
编辑 config.php,填入你自己的服务器地址:
return [
'minecraft' => [
'host' => 'mc.example.com',
'port' => 25565,
'timeoutMs' => 1500,
// 'cacheTtlSec' => 10,
],
];
本地快速跑起来(仓库根目录):
php -S 127.0.0.1:8000 -t public
然后访问:
部署到 Nginx/Apache 时,把站点根目录指向 public/ 即可。
可扩展点(如果你想继续打磨)
这个项目刻意保持“轻量”,但扩展也很自然:
- 展示 MOTD / 版本号:API 里已经拿到了服务端返回的
data,可以按需提取字段 - 同时监控多个服务器:让
config.php支持列表,然后 API 增加参数或多端点 - 更稳健的错误分类:例如区分 DNS 失败、连接超时、读超时、协议异常
常见问题排查
- 一直离线 / connect failed:先确认运行 PHP 的机器能否访问
host:port(防火墙、NAT、云安全组) - 在线人数显示为
-:说明探测成功但没读到players信息(某些服务端/代理可能返回结构不同) - 延迟为 null:ping/pong 不是强制流程,若读取失败会返回空;也可能是网络抖动/超时
结语
这个项目的价值不在于“功能堆满”,而在于它把 Minecraft Java 的 Server List Ping 用 PHP 清晰地跑通了:连接、VarInt 编码、状态 JSON、以及一个足够实用的 API 封装。


