mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 14:50:45 +00:00
实测基线:
ykxmz-setup-1.8.1.exe 只下到 237568/15067199 字节就停(17 KB/s)
ykxmz-watch-1.8.1.apk 201523 字节 / 66.8 秒 = 3 KB/s
ykxmz-server-1.8.1.zip 214900 字节 / 3.1 秒 = 67 KB/s
原因:relay 给所有响应都发 Cache-Control: no-store,
Cloudflare 完全不缓存 —— 每次下载/每张图都要穿隧道回 4G 源站。
改动:
/download/* no-store -> public, max-age=604800, immutable
文件名带版本号,同一名字内容永不改变,可以长缓存
/ -> public, max-age=60(挡住刷新重复回源)
/favicon.ico 已是 max-age=86400,保持不变
/shot/* 已是 max-age=86400,保持不变
/v1/* 保持 no-store(接口必须实时)
顺带修两个自己踩的坑:
1. _send() 无条件先发 no-store,再追加 extra 里的 Cache-Control,
结果是**两个 Cache-Control 头**,客户端只认第一个 ——
调用方传的缓存策略被吃掉。改成 extra 里有就不再发默认值。
2. do_HEAD 和 do_GET 的缓存头不一致:HEAD 对非 /shot/ 一律报 no-store。
下载器/代理用 HEAD 探测时会误判文件不可缓存。
现在 _head_info 给每个路由带上和 GET 完全相同的缓存头,
do_HEAD 也不再盲目覆盖。
结果(同一个测试口径):
setup.exe 17 KB/s(截断) -> 695 KB/s 完整 15 MB 41 倍
watch.apk 3 KB/s -> 101 KB/s 33 倍
server.zip 67 KB/s -> 87 KB/s
三个文件全部 cf-cache-status=HIT(不再穿隧道回源)
排查过程记一笔:我一开始用 curl -I 验证,
看到的全是 no-store —— 因为 **-I 发的是 HEAD**,
而 HEAD 走的是另一个分支。查了半天才发现是验证方法错了。
367 lines
12 KiB
Python
367 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""网络自检 —— 确认手表的数据包到底有没有到达这台电脑。
|
||
|
||
这个工具回答一个二选一的问题:
|
||
|
||
手表的连接请求,究竟「到了电脑但被拒绝」,还是「压根没到」?
|
||
|
||
用法:
|
||
由 ykxmz.exe --netcheck 调用(已打包进 EXE,不需要装 Python)
|
||
|
||
会监听 90 秒,可以按 Ctrl+C 提前结束。
|
||
|
||
编码说明:
|
||
脚本启动时会把控制台代码页设成 UTF-8(65001) 再输出,
|
||
所以不论系统默认代码页是什么,中文都不会乱码。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import socket
|
||
import subprocess
|
||
import sys
|
||
import threading
|
||
import time
|
||
from datetime import datetime
|
||
|
||
TCP_PORT = 8765
|
||
UDP_PORT = 8766
|
||
LISTEN_SECONDS = 90
|
||
|
||
_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||
|
||
# WSAEADDRINUSE / WSAEACCES
|
||
_ADDR_IN_USE = (10048, 10013)
|
||
|
||
|
||
def setup_console() -> None:
|
||
"""把控制台和标准输出都设成 UTF-8,避免中文乱码。"""
|
||
try:
|
||
import ctypes
|
||
ctypes.windll.kernel32.SetConsoleOutputCP(65001)
|
||
ctypes.windll.kernel32.SetConsoleCP(65001)
|
||
except Exception:
|
||
pass
|
||
for stream in (sys.stdout, sys.stderr):
|
||
try:
|
||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def line(text: str = "") -> None:
|
||
print(text, flush=True)
|
||
|
||
|
||
def title(text: str) -> None:
|
||
line()
|
||
line("=" * 62)
|
||
line(f" {text}")
|
||
line("=" * 62)
|
||
|
||
|
||
def stamp() -> str:
|
||
return datetime.now().strftime("%H:%M:%S")
|
||
|
||
|
||
def local_ips() -> list[tuple[str, str]]:
|
||
found: list[tuple[str, str]] = []
|
||
try:
|
||
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
probe.connect(("8.8.8.8", 80))
|
||
addr = probe.getsockname()[0]
|
||
probe.close()
|
||
if addr and not addr.startswith("127."):
|
||
found.append((addr, "默认路由"))
|
||
except OSError:
|
||
pass
|
||
try:
|
||
for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
|
||
addr = info[4][0]
|
||
if addr and not addr.startswith("127.") and addr not in [a for a, _ in found]:
|
||
found.append((addr, "主机名"))
|
||
except OSError:
|
||
pass
|
||
return found
|
||
|
||
|
||
def run(cmd: list[str], timeout: int = 15) -> str:
|
||
try:
|
||
proc = subprocess.run(cmd, capture_output=True, timeout=timeout,
|
||
creationflags=_NO_WINDOW,
|
||
encoding="mbcs", errors="replace")
|
||
return (proc.stdout or "") + (proc.stderr or "")
|
||
except Exception as exc: # noqa: BLE001
|
||
return f"({exc})"
|
||
|
||
|
||
def agent_running() -> bool:
|
||
"""被控端是不是正在运行(它会占着 8765/8766)。"""
|
||
text = run(["tasklist", "/FI", "IMAGENAME eq ykxmz.exe"])
|
||
return "ykxmz" in text
|
||
|
||
|
||
def stop_agent() -> bool:
|
||
"""结束被控端进程,好让自检能绑上端口。"""
|
||
run(["taskkill", "/IM", "ykxmz.exe", "/F"])
|
||
time.sleep(1.5)
|
||
return not agent_running()
|
||
|
||
|
||
def ask_stop_agent() -> bool:
|
||
"""询问是否关掉被控端。非交互环境直接返回 False。"""
|
||
if not sys.stdin.isatty():
|
||
return False
|
||
line()
|
||
line(" 被控端正在运行,占着 8765/8766,本自检绑不上端口。")
|
||
line(" 要抓包必须先让它让出端口。")
|
||
line()
|
||
try:
|
||
answer = input(" >>> 现在自动关掉被控端并继续抓包吗?(Y/N) ").strip().lower()
|
||
except Exception:
|
||
return False
|
||
if not answer.startswith("y"):
|
||
line(" 好,那本次不抓包。")
|
||
line(" (这不是故障 —— 被控端在跑就说明它能收到包;")
|
||
line(" 想抓包时先在它窗口点「退出」再重跑本脚本。)")
|
||
return False
|
||
if stop_agent():
|
||
line(" 已关闭被控端,继续抓包。")
|
||
line(" 抓完记得重新启动它。")
|
||
return True
|
||
line(" [警告] 关闭失败,可能是权限不够。请手动在它窗口点「退出」。")
|
||
return False
|
||
|
||
|
||
def show_network() -> str:
|
||
title("1. 本机网络")
|
||
ips = local_ips()
|
||
if ips:
|
||
for addr, tag in ips:
|
||
line(f" 本机 IP:{addr} [{tag}]")
|
||
line()
|
||
line(f" >>> 手表上就填这个 IP:{ips[0][0]} <<<")
|
||
else:
|
||
line(" 没能读到本机 IP(网络可能没连上)")
|
||
|
||
line()
|
||
wlan = run(["netsh", "wlan", "show", "interfaces"])
|
||
ssid = None
|
||
for raw in wlan.splitlines():
|
||
text = raw.strip()
|
||
if text.startswith("SSID") and "BSSID" not in text:
|
||
ssid = text.split(":", 1)[-1].strip()
|
||
if ssid:
|
||
line(f" 当前 WiFi:{ssid}")
|
||
line(" (手表必须连的是同一个 WiFi)")
|
||
else:
|
||
line(" 当前不是 WiFi 连接,或有线连接")
|
||
|
||
return ips[0][0] if ips else ""
|
||
|
||
|
||
def show_ports() -> None:
|
||
title("2. 端口占用情况")
|
||
tcp = run(["netstat", "-ano", "-p", "TCP"])
|
||
udp = run(["netstat", "-ano", "-p", "UDP"])
|
||
tcp_hit = [l.strip() for l in tcp.splitlines()
|
||
if f":{TCP_PORT}" in l and "LISTENING" in l.upper()]
|
||
udp_hit = [l.strip() for l in udp.splitlines() if f":{UDP_PORT}" in l]
|
||
|
||
if tcp_hit:
|
||
for item in tcp_hit:
|
||
line(f" TCP {TCP_PORT} 已被监听: {item}")
|
||
else:
|
||
line(f" TCP {TCP_PORT} 没有监听")
|
||
if udp_hit:
|
||
for item in udp_hit:
|
||
line(f" UDP {UDP_PORT} 已被监听: {item}")
|
||
else:
|
||
line(f" UDP {UDP_PORT} 没有监听")
|
||
|
||
line()
|
||
if agent_running():
|
||
line(" >>> 被控端(ykxmz.exe)正在运行。")
|
||
line(" 它会占着 8765/8766,本自检就绑不上这两个端口。")
|
||
line(" 想用自检抓包:先在它窗口上点「退出」,再重跑本脚本。")
|
||
line(" 其实不用自检也可以 —— 被控端在跑就说明它能收包。")
|
||
else:
|
||
line(" 被控端没有在运行。想连手表的话需要先把它启动。")
|
||
|
||
|
||
def show_firewall() -> None:
|
||
title("3. 防火墙放行状态")
|
||
for name in ("LAN Remote TCP", "LAN Remote UDP", "LAN Remote App TCP"):
|
||
out = run(["netsh", "advfirewall", "firewall", "show", "rule",
|
||
f"name={name}"])
|
||
if not out.strip() or "No rules match" in out or "没有与指定条件" in out:
|
||
line(f" {name}:未放行")
|
||
else:
|
||
enabled = action = ""
|
||
for raw in out.splitlines():
|
||
text = raw.strip()
|
||
if text.startswith("Enabled") or text.startswith("已启用"):
|
||
enabled = text.split(":", 1)[-1].strip()
|
||
elif text.startswith("Action") or text.startswith("操作"):
|
||
action = text.split(":", 1)[-1].strip()
|
||
line(f" {name}:已放行 Enabled={enabled or '?'} Action={action or '?'}")
|
||
|
||
|
||
# ----------------------------------------------------------------------
|
||
# 监听:这段才是关键
|
||
# ----------------------------------------------------------------------
|
||
|
||
received_tcp: list[str] = []
|
||
received_udp: list[str] = []
|
||
bind_errors: list[str] = []
|
||
|
||
|
||
def _bind(sock: socket.socket, port: int) -> bool:
|
||
"""独占绑定。Windows 上必须用 SO_EXCLUSIVEADDRUSE,
|
||
否则第二个进程也能绑上,抓包结果会失真。"""
|
||
if hasattr(socket, "SO_EXCLUSIVEADDRUSE"):
|
||
try:
|
||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
|
||
except OSError:
|
||
pass
|
||
else:
|
||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||
try:
|
||
sock.bind(("0.0.0.0", port))
|
||
return True
|
||
except OSError as exc:
|
||
reason = ""
|
||
if exc.errno in _ADDR_IN_USE or (exc.winerror or 0) in _ADDR_IN_USE:
|
||
reason = "端口被占用"
|
||
bind_errors.append(f"{port}: {reason or exc}")
|
||
return False
|
||
|
||
|
||
def tcp_listener(stop: threading.Event) -> None:
|
||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
if not _bind(server, TCP_PORT):
|
||
line(f" !! 无法监听 TCP {TCP_PORT}")
|
||
if agent_running():
|
||
line(" 原因:被控端正在运行,占着这个端口。")
|
||
line(" -> 先在它窗口上点「退出」,再重跑本自检。")
|
||
return
|
||
server.listen(8)
|
||
server.settimeout(0.5)
|
||
line(f" 正在监听 TCP {TCP_PORT} …")
|
||
while not stop.is_set():
|
||
try:
|
||
conn, addr = server.accept()
|
||
except socket.timeout:
|
||
continue
|
||
except OSError:
|
||
break
|
||
src = f"{addr[0]}:{addr[1]}"
|
||
received_tcp.append(src)
|
||
line(f" [{stamp()}] >>> 收到 TCP 连接:来自 {src} <<<")
|
||
try:
|
||
conn.sendall(b"LANREMOTE-SELFTEST\n")
|
||
except OSError:
|
||
pass
|
||
finally:
|
||
try:
|
||
conn.close()
|
||
except OSError:
|
||
pass
|
||
server.close()
|
||
|
||
|
||
def udp_listener(stop: threading.Event) -> None:
|
||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
if not _bind(sock, UDP_PORT):
|
||
line(f" !! 无法监听 UDP {UDP_PORT}")
|
||
if agent_running():
|
||
line(" 原因:被控端正在运行,占着这个端口。")
|
||
return
|
||
sock.settimeout(0.5)
|
||
line(f" 正在监听 UDP {UDP_PORT} …")
|
||
while not stop.is_set():
|
||
try:
|
||
data, addr = sock.recvfrom(2048)
|
||
except socket.timeout:
|
||
continue
|
||
except OSError:
|
||
break
|
||
src = f"{addr[0]}:{addr[1]}"
|
||
received_udp.append(src)
|
||
preview = data[:80].decode("utf-8", errors="replace")
|
||
line(f" [{stamp()}] >>> 收到 UDP 包:来自 {src} {preview} <<<")
|
||
sock.close()
|
||
|
||
|
||
def run_check(seconds: int = 90) -> int:
|
||
global LISTEN_SECONDS
|
||
LISTEN_SECONDS = int(seconds or 90)
|
||
setup_console()
|
||
|
||
title("局域网遥控 · 网络自检")
|
||
line(" 这个工具监听端口,看手表的请求有没有到达这台电脑。")
|
||
|
||
show_network()
|
||
show_ports()
|
||
show_firewall()
|
||
|
||
title(f"4. 开始监听({LISTEN_SECONDS} 秒)")
|
||
if agent_running():
|
||
ask_stop_agent()
|
||
line()
|
||
line(" 现在去手表上:打开 App →「设备诊断」→ 底部填上面的 IP → 点「测一下」")
|
||
line()
|
||
|
||
stop = threading.Event()
|
||
for target, args in ((tcp_listener, (stop,)), (udp_listener, (stop,))):
|
||
threading.Thread(target=target, args=args, daemon=True).start()
|
||
time.sleep(0.2)
|
||
|
||
deadline = time.time() + LISTEN_SECONDS
|
||
try:
|
||
while time.time() < deadline:
|
||
time.sleep(0.5)
|
||
left = int(deadline - time.time())
|
||
print(f"\r 剩余 {left:3d} 秒 … 已收到 TCP {len(received_tcp)} 个 / "
|
||
f"UDP {len(received_udp)} 个", end="", flush=True)
|
||
except KeyboardInterrupt:
|
||
pass
|
||
stop.set()
|
||
line()
|
||
line()
|
||
|
||
title("5. 结论")
|
||
if received_tcp or received_udp:
|
||
line(f" TCP 收到 {len(received_tcp)} 个连接,UDP 收到 {len(received_udp)} 个包。")
|
||
line()
|
||
line(" >>> 网络是通的!手表的数据包能到达这台电脑。")
|
||
line(" 既然包能到,连不上就只可能是:")
|
||
line(" * 被控端没在运行")
|
||
line(" * 或端口被别的程序占了")
|
||
elif bind_errors and agent_running():
|
||
line(" 没能抓包 —— 被控端正占着 8765/8766。")
|
||
line()
|
||
line(" >>> 这其实说明被控端是好的。要抓包请先退出它再重跑。")
|
||
else:
|
||
line(" TCP 和 UDP 都一个包都没收到。")
|
||
line()
|
||
line(" >>> 手表的数据包根本没到这台电脑。依次排查:")
|
||
line(" 1) 电脑是不是休眠/关机了")
|
||
line(" 2) 手表连的 WiFi 和电脑是不是同一个")
|
||
line(" 3) 路由器有没有开「AP 隔离 / 客户端隔离」")
|
||
line(" 4) 电脑上是不是有 VPN / 加速器在改路由表")
|
||
line()
|
||
if sys.stdin.isatty():
|
||
line(" 按回车退出…")
|
||
try:
|
||
input()
|
||
except Exception:
|
||
pass
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 直接 `python netcheck.py [秒数]` 也能用。
|
||
# 原来这里写的是 main(),但本文件没有 main() ——
|
||
# 入口是 run_check(),直接跑会 NameError。
|
||
sys.exit(run_check(int(sys.argv[1]) if len(sys.argv) > 1 else 90))
|