学校网太卡了,这是很多住过宿舍、泡过实验室的人都踩过的坑。白天访问还正常,晚高峰一到延迟飙高、丢包上升、网页转圈,问题到底出在校园网出口带宽、DNS 解析、无线干扰还是本机网卡,不测一下根本说不清楚。
这次我们来看一个可以自己部署的轻量级网络诊断面板。它的思路很直接:后端用 Python + Flask 写测速和延迟检测接口,前台套一个现成的 Web 管理模板,跑起来之后通过浏览器查看当前网络状态、历史趋势和丢包情况。整个项目不需要 GPU,不需要深度学习环境,几十分钟就能跑通最小版本。问题定位清楚了,再去找学校信息中心反馈,或者调整实验室的网络策略,就有数据支撑了。
先给结论:这个面板的特点是纯本地 Web 服务、Python 实现、无 GPU 需求、支持定时批量检测、提供 HTTP API、前端模板可替换。部署难度低,适合网络排查、日常监控和接口集成的场景。下面我会按“环境准备 → 安装部署 → 功能测试 → 前端模板 → 接口与批量任务 → 资源占用 → 问题排查”的顺序展开,末尾给出合规使用建议。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 本地网络诊断 Web 工具 |
| 后端技术 | Python Flask,可替换成 FastAPI |
| 前端模板 | Bootstrap 5 / AdminLTE 风格,静态资源本地化 |
| 主要功能 | 延迟测试、带宽测速、DNS 解析耗时、丢包率、历史记录 |
| 推荐硬件 | 普通 PC、迷你主机、树莓派,无 GPU 要求 |
| 支持系统 | Windows / Linux / macOS,需 Python 3.9+ |
| 启动方式 | 命令行启动,绑定自定义端口 |
| API 接口 | /api/health、/api/latency、/api/speed、/api/history |
| 批量任务 | 使用 APScheduler 定时检测,可选启用 |
| 适用场景 | 宿舍、实验室、办公室的校园网链路自查 |
说明:表格里的接口路径和功能清单是通用设计模板,实际部署时建议按照自己的项目结构调整。不要把它当成某个现成仓库的完整文档,重点是理解“后端接口 + 模板页面 + 定时任务”这套组合思路。
2. 适用场景与使用边界
这套面板解决的是“卡在哪”的问题,不是“把网变快”的问题。它适合下面这些情况:
- 学生或开发者想收集网络质量数据,向学校信息中心反馈时有依据。
- 实验室或工作室需要监控办公网络的延迟和带宽波动。
- 想验证不同时段、不同 DNS 服务器、不同网卡模式下的网络表现。
- 想把网络质量数据接入到自己的监控系统或告警脚本里。
不合适的场景也要说清楚:
- 不要用它去扫描校园网内未授权的主机和服务,主动探测他人设备可能违反学校网络管理规定。
- 不要用它绕过校园网认证、计费或访问控制机制。本文提到的所有功能只做本机或授权网关的常规测量。
- 不要把它当作正式的网管平台。它能记录趋势、暴露问题,但不会自动修复网络故障。
合规提醒放在前面:涉及网络测量时,先确认你有权限对自己所在网段和目标主机发起 ICMP、DNS、HTTP 请求。涉及人脸、声音、设备指纹等信息采集的别碰。校园网属于公共基础设施,任何未授权扫描都可能带来麻烦,这一点务必重视。
3. 环境准备与前置条件
3.1 确认 Python 版本
Flask 和 APScheduler 对 Python 版本要求不高,但建议使用 3.9 以上。
python --version如果系统里没有 Python,先去官网安装,或者在 Windows 的 Microsoft Store 里搜索 Python 3.11 或更高版本安装。Linux 下使用系统包管理器安装也可以。
3.2 确认端口与静态资源目录
默认会使用 8000 端口,先检查端口是否被占用:
- Windows:
netstat -ano | findstr :8000 - Linux / macOS:
lsof -i:8000
如果端口被占用,运行服务时换一个,比如 8080 或 9000。
模板页面需要的 Bootstrap、ECharts 等静态资源,建议提前下载后放到static/目录。不要在页面上直接引用公网 CDN,因为校园网访问外部 CDN 可能很慢甚至被拦截,本地化资源才能保证面板在断网时也能打开。
3.3 网络测量所需的权限
延迟测试依赖 ICMP 协议,部分校园网会限制 ICMP,导致 ping 超时但网页能正常打开。遇到这种情况,可以用 TCP 或 HTTP 探测代替 ping。
速度测试依赖公网测速节点。如果校园网对测速域名有流量限制或 QoS 限制,测出来的带宽不一定是你本机的真实带宽,这一点在分析结果时要留意。
4. 安装部署与启动方式
4.1 创建项目结构
推荐目录结构如下:
network-health-dashboard/ ├── app.py ├── requirements.txt ├── config.yaml ├── templates/ │ ├── base.html │ └── index.html ├── static/ │ ├── css/ │ │ ├── bootstrap.min.css │ │ └── custom.css │ ├── js/ │ │ ├── bootstrap.min.js │ │ ├── echarts.min.js │ │ └── main.js └── data/data/目录存放 SQLite 数据库,历史记录会写到这里。templates/和static/是 Flask 默认的模板与静态资源目录,名称不要随意改。
4.2 安装依赖
创建requirements.txt,内容如下:
flask==3.0.3 requests==2.32.3 psutil==5.9.8 ping3==4.0.8 speedtest-cli==2.1.3 dnspython==2.6.1 apscheduler==3.10.4 pyyaml==6.0.1版本号以后续实际安装结果为准,如果某个包在你们学校的 pip 源里不可用,去掉版本号重新安装:
pip install -r requirements.txt网络环境不稳定时可以换国内镜像源:
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple4.3 后端开发
app.py是一个最小可运行的 Flask 应用,同时包含初始化数据库、页面路由、延迟测试接口和速度测试接口。
import argparse import os import sqlite3 import time from datetime import datetime from flask import Flask, jsonify, render_template, request from ping3 import ping BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATABASE = os.path.join(BASE_DIR, "data", "history.db") app = Flask(__name__) def get_db(): conn = sqlite3.connect(DATABASE) conn.row_factory = sqlite3.Row return conn def init_db(): os.makedirs(os.path.dirname(DATABASE), exist_ok=True) conn = get_db() conn.execute( """ CREATE TABLE IF NOT EXISTS network_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, test_type TEXT, target TEXT, value REAL, unit TEXT, created_at TEXT ) """ ) conn.commit() conn.close() def write_history(test_type, target, value, unit): conn = get_db() conn.execute( "INSERT INTO network_history (test_type, target, value, unit, created_at) VALUES (?, ?, ?, ?, ?)", (test_type, target, value, unit, datetime.now().isoformat()), ) conn.commit() conn.close() @app.route("/") def index(): return render_template("index.html") @app.route("/api/health") def health(): return jsonify({"status": "ok", "time": datetime.now().isoformat()}) @app.route("/api/latency") def latency(): target = request.args.get("target", "223.5.5.5") rtt = ping(target, timeout=3) if rtt is None: result = {"target": target, "rtt_ms": None, "reachable": False} write_history("latency", target, -1, "ms") else: rtt_ms = round(rtt * 1000, 2) result = {"target": target, "rtt_ms": rtt_ms, "reachable": True} write_history("latency", target, rtt_ms, "ms") return jsonify(result) @app.route("/api/history") def history(): test_type = request.args.get("type", "latency") limit = request.args.get("limit", 50, type=int) conn = get_db() rows = conn.execute( "SELECT * FROM network_history WHERE test_type = ? ORDER BY id DESC LIMIT ?", (test_type, limit), ).fetchall() data = [dict(row) for row in rows] return jsonify(data) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=8000) args = parser.parse_args() init_db() app.run(host=args.host, port=args.port, debug=False)代码里默认测试目标是223.5.5.5,这是阿里公共 DNS。如果用校园网关做延迟测试,把目标地址换成网关 IP。目标 IP 不要填对外部未授权服务器的地址。
速度测试接口单独拆到一个路由里,因为speedtest-cli的耗时比较长,建议用异步执行或前端轮询结果。下面是简化的速度测试接口:
@app.route("/api/speed") def speed(): import speedtest st = speedtest.Speedtest() st.get_best_server() download = st.download() / 1_000_000 upload = st.upload() / 1_000_000 result = { "download_mbps": round(download, 2), "upload_mbps": round(upload, 2), "timestamp": datetime.now().isoformat(), } write_history("speed_download", speedtest.BEST_SERVER.get("host", "unknown"), download, "Mbps") write_history("speed_upload", speedtest.BEST_SERVER.get("host", "unknown"), upload, "Mbps") return jsonify(result)注意speedtest包的接口在不同版本中不完全一致,生产环境里建议把测速逻辑封装成独立模块,再在 API 层调用。
4.4 启动服务
在项目根目录执行:
python app.py --host 127.0.0.1 --port 8000启动后终端会显示 Flask 的访问地址。浏览器打开http://127.0.0.1:8000,能看到模板页面说明服务正常。
默认绑定127.0.0.1是为了安全,只有本机能访问。如果需要在宿舍里用手机查看,可以绑定到本机局域网 IP:
python app.py --host 0.0.0.0 --port 8000绑定0.0.0.0后,需要同步考虑防火墙和局域网访问控制。Windows 下开放端口需要管理员权限:
New-NetFirewallRule -DisplayName "NetworkDashboard" -Direction Inbound -Protocol TCP -LocalPort 8000 -Action AllowLinux 下可以使用 ufw:
sudo ufw allow 8000/tcp跨设备访问完成后,建议改回127.0.0.1,避免其他设备扫描到这个服务。
5. 功能测试与效果验证
5.1 服务健康检查
先确认服务和数据库都正常。浏览器访问http://127.0.0.1:8000/api/health,预期返回:
{ "status": "ok", "time": "2025-01-01T12:00:00" }如果页面提示 404,检查路由是否写错;如果提示 500,检查data/目录是否能写入。
5.2 延迟测试
访问下面链接,默认测试目标为223.5.5.5:
http://127.0.0.1:8000/api/latency?target=223.5.5.5预期返回:
{ "target": "223.5.5.5", "rtt_ms": 12.34, "reachable": true }判断成功的标准很直接:reachable为true,rtt_ms是正常数值。如果reachable为false,说明 ICMP 探测被拦截或目标不可达。
再增加一个对比测试,把目标换成校园网网关 IP,例如:
curl "http://127.0.0.1:8000/api/latency?target=192.168.1.1"网关延迟明显低于公网 IP 是正常的。如果网关延迟也高,问题可能出在无线链路或本机网卡。
5.3 带宽测速
调用速度测试接口:
curl "http://127.0.0.1:8000/api/speed"测速过程可能持续 30 到 60 秒,接口超时时间要调大。返回结果包含上传和下载速率:
{ "download_mbps": 85.23, "upload_mbps": 20.11, "timestamp": "2025-01-01T12:00:00" }宽带测速结果受测速节点影响很大,同一个网络在不同节点下结果可能差很多。建议在同一个节点下跑三次,取中位数,不要用单次最高值作为结论。
5.4 DNS 解析耗时测试
新增一个/api/dns接口,用dnspython测试域名解析耗时:
import dns.resolver @app.route("/api/dns") def dns_check(): domain = request.args.get("domain", "www.baidu.com") dns_server = request.args.get("dns", "223.5.5.5") resolver = dns.resolver.Resolver() resolver.nameservers = [dns_server] resolver.timeout = 3 resolver.lifetime = 3 start = time.time() try: answers = resolver.resolve(domain, "A") cost_ms = round((time.time() - start) * 1000, 2) result = { "domain": domain, "dns_server": dns_server, "resolve_time_ms": cost_ms, "ip": answers[0].address, } except Exception: result = { "domain": domain, "dns_server": dns_server, "resolve_time_ms": None, "ip": None, } return jsonify(result)测试时可以换成学校内部域名或常见公网域名。解析耗时超过几百毫秒,说明 DNS 配置可能有问题,可对比不同 DNS 服务器的耗时。
5.5 历史记录验证
跑完几次测试后,打开:
http://127.0.0.1:8000/api/history?type=latency&limit=10预期返回最近 10 条延迟记录。历史数据说明 SQLite 写入和查询链路是通的,后面做定时批量任务时,历史曲线就是从这里取数。
6. Web 模板套用:从静态页面到数据可视化
“套个模板”不是简单下载一个 HTML 就完事,关键是把后端返回的数据塞进模板,并让静态资源能在本机稳定加载。
先写templates/base.html,统一页面布局:
<!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{% block title %}网络诊断面板{% endblock %}</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}"> </head> <body> <div class="container-fluid"> <div class="row"> <nav class="col-md-2 sidebar"> <h5>网络诊断面板</h5> <ul> <li><a href="/">首页</a></li> <li><a href="/history">历史记录</a></li> </ul> </nav> <main class="col-md-10"> {% block content %}{% endblock %} </main> </div> </div> <script src="{{ url_for('static', filename='js/echarts.min.js') }}"></script> {% block scripts %}{% endblock %} </body> </html>再写templates/index.html:
{% extends "base.html" %} {% block content %} <div class="card"> <h5>延迟测试</h5> <div class="form-group"> <input type="text" id="target" value="223.5.5.5" class="form-control"> </div> <button class="btn btn-primary" id="run-latency">运行延迟测试</button> <div id="latency-result" class="mt-3"></div> </div> <div class="card"> <h5>延迟趋势</h5> <div id="chart" style="height: 300px;"></div> </div> {% endblock %} {% block scripts %} <script> document.getElementById("run-latency").addEventListener("click", async () => { const target = document.getElementById("target").value; const resp = await fetch("/api/latency?target=" + encodeURIComponent(target)); const data = await resp.json(); document.getElementById("latency-result").textContent = "延迟: " + data.rtt_ms + " ms, 可达: " + data.reachable; loadHistory(); }); async function loadHistory() { const resp = await fetch("/api/history?type=latency&limit=30"); const rows = await resp.json(); const times = rows.map(function(row) { return row.created_at; }); const values = rows.map(function(row) { return row.value; }); const chart = echarts.init(document.getElementById("chart")); chart.setOption({ xAxis: { type: "category", data: times }, yAxis: { type: "value" }, series: [{ type: "line", data: values, smooth: true }] }); } loadHistory(); </script> {% endblock %}模板套用有几个容易踩的坑:
- 静态资源路径必须用
url_for('static', filename='...')生成,不能写死/static/css/bootstrap.min.css,不然子路由下会 404。 - CDN 资源在校园网内不一定快,把 Bootstrap 和 ECharts 的文件下载到本地。
- 图表数据为空时,ECharts 会显示空白区域,要判断一下历史记录接口是否返回了数据。
如果需要更专业的管理后台风格,可以直接套 AdminLTE 或 Tabler 这类开源模板,把模板的dist静态文件放到static/下,再改一下base.html的导航结构即可。模板只是皮,核心还是接口返回的数据结构。
7. 接口 API 与批量任务
7.1 API 一览
| 接口 | 方法 | 参数 | 说明 |
|---|---|---|---|
| /api/health | GET | 无 | 服务健康检查 |
| /api/latency | GET | target | 延迟测试 |
| /api/speed | GET | 无 | 带宽测速 |
| /api/dns | GET | domain、dns | DNS 解析耗时 |
| /api/history | GET | type、limit | 查询历史记录 |
接口设计可以让你把网络质量数据接到自己的脚本或告警系统里,这也是最有复用价值的部分。
7.2 curl 调用
curl "http://127.0.0.1:8000/api/latency?target=1.2.4.8"1.2.4.8是 CNNIC 公共 DNS,可以在国内网络环境下使用。
7.3 Python 调用
写一个小脚本,批量测试多个目标:
import time import requests API_BASE = "http://127.0.0.1:8000" TARGETS = [ "223.5.5.5", "1.2.4.8", "你的校园网网关IP", ] def batch_latency(): for target in TARGETS: try: resp = requests.get( f"{API_BASE}/api/latency", params={"target": target}, timeout=10, ) print(target, resp.json()) except requests.RequestException as exc: print(target, "请求失败", exc) time.sleep(1) if __name__ == "__main__": batch_latency()批量测试时要控制并发和频率,避免给自己的网卡造成额外负载。
7.4 定时批量任务
定时任务可以使用 APScheduler,在app.py里启动后台调度器:
from apscheduler.schedulers.background import BackgroundScheduler def periodic_check(): rtt = ping("223.5.5.5", timeout=3) if rtt is not None: write_history("latency", "223.5.5.5", round(rtt * 1000, 2), "ms") scheduler = BackgroundScheduler() scheduler.add_job(periodic_check, "interval", minutes=30) scheduler.start()这样每 30 分钟记录一次延迟。保存后重启服务,持续跑一两天,就能从历史数据看到晚高峰延迟飙升的具体时间点。
8. 资源占用与性能观察
这个面板没有 GPU 推理,资源占用主要来自 Python 解释器、Flask 服务和 SQLite 写入。
用户排查时重点观察三个维度:
- CPU 占用:低负载运行时 CPU 占用很低,测速或批量任务执行时会有短时上升。观察方法:Windows 用任务管理器,Linux 用
top或htop。 - 内存占用:具体占用取决于依赖项数量,通常不会太高。确认方式是在任务管理器里观察
python进程的内存列。 - 磁盘占用:SQLite 数据库会随时间增长,每条历史记录包含时间戳、测试类型、目标、值等字段,运行几个月也只是百 MB 级别,不需要频繁清理。
测速接口是阻塞式的,如果同时有多个请求进来,后发的请求会被前面的测速阻塞。建议在测速接口前加一个单飞控制,避免并发测速把网络带宽打满。简单做法:
import threading speed_test_lock = threading.Lock() @app.route("/api/speed") def speed(): if not speed_test_lock.acquire(blocking=False): return jsonify({"error": "speed test already running"}), 429 try: # 测速逻辑 pass finally: speed_test_lock.release()端口冲突也是常见问题。如果服务启动时报Address already in use,先找到占用进程再杀掉,或者直接换端口启动。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| pip install 失败 | 网络不稳定或 Python 版本过低 | 查看报错信息 | 换国内镜像源,升级 Python |
| 启动后页面打不开 | 端口被占用或服务未启动 | 查看终端日志 | 换端口,重新启动 |
| 页面有文字没有样式 | 静态资源路径错误 | 浏览器 F12 看 Network 请求 | 使用 url_for 生成静态路径 |
| 延迟测试一直超时 | ICMP 被防火墙拦截 | 用其他主机 ping 目标 | 放行 ICMP 或改用 http 探测 |
| 测速接口卡死 | 网络节点不可达 | 看测速日志 | 换测速节点,增加超时 |
| SQLite 写入报错 | data 目录不存在 | 检查目录 | 创建 data 目录 |
| 局域网内手机无法访问 | 绑定地址不是 0.0.0.0 | 检查启动参数 | 切换到 0.0.0.0,配置防火墙 |
| 历史记录为空 | 还没触发时序接口 | 查看数据库表 | 先手动访问一次接口 |
依赖安装失败时,不要盲目升级所有包,把报错信息里的包名单独安装,通常是因为某个包版本和当前 Python 版本不兼容。
10. 最佳实践与使用建议
第一次部署时,先拿最小配置跑通流程,不要一上来就放四个测试指标。建议先做延迟测试,加一个目标,跑通后再扩展带宽、DNS 和定时任务。
数据管理上,输入素材、测试配置、SQLite 数据库分目录存放,config.yaml中统一管理测试目标、测速节点和定时任务间隔。这样后续调整参数时不需要改代码。
接口服务如果对局域网开放,要限制访问范围。最简单的方式是只绑定127.0.0.1,或者用防火墙放行指定 IP 段。不要把面板直接部署到公网服务器上,测速接口会被别人当成免费探针使用。
批量任务和定时检测要加日志。APScheduler 的周期任务如果出现异常,需要在回调里捕获:
def periodic_check(): try: rtt = ping("223.5.5.5", timeout=3) if rtt is not None: write_history("latency", "223.5.5.5", round(rtt * 1000, 2), "ms") except Exception: pass最后强调一下合规边界。排查校园网卡顿,重点是收集同一时段的多项指标,判断瓶颈在出口带宽、DNS 还是无线链路。让面板持续记录 24 到 48 小时,再对比高峰和非高峰的曲线,比临时测一两次更有价值。如果你们学校对校园网设备有明确的测试要求,先确认授权范围,未授权的主动探测不要做。这个面板只做被动测量和常规测速,不碰认证、计费和设备管理功能,这样用起来最安全。先让最小版本跑起来,把延迟历史记录下来,再决定要不要加更多功能。