21 10 月 2026

公共服运营到一定规模,你会需要一张状态页:玩家打开就知道服在不在、多少人在线、什么版本、延迟多少——省掉群里每天问"服炸了吗"。这篇教程用一套 30 行的探测脚本 + 一个静态页做出 Battlemetrics 风格的状态监控,跑在自己的服务器上,数据不外传。

和运维向的 Uptime Kuma 教程(Uptime Kuma 状态监控)互补:那篇看"服务活着没",这篇做"给玩家看的门面"。

本文由财富云计算整理,我们主营 NAT 型云服务器(无独立公网 IP),价格比独立 IP 机型省一半以上。更多开服教程见端口速查表。

一、效果与原理

最终效果:一个网页,显示:

  • 服务器在线状态(绿点/红点)
  • 当前在线人数 / 最大人数
  • 地图名、版本号、游戏模式
  • 服务器延迟(ms)
  • 玩家列表(可选)

原理分三块:

  1. 探测:用 Steam A2S 查询协议(所有 Source 引擎/Steam 游戏服都支持)定时查询游戏端口,拿到人数、地图、版本
  2. 输出:探测结果写成 JSON 文件放 Web 目录
  3. 展示:静态 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 安装与配置 · 游戏服运营完全手册

延伸阅读
  1. 高防误区与避坑清单:25 个坑+下单前必问 14 问+到货验货流程
  2. DNS 攻击防护专题:劫持、隧道、投毒、放大四类攻击与 DNSSEC
  3. 电商大促防攻击预案:T-14 到当天的分级响应与降级开关
  4. 源站真实 IP 隐藏完全指南:六大暴露面排查+NAT 型终极方案
  5. 日志分析找攻击源:Nginx 日志三维度分析法与证据固定
  6. 攻击应急响应手册:六阶段响应流程,从确认到复盘的照做清单
  7. iptables 抗 DDoS 配置模板:状态过滤+SYN 防护+防扫描+游戏服版
  8. OpenResty Lua 自研 WAF:动态限流+攻击特征检测+Redis 黑名单
更多阅读
  1. 上一篇:游戏服攻击防护实战:DDoS、连接耗尽、RCON 爆破的识别与防御
  2. 下一篇:高防服务器选购指南:防护带宽、独享共享、黑洞时长一篇看懂
发表评论 2

取消回复