公共服运营到一定规模,你会需要一张状态页:玩家打开就知道服在不在、多少人在线、什么版本、延迟多少——省掉群里每天问"服炸了吗"。这篇教程用一套 30 行的探测脚本 + 一个静态页做出 Battlemetrics 风格的状态监控,跑在自己的服务器上,数据不外传。
和运维向的 Uptime Kuma 教程(Uptime Kuma 状态监控)互补:那篇看"服务活着没",这篇做"给玩家看的门面"。
本文由财富云计算整理,我们主营 NAT 型云服务器(无独立公网 IP),价格比独立 IP 机型省一半以上。更多开服教程见端口速查表。
一、效果与原理
最终效果:一个网页,显示:
- 服务器在线状态(绿点/红点)
- 当前在线人数 / 最大人数
- 地图名、版本号、游戏模式
- 服务器延迟(ms)
- 玩家列表(可选)
原理分三块:
- 探测:用 Steam A2S 查询协议(所有 Source 引擎/Steam 游戏服都支持)定时查询游戏端口,拿到人数、地图、版本
- 输出:探测结果写成 JSON 文件放 Web 目录
- 展示:静态 HTML 页 JS 定时拉 JSON 渲染
不装复杂服务,一个脚本 + nginx 静态托管就够。
二、部署探测脚本
用 Python + python-valve 库(轻量,只做查询):
# 系统 Python 或 venv 均可
pip install python-valve探测脚本 /opt/status/query.py:
#!/usr/bin/env python3
import json, time, os
import valve.source as vs
import valve.source.a2s as a2s
import valve.source.master_server as ms
SERVERS = [
{"name": "英灵殿-主服", "host": "127.0.0.1", "port": 2456},
# 多服继续加:{"name": "Rust-周清", "host": "127.0.0.1", "port": 28015},
]
OUT = "/opt/status/www/status.json"
def query_one(s):
t0 = time.time()
try:
with a2s.ServerQuerier((s["host"], s["port"])) as q:
info = q.info()
players = q.players()
rtt = int((time.time() - t0) * 1000)
return {
"name": s["name"],
"online": True,
"ping": rtt,
"game": info.get("game", "?"),
"server_name": info.get("server_name", "?"),
"map": info.get("map", "?"),
"players": info.get("player_count", 0),
"max_players": info.get("max_players", 0),
"version": info.get("game_version", "?"),
"player_list": [
{"name": p["name"], "score": p["score"], "duration": int(p["duration"])}
for p in players.get("players", [])[:20]
],
}
except Exception as e:
return {"name": s["name"], "online": False, "ping": None, "error": str(e)}
result = {"updated": int(time.time()), "servers": [query_one(s) for s in SERVERS]}
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"[{time.strftime('%F %T')}] status written")注意查询地址:服务器自己查自己,用 127.0.0.1:游戏端口(NAT 型查内网 IP 即可,不需要走外网映射)。查询协议走的是 UDP,服务端监听就能响应。
NAT 型用户:探测脚本在机器上跑,查 127.0.0.1 或内网 IP,零配置。对外展示的 Web 页另说(见第四节)。
定时任务,每 30 秒一次:
(crontab -l 2>/dev/null; echo "* * * * * /opt/status/query.py >> /opt/status/query.log 2>&1"; echo "* * * * * sleep 30 && /opt/status/query.py >> /opt/status/query.log 2>&1") | crontab -(两行错开 30 秒,实现 30 秒粒度。)
三、写一个像样的状态页
/opt/status/www/index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>服务器状态</title>
<style>
body { font-family: system-ui, sans-serif; background: #f5f6f8; color: #222; max-width: 720px; margin: 40px auto; padding: 0 16px; }
h1 { font-size: 22px; }
.card { background: #fff; border-radius: 10px; padding: 16px 20px; margin: 12px 0; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
.row { display: flex; justify-content: space-between; align-items: center; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 8px; }
.on { background: #22c55e; } .off { background: #ef4444; }
.meta { color: #666; font-size: 13px; margin-top: 6px; }
.big { font-size: 24px; font-weight: 700; }
table { width: 100%; border-collapse: collapse; font-size: 14px; margin-top: 10px; }
td, th { text-align: left; padding: 6px 4px; border-bottom: 1px solid #eee; }
.foot { color: #999; font-size: 12px; text-align: center; margin-top: 20px; }
</style>
</head>
<body>
<h1>🎮 服务器状态</h1>
<div id="list">加载中…</div>
<div class="foot">数据每 30 秒自动刷新 · <span id="t"></span></div>
<script>
async function load() {
const r = await fetch('status.json?_=' + Date.now());
const d = await r.json();
document.getElementById('t').textContent = '更新于 ' + new Date(d.updated * 1000).toLocaleTimeString();
document.getElementById('list').innerHTML = d.servers.map(s => s.online ? `
<div class="card">
<div class="row">
<div><span class="dot on"></span><b>${s.name}</b>
<div class="meta">${s.server_name} · ${s.map} · v${s.version}</div></div>
<div class="big">${s.players}<span style="font-size:14px;color:#888">/${s.max_players}</span></div>
</div>
<div class="meta">延迟 ${s.ping} ms</div>
${s.player_list && s.player_list.length ? `<table><tr><th>玩家</th><th>分数</th><th>时长</th></tr>
${s.player_list.map(p => `<tr><td>${p.name || '(空)'}</td><td>${p.score}</td><td>${Math.floor(p.duration/60)} 分钟</td></tr>`).join('')}
</table>` : ''}
</div>` : `
<div class="card">
<div class="row"><div><span class="dot off"></span><b>${s.name}</b>
<div class="meta">离线或查询超时</div></div></div>
</div>`).join('');
}
load(); setInterval(load, 30000);
</script>
</body>
</html>纯静态,无依赖,手机访问自适应。
四、对外发布:nginx + 域名 + HTTPS
状态页要给外网玩家看,配 nginx:
server {
listen 443 ssl;
server_name status.你的域名;
ssl_certificate /etc/nginx/ssl/你的域名.pem;
ssl_certificate_key /etc/nginx/ssl/你的域名.key;
root /opt/status/www;
index index.html;
location / {
try_files $uri $uri/ =404;
}
# status.json 不允许缓存
location = /status.json {
add_header Cache-Control "no-store";
}
}HTTPS 证书申请见SSL 证书完全指南,nginx 配置基础见Nginx 安装与配置。
NAT 型用户注意:Web 服务需要映射 80/443 端口(HTTP/HTTPS 是固定端口,NAT 商家一般都提供)。如果商家不提供 443 映射,退而求其次用非标准端口 + 域名(如 status.域名:8443),或者状态页就挂在内网/群文件分享。域名解析和备案事项见域名解析指南——没有备案域名时先纯 IP 访问,玩家能打开就行。
五、进阶玩法
1. 状态页加群入口:HTML 里塞个"加入群聊"按钮,公共服引流主路径。
2. 多游戏聚合:一台机器跑英灵神殿 + Rust + MC,query.py 的 SERVERS 数组加三条,一页看全,这就是小型 hosting 面板的雏形(多游戏服管理)。
3. 历史在线曲线:query.py 每次把 {"t": 时间戳, "players": 人数} 追加到 history.jsonl,前端用 Chart.js 画 24 小时在线曲线——判断活跃时段、安排活动时间的依据:
# query.py 末尾追加
with open("/opt/status/www/history.jsonl", "a") as f:
for s in result["servers"]:
f.write(json.dumps({"t": result["updated"], "name": s["name"],
"players": s.get("players", 0), "online": s["online"]}) + "\n")4. 挂了自动重启联动:状态页只展示不动作;发现服挂了要能自愈,靠的是游戏服监控里的 systemd 自动重启 + Uptime Kuma 报警,这套是给运维看的,和玩家门面页分工明确。
5. Discord/QQ 群机器人推送:query.py 结果 POST 到 Webhook,人多时往群里推"服务器已恢复/人数破纪录"(Webhook 用法见自动更新实战第四节)。
六、常见问题
Q1:查询脚本报 timeout?
游戏端口没在监听(服没起)或防火墙挡了 UDP。服务器本地查用 127.0.0.1:端口 先排除防火墙因素。
Q2:状态显示 0 人在线但实际有人?
查询协议拿的是 Steam 层玩家数,部分游戏(饥荒、PZ)的 A2S 玩家列表可能不准。看 player_count 即可,玩家列表当参考。
Q3:页面打不开?
nginx 起了吗、443 映射了吗、证书路径对吗。curl -k https://localhost/status.json 本地先测。
Q4:数据不刷新?
crontab 挂了吗(crontab -l 看)、status.json 的 mtime 在动吗、浏览器缓存(HTML 里已加 ?_= 防缓存)。
Q5:手机访问样式错乱?
页面已做 viewport 适配;群公告里直接发链接,玩家点开就能看。
Q6:怕被扫端口拖垮?
状态页本身静态、无数据库,被扫也无非是点流量。Web 侧基础防护(限速、WAF)见Nginx 限速与 WAF。
七、小结
一张状态页 = 一个查询脚本 + 一个静态页 + 一个 nginx。半小时搭完,收益是群里少 80% 的"服在吗"和玩家多一分的信任。公共服运营者建议再加历史在线曲线,那才是判断"这服活着吗"的真数据。
需要服务器搭状态页 + 跑游戏服,可以看看财富云计算的 NAT 型云服务器——无独立公网 IP,价格比独立 IP 机型省一半以上,443 端口映射齐全,查询脚本查内网零配置。更多开服与运维教程见游戏配置与开服教程总索引。
相关阅读:Uptime Kuma 状态监控 · 游戏服监控实战 · SSL 证书完全指南 · Nginx 安装与配置 · 游戏服运营完全手册
延伸阅读
- 高防误区与避坑清单:25 个坑+下单前必问 14 问+到货验货流程
- DNS 攻击防护专题:劫持、隧道、投毒、放大四类攻击与 DNSSEC
- 电商大促防攻击预案:T-14 到当天的分级响应与降级开关
- 源站真实 IP 隐藏完全指南:六大暴露面排查+NAT 型终极方案
- 日志分析找攻击源:Nginx 日志三维度分析法与证据固定
- 攻击应急响应手册:六阶段响应流程,从确认到复盘的照做清单
- iptables 抗 DDoS 配置模板:状态过滤+SYN 防护+防扫描+游戏服版
- OpenResty Lua 自研 WAF:动态限流+攻击特征检测+Redis 黑名单

[...]netsh advfirewall firewall add rule name="GameRange" dir=in action=allow protocol=UDP localport=26901-26903完整的 Windows 环境准备(远程桌面连接、运行库安装、SteamCMD 用法、开机自启、8 个常见坑)见:《Windows 云服务器开服通用前置教程》→ htt[...]
2026-10-21 回复
[...]"定时跑(cron 每分钟),失败告警——查询端口挂了,玩家群里会先于监控发现,你不想被动解释。管理面的完整防护见RCON 安全篇,备份策略见备份体系。十、小结游戏服防护和网站是两个世界:CDN 缺席,UDP 清洗是核心,RCON 和查询端口是最常被忽视的软肋。选型记住四点:UDP 必须洗、公共服必须独享 IP、端口段提前规划、RCON 绝不裸奔。架构上「NAT 型源站 + 高防转发」是[...]
2026-10-21 回复