不知道你有没有这种经历:客厅和卧室各放了一台游戏主机,人窝在沙发上想看另一台是不是已经把游戏更新完了,还是得光脚下地跑过去切一遍 HDMI。我一开始觉得这需求挺冷门,结果在几个折腾群里随口问了句,发现至少一半人想把多台主机的状态集中到一块屏幕上。
后来我花了两周多做了个小项目,名字就叫AnyPS5,核心只做三件事:局域网内发现任意一台目标主机、远程唤醒、状态监控(在线 / 待机 / 关机)。确实,官方 App 能实现部分功能,但要打开 App、等加载、甚至要走云端中转,对一个本地局域网来说太绕了。AnyPS5 的思路很简单:所有数据交换都发生在内网里,一台能跑 Python 的小设备就够了。
这篇东西我尽量写成可以直接照着复现的实战记录,涉及主机发现、WoW 唤醒包、轮询状态、告警联动几个模块,也把我在真实网络环境里踩过的大大小小的坑都列出来了。适合两类人看:一是家里有多台主机、想集中管理的玩家朋友,二是对局域网开发、网络探测感兴趣的开发朋友。
1. 为什么非要自己写一个 AnyPS5:需求拆解与方案取舍
聊技术之前先聊清楚动机。很多项目写着写着就烂尾,不是代码写不动,而是最开始没想清楚"到底要解决谁的问题"。AnyPS5 的出发点其实特别小:我想在沙发上就知道卧室那台主机是否待机、是否需要远程叫醒它去下载东西。
1.1 多主机玩家的真实痛点
先说我的场景。客厅一台主力机接投影,卧室一台接显示器。两台机器各自登录不同账号、装了不同游戏。平时最烦的就是这么几件事:
- 新游戏预载往往要晚上挂机下载,但人已经躺在床上了,不想再跑去客厅。
- 大型更新触发时,主机可能自动进入待机,导致下载中断,又没法及时发现。
- Steam 之类的平台倒是有手机端,但自家主机偏偏在局域网里反而不方便监控。
- 想确认它到底是"开机了"还是"待机了",传统 ping 只能告诉你网络通不通,区分不了这俩状态。
这些需求单看都不重,但组合起来就非常恶心人。官方工具不是不能做,而是体验不符合"本地自动化"这件事的直觉。
1.2 现成方案为什么不够用
做之前我专门列了个表,把能找到的方案都对比了一遍:
| 方案 | 能发现主机 | 能唤醒 | 能区分待机/开机 | 本地化 | 可二次开发 | 结论 |
|---|---|---|---|---|---|---|
| 官方 App | 部分支持 | 支持 | 有限 | 否 | 否 | 不折腾,但没法联动 |
| 某开源遥控工具 | 偶尔行 | 支持 | 兼容性看型号 | 是 | 是 | 停止维护,依赖老库 |
| 自写脚本套件 | 自己实现 | 自己实现 | 自己实现 | 是 | 完全可控 | 最终选择 |
有些第三方工具其实做得很细,但普遍存在两个致命伤:一是对网络环境要求苛刻,比如不支持跨网段、不支持动态 IP;二是我怕用到"非官方机制"的东西,稳定性和安全性都没保障。AnyPS5 从一开始就给自己划了条线:只使用主机在网络层面正常开放的能力,不碰任何系统内部数据。这样既安全,也经得起长期运行。
1.3 技术栈选型:我最终定了什么
最终定下来一套非常朴素的技术组合:
- Python 3.11:写网络探测、抓包、报文构造都顺手,生态里不缺现成的库。
- FastAPI + WebSocket:后端接口和实时状态推送。状态变化这种事用轮询也能做,但 WebSocket 明显更优雅,前端不用每秒刷接口。
- APScheduler:定时任务调度,负责周期性扫描和状态采集。
- SQLite:保存主机信息、历史状态。一台家庭环境撑死几十台设备,SQLite 完全够了,上 MySQL/PostgreSQL 反而属于过度设计。
- 前端只用单页 HTML + 原生 JS:不引构建工具,方便在 NAS 上直接托静态文件。
选择这套组合的核心原因是"可维护性"。Python 网络脚本写得舒服,FastAPI 起服务快,SQLite 备份就是拷个文件。后来证明,这个选择让整个项目在部署阶段省了非常多事。
2. 核心协议原理:局域网里如何发现并唤醒任意一台主机
AnyPS5 名字里的 "Any" 不是吹牛,它强调的是一套通用的局域网处理流程。无论目标主机是什么型号、什么系统版本,只要它在网络中暴露了基本生态能力,这套流程就能工作。
2.1 主机发现:UDP 广播与 mDNS
局域网里的设备发现,最常用的手段是 mDNS(多播 DNS)。它本质上是在局域网内发一条查询"谁是 XX 服务",然后等待设备应答。如果网络环境支持 mDNS,设备会返回自己的主机名、IP 和 MAC 地址。
我用 Python 做了一个简易版发现器,核心代码长这样:
import socket import time def discover_mdns(timeout=3): """ 发送 mDNS 查询,收集局域网内设备的应答。 这里不针对任何私有协议,只是通用设备发现。 """ query = b"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" \ b"\x09_services\x07_dns-sd\x04_udp\x05local\x00\x00\x0c\x00\x01" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(("", 5353)) sock.settimeout(timeout) sock.sendto(query, ("224.0.0.251", 5353)) results = [] try: while True: data, addr = sock.recvfrom(4096) results.append({"ip": addr[0], "raw": data}) except socket.timeout: pass finally: sock.close() return results这段代码并不负责"确认这台就是目标主机",它只负责把局域网里愿意回应的设备捞出来。真正区分哪些设备需要纳入管理,还需要结合端口探测:我通过扫描主机常见远程管理端口,把可唤醒、可管理的设备识别出来,存进数据库。
提示:mDNS 在家庭路由器上一般默认支持,但在企业级 AP 的隔离策略下经常失效。如果你发现在电脑上能发现,在容器里发现不了,多半是网络隔离问题,后面第 4 章会详细讲。
2.2 唤醒机制:Magic Packet 的构造与发送
局域网唤醒(WoL)的原理是向目标主机的网卡发送一个"魔术包"(Magic Packet)。这个包的结构很简单:先是 6 个字节的FF FF FF FF FF FF,紧接着是目标 MAC 地址重复 16 遍。网卡收到后校验无误,就会把主机从待机状态拉起来。
构造代码非常短:
import socket import struct def build_magic_packet(mac: str) -> bytes: mac_hex = mac.replace(":", "").replace("-", "") mac_bytes = bytes.fromhex(mac_hex) # 6 个 FF + MAC 重复 16 遍 payload = b"\xff" * 6 + mac_bytes * 16 return payload def send_wol(mac: str, broadcast_ip: str = "255.255.255.255", port: int = 9): packet = build_magic_packet(mac) with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) s.sendto(packet, (broadcast_ip, port))这里得注意一个关键点:Magic Packet 既可以用子网广播地址发送,也可以直接发给目标 IP。但如果你用目标 IP 单播发送,必须确保目标主机的网卡正处于可接收状态,很多情况下并不稳定。所以我实际发的时候,优先通过当前网段的广播地址发。
比如设备 IP 是192.168.1.200、掩码是255.255.255.0,那我就发到192.168.1.255。跨 VLAN 场景下,广播域不同,这条路径会失效,最简单的办法是让 AnyPS5 部署在与目标主机同一个二层网络里。
2.3 状态识别:在线、待机与关机的判断逻辑
状态识别是 AnyPS5 里最容易做糙的部分。很多人一上来就用"ping 通 = 开机",结果把待机和关机混为一谈。
我的做法是组合探测,三个维度交叉判断:
| 探测维度 | 在线开机 | 待机 | 关机 |
|---|---|---|---|
| ICMP Ping | 通 | 不通 | 不通 |
| 远程管理端口 TCP 探测 | 通 | 可能通或不通 | 不通 |
| 主机名解析/设备指纹 | 能应答 | 不一定 | 不一定 |
区别在于:真正开机的设备,TCP 端口通常积极响应;待机状态虽然网卡还通着,但系统服务没起来,远程管理端口不会响应。我把探测结果汇总后放进一个状态机,只有连续两次结果一致才更新最终状态,避免瞬间抖动导致误报。
def judge_status(ping_ok: bool, tcp_ok: bool) -> str: if ping_ok and tcp_ok: return "online" if not ping_ok and not tcp_ok: return "off" # ping 不通但系统没有完全下线,归为待机 return "standby"这套逻辑不完美,但实测足够准。它的核心思想其实是用多证据源的共识去对抗单一信号的噪声,这也是所有可靠状态监控的基本功。
3. 工程实现细节:代码结构、关键模块与联动逻辑
光有协议原理还不够,要做成一个能长期跑的服务,还得考虑代码结构、数据存储、通知联动这些东西。这一章我讲讲 AnyPS5 的工程化细节。
3.1 项目目录与数据模型
最终的项目结构非常简单,没有强行分层:
anyps5/ ├── app.py # FastAPI 入口 ├── discovery.py # 设备发现模块 ├── wake.py # WoL 唤醒模块 ├── monitor.py # 状态轮询与调度 ├── notify.py # WebHook / 邮件通知 ├── static/ │ └── index.html # 控制台单页 └── anyps5.db # SQLite 数据库数据模型上,我只建了一张主机表,存这些字段:
hostname:设备名,比如living-room-ps5ip:当前 IP 地址mac:MAC 地址,唤醒时使用status:当前状态,online/standby/offlast_seen:最后探测到的时间notify_enabled:是否允许发送状态变更通知
这个模型从第 1 天用到现在,没有加过一张多余的表。个人项目的经验就是:能用字段解决的,不要急着上表。
3.2 发现模块代码拆解
发现模块要完成的动作是:定时扫一遍局域网,把新设备加进来,同时更新老设备的 IP。
import ipaddress import socket def scan_subnet(subnet: str): network = ipaddress.ip_network(subnet, strict=False) alive = [] for ip in network.hosts(): try: sock = socket.create_connection((str(ip), 9295), timeout=0.3) alive.append(str(ip)) sock.close() except (socket.timeout, OSError): pass return alive这里扫描端口我用了一个比较"宽松"的端口号,实际场景中你可以改成你目标设备真正开启的端口。扫描范围别设太大,家庭网络一般是/24,也就 254 个地址,单线程扫大概十几秒,完全能接受。
注意:别用
ping 整段的方式去发现设备,很多设备会屏蔽 ICMP。TCP 端口探测更可靠,但前提是目标设备确实监听了某个外部端口。
3.3 状态采集与任务调度
状态采集用 APScheduler 起一个后台任务,默认每 30 秒跑一次。每次执行时遍历数据库里的所有主机,分别做 ping 和端口探测,再通过状态机更新最终状态。
from apscheduler.schedulers.background import BackgroundScheduler scheduler = BackgroundScheduler() scheduler.add_job( status_recon_all, "interval", seconds=30, id="status_recon", max_instances=1, coalesce=True, )这里两个参数值得说下:max_instances=1是为了防止上次任务还没跑完、下次又开始;coalesce=True的意思是如果一堆任务堆积了,只执行一次。这两个参数直接保护了后续 WebSocket 推送逻辑,不然状态变更事件会连环爆发。
状态一旦变化,服务端会通过 WebSocket 推送最新结果到浏览器控制台,同时按需触发通知模块。
3.4 告警通知与后续联动
通知模块我留了一个特别简单的扩展口子:往 Webhook 地址发一个 JSON POST。这样不管是接到企业微信、钉钉、Telegram Bot 还是自己写的脚本,都只要加一个配置项。
import requests def notify_change(host, old_status, new_status): payload = { "event": "status_changed", "hostname": host["hostname"], "old_status": old_status, "new_status": new_status, "time": datetime.now().isoformat(), } requests.post(WEBHOOK_URL, json=payload, timeout=5)我实际用的时候,把 Webhook 接到了家里的智能家居网关。主机状态变为online时,自动触发"影音模式":关灯、拉窗帘。也就是从"监控主机"顺带变成了"场景联动",这也是本地网络数据的价值所在。
4. 踩坑记录:局域网自动化里最容易翻车的四个地方
这部分是含金量最高的实操经验。我在从开发到稳定运行的三个月里,把局域网自动化的常见坑基本都撞了一遍。下面每一个都是完整排查思路,不是简单给个结论。
4.1 访客网络隔离导致"搜不到主机"
现象特别诡异:AnyPS5 跑在 NAS 上,手机连同一 Wi-Fi 能看到大部分设备,但发现列表里始终没有那台目标主机。
排查链路:先用手机上的网络调试工具手动 ping 目标主机 IP,能通;再尝试 TCP 端口探测,发现不通。此时基本确定是端口被拦了。之后登录路由器后台,发现手机连接的是"访客网络",而访客网络和主网络之间默认开启了"AP 隔离",连广播都隔离了。
最终解决方案是让 AnyPS5 所在设备改用有线连接,并接在主路由器下。所以部署前先确认:AnyPS5 和主机必须在同一个二层网络,否则发现和唤醒都会失败。
4.2 轮询频率太快造成"伪待机"误判
我最初把轮询间隔设成了 5 秒,结果状态经常在online和standby之间疯狂跳变。原因特简单:主机刚进入待机时,网卡和系统部分服务还没完全释放端口,导致 TCP 探测时而成功时而失败。
我一开始以为是网络波动,后来打印了连续 20 次探测日志才发现时间规律:每次跳变都发生在待机后的前 1 分钟。这个阶段主机正在做"软关机",端口响应不稳定,属于正常现象。
解决方法是引入连续确认机制:只有当连续 3 次探测结果一致时,才允许状态从online切到standby;反向同理。同时把轮询间隔从 5 秒调到 30 秒,彻底解决状态抖动。
4.3 WoL 包发出去了但主机没反应
更头疼的是唤醒:工具明明打印了"已发送 Magic Packet",主机就是没反应。
排查链路分四步:
- 先确认目标主机是否开启了"网络唤醒"。有些主机的待机模式是"S3 深度睡眠",网卡根本没供电,需要进入系统设置开启网络唤醒。
- 再确认 Magic Packet 的发送地址。如果发给单播 IP 而不是广播地址,部分网卡会忽略它。
- 接着检查交换机的"未知单播/广播抑制"策略。有些家用交换机默认过滤了未知广播,导致 Magic Packet 到不了目标端口。
- 最后看目标主机的"节能以太网"设置,在电脑上需要在网卡高级设置里关闭"节能以太网",否则低功耗状态下网卡会丢弃唤醒包。
我这边的最终方案:AnyPS5 发送时同时向255.255.255.255和子网广播地址各发一遍,并在目标主机上关闭节能以太网。之后唤醒成功率从 60% 提到了接近 100%。
4.4 mDNS 放在容器里会失效
我把 AnyPS5 部署到 Docker 后,设备发现功能直接失效。排查发现,mDNS 依赖 224.0.0.251 这个组播地址,而 Docker 默认 bridge 网络不会透传组播流量。
解决办法有两个:
- 最简单:用
network_mode: "host",让容器直接共享宿主机网络栈。 - 或者:配置 Docker 的 MACVLAN 网络,让容器拥有独立的局域网 IP。
家庭场景我推荐前者,少折腾,而且 AnyPS5 本身就是轻量服务,不需要容器网络隔离带来的那点好处。
提示:如果你的 NAS 支持 Docker-compose,直接写
network_mode: "host"即可;如果在路由器上跑,建议用 systemd 直接挂进程,别套容器层。
5. 部署上线与三个月的实测结果
工具写出来不是终点,能长期稳定跑才是。这章讲讲部署方式和真实运行数据。
5.1 部署到 NAS:Docker 与 systemd 两种方式
如果你有 NAS,推荐用 Docker Compose 部署:
version: "3" services: anyps5: image: anyps5:latest network_mode: "host" restart: unless-stopped volumes: - ./data:/app/data environment: - SCAN_INTERVAL=30 - WEBHOOK_URL=http://home-gateway/api/notify如果你更习惯直接挂进程,写一个 systemd service 也很简单:
[Unit] Description=AnyPS5 Monitor After=network.target [Service] Type=simple WorkingDirectory=/opt/anyps5 ExecStart=/usr/bin/python3 /opt/anyps5/app.py Restart=always RestartSec=10 [Install] WantedBy=multi-user.target我的建议是:家庭环境优先用 systemd。原因很实在——少了 Docker 这层抽象,排障时直接看进程日志,网络问题也不会多一层"容器里看不到主机"的干扰。
5.2 唤醒成功率、延迟与资源占用实测
跑了三个月,我把关键指标统计出来:
| 指标 | 实测值 | 说明 |
|---|---|---|
| 状态识别准确率 | 97% | 剩余 3% 基本发生在待机切换的模糊窗口 |
| 唤醒成功率 | 98.6% | 关闭节能以太网后明显提升 |
| 唤醒到可进入系统时间 | 8~15 秒 | 这是硬件启动时间,服务端只负责发包 |
| 轮询耗时 | 约 300ms / 台 | 30 秒一轮,两台主机几乎不占资源 |
| 内存占用 | 110MB 左右 | Python 进程 + FastAPI |
这个结果比我预期好不少。早期状态频繁抖动的时候,准确率可能只有 80%,加了连续确认机制后才稳定下来。所以如果你也要做类似监控,前三周一定要留日志,用真实数据校正阈值,而不是拍脑袋定参数。
5.3 在此基础上还能扩展什么
AnyPS5 目前的状态监控只是第一步。基于这套局域网探测框架,我后面计划加几个功能:
- 定时唤醒:比如每周末早上自动叫醒主机,触发系统更新和游戏预载。
- 截图/录像自动收集:主机通常提供局域网内访问多媒体内容的能力,可以定期把截图拖回 NAS 统一归档。
- 状态历史图表:SQLite 里已经把历史状态存下来了,画个 24 小时温度计式的状态条,很直观。
- 一键切换账号场景:把"客厅玩 / 卧室玩"的配置改成主题包,每次唤醒后自动切到对应场景。这个要看主机侧能给多少权限,但思路没问题。
说实话,这些功能单拎出来都不算复杂,难的是把"网络发现、状态判断、可靠唤醒"这三个底座做稳。底座稳了,上层想加什么都是应用层的事。
最后多说一句:用 AnyPS5 这类工具前,记得先去目标主机设置里把网络唤醒、允许远程管理这些选项打开。它只是个调度器,没法变出主机没开放的能力。我在这个项目里最大的体会是:局域网自动化不难,难的是信任数据——日志永远比直觉可靠。你如果也在做类似的东西,把日志打全,跑两周再谈优化,很多问题根本不用猜。