diff --git a/_build/ab_v4.sh b/_build/ab_v4.sh new file mode 100644 index 0000000..db6d743 --- /dev/null +++ b/_build/ab_v4.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# 1) 把 MTU 1400 固化下来(重启也在) +# 2) 在 MTU 1400 下对照 quic+6 / quic+8 / quic+10 +# +# 为什么要固化 MTU: +# QUIC 走 UDP,**我们那条 --set-mss 1400 的 iptables 规则只管 TCP**。 +# 实测把 wlan0 MTU 从 1500 降到 1400 之后: +# QUIC 建齐 6 条连接的时间 28 秒 -> 13 秒 +# dial 失败次数 2 -> 1 +# 原因:UDP 丢包不会回 ICMP,QUIC 发了超出路径 MTU 的包只能等超时。 +# 把接口 MTU 压到 1400,QUIC 的包就都在安全范围内。 +# +# 为什么还要试更多连接: +# 已经看到 http2+8 建齐只要 12 秒且 0 失败(QUIC+6 是 28 秒/2 次失败)。 +# 连接多 = 单条掉线影响小。但 QUIC 的连接建立更贵,得实测平衡点。 +set -u + +BASE=https://relay.xxmxxs.top +UA='LanRemote/1.8.2' +SVC=/etc/systemd/system/lanremote-tunnel.service +NETTUNE=/etc/systemd/system/lanremote-nettune.service +BACKUP=/root/tunnel.bak.v4.$(date +%s) +cp -a "$SVC" "$BACKUP" +N=60 +MTU=1400 +MSS=1360 + +echo "════════ 1. 固化 MTU(写进 net-tune 服务)════════" +echo " 现在的 net-tune 服务:" +systemctl cat lanremote-nettune 2>/dev/null | grep -E 'ExecStart' | sed 's/^/ /' + +# 把 MTU 设置追加进去(幂等:已有的先删掉) +cp -a "$NETTUNE" "${NETTUNE}.bak.$(date +%s)" 2>/dev/null || true +python3 - <<'PYEOF' +from pathlib import Path +p = Path("/etc/systemd/system/lanremote-nettune.service") +if not p.is_file(): + print(" ! net-tune 服务不存在,跳过") + raise SystemExit(0) +text = p.read_text(encoding="utf-8") +# 去掉旧的 MTU/MSS 行 +lines = [l for l in text.splitlines() + if "ip link set wlan0 mtu" not in l and "--set-mss" not in l] +out = [] +for line in lines: + out.append(line) + # 在每个 ExecStart 之后补上 MTU 设置 + if line.strip().startswith("ExecStart="): + out.append("ExecStart=/sbin/ip link set wlan0 mtu 1400") + out.append("ExecStart=/sbin/iptables -t mangle -C OUTPUT -p tcp " + "--tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360 || " + "/sbin/iptables -t mangle -A OUTPUT -p tcp " + "--tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360") +p.write_text("\n".join(out) + "\n", encoding="utf-8") +print(" 已写入 MTU 1400 + MSS 1360") +PYEOF + +systemctl daemon-reload +echo " 立即执行一次:" +ip link set wlan0 mtu $MTU && echo " wlan0 MTU -> $(cat /sys/class/net/wlan0/mtu)" +iptables -t mangle -C OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss $MSS 2>/dev/null \ + || iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss $MSS +echo " MSS 规则: $(iptables -t mangle -S OUTPUT | grep TCPMSS | head -1)" +echo " net-tune 现在的内容:" +systemctl cat lanremote-nettune 2>/dev/null | grep ExecStart | sed 's/^/ /' +echo " 服务状态: $(systemctl is-enabled lanremote-nettune 2>/dev/null) / $(systemctl is-active lanremote-nettune 2>/dev/null)" + +wait_conns() { + local want="$1" limit="$2" t0 t1 n + t0=$(date +%s) + while true; do + n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') + n=${n:-0} + t1=$(date +%s) + [ "$n" -ge "$want" ] || [ $((t1 - t0)) -ge "$limit" ] && { echo "$n $((t1 - t0))"; return; } + sleep 3 + done +} + +run_group() { + local conns="$1" + echo "════════ quic + $conns (MTU $MTU)════════" + cp -a "$BACKUP" "$SVC" + sed -i "s/--protocol [a-z0-9]*/--protocol quic/" "$SVC" + sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC" + systemctl daemon-reload + local mark; mark=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0) + systemctl restart lanremote-tunnel + local res; res=$(wait_conns "$conns" 150) + local n s + n=$(echo "$res" | awk '{print $1}'); s=$(echo "$res" | awk '{print $2}') + local df; df=$(tail -n +$((mark+1)) /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'Failed to dial' || echo 0) + echo " 建齐 $n/$conns,用 ${s} 秒,dial 失败 ${df} 次" + + local ok=0 times=() + for i in $(seq 1 "$N"); do + out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 -H "User-Agent: $UA" "$BASE/v1/health") + c=$(echo "$out" | awk '{print $1}'); ms=$(echo "$out" | awk '{printf "%d", $2*1000}') + [ "$c" = "200" ] && { ok=$((ok+1)); times+=("$ms"); } + done + local pct=$((ok * 100 / N)) + local stats="无数据" + [ "${#times[@]}" -gt 0 ] && stats=$(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END { + printf "中位 %d P90 %d P99 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[int(NR*0.99)+1], a[NR]}') + printf " 可用率 %d%% (%d/%d) %s ms\n" "$pct" "$ok" "$N" "$stats" + echo "quic+$conns|$n|$s|$df|$pct|$stats" >> /tmp/v4.txt + echo +} + +rm -f /tmp/v4.txt +run_group 6 +run_group 8 +run_group 10 + +echo "════════ 汇总 ════════" +printf " %-10s %-14s %-10s %-8s %s\n" "配置" "建齐/耗时" "dial失败" "可用率" "延迟" +while IFS='|' read -r label n s df pct stats; do + printf " %-10s %-14s %-10s %-8s %s\n" "$label" "${n}条 ${s}s" "$df" "${pct}%" "$stats" +done < /tmp/v4.txt + +echo +BESTN=$(sort -t'|' -k5 -rn /tmp/v4.txt | head -1 | sed 's/quic+//' | cut -d'|' -f1) +echo " → 采用 quic + $BESTN" +cp -a "$BACKUP" "$SVC" +sed -i "s/--protocol [a-z0-9]*/--protocol quic/" "$SVC" +sed -i "s/--ha-connections [0-9]*/--ha-connections $BESTN/" "$SVC" +systemctl daemon-reload +systemctl restart lanremote-tunnel +sleep 5 +echo " 最终参数: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ')" +echo " MTU: $(cat /sys/class/net/wlan0/mtu)" +echo " 备份: $BACKUP" diff --git a/_build/add_fail_threshold.py b/_build/add_fail_threshold.py new file mode 100644 index 0000000..d0e51c3 --- /dev/null +++ b/_build/add_fail_threshold.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +r'''单次请求失败不再立刻判"离线",改成连续失败 3 次才算。 + +== 为什么 == + +实测单个请求会有 2~4% 的偶发失败(4G 丢包打在一条隧道连接上, +或者那条连接正在重连)。原来的逻辑是**一失败就 online = false**, +于是: + + 手表/电脑的界面上会闪一下"已断开" + 用户点按钮时正好撞上那一闪 -> 看起来像"又连不上了" + +而实际上链路 1.5~15 秒后就自己恢复了(客户端本来就有退避重试)。 + +改成"连续失败 3 次才判离线",效果是: + * 单次抖动完全不影响界面显示 + * 真的断了(连续 3 次,至少 4.5 秒都没通)仍然会如实显示离线 + +这不是掩盖问题,而是把"是否可用"的判断从"一次采样"改成 +"连续采样"—— 单点采样在有丢包的链路上噪声太大。 + +== 两边都要改 == + + 1. android/src/com/lanremote/watch/RelayTransport.java (手表轮询) + 2. windows/lanremote/relay.py (电脑轮询) +''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +BOM = b"\xef\xbb\xbf" + +# ---------------------------------------------------------------- 手表 +WATCH = ROOT / "android/src/com/lanremote/watch/RelayTransport.java" + +WATCH_CONST_OLD = " private static final int RETRY_MAX_MS = 15000;" +WATCH_CONST_NEW = """ private static final int RETRY_MAX_MS = 15000; + /** + * 连续失败几次才判定"离线"。 + * + *

单次失败就报离线太敏感:4G 链路上随手就有 2~4% 的偶发失败, + * 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。 + * 改成连续 3 次(至少 4.5 秒都不通)才判离线 —— + * 真的断了仍然会如实显示。 + */ + private static final int OFFLINE_AFTER = 3;""" + +WATCH_FIELD_OLD = " private Thread pollThread;" +WATCH_FIELD_NEW = """ private Thread pollThread; + /** 连续失败次数(成功一次就清零)。 */ + private volatile int failStreak;""" + +# 401/403 分支 +W_401_OLD = """ if (result.code == 401 || result.code == 403) { + // 会话过期(中转重启等)—— 这种可以自动重连 + online = false; + lastError = "会话已失效,正在重新接入";""" +W_401_NEW = """ if (result.code == 401 || result.code == 403) { + // 会话过期(中转重启等)—— 这种可以自动重连。 + // 会话失效是明确的状态,直接标离线。 + online = false; + lastError = "会话已失效,正在重新接入";""" + +# 非 200 分支 +W_OTHER_OLD = """ if (result.code != 200) { + online = false; + lastError = "轮询返回 " + result.code; + sleep(backoff); + backoff = Math.min(RETRY_MAX_MS, backoff * 2); + continue; + } + online = true; + lastError = ""; + backoff = RETRY_MIN_MS;""" +W_OTHER_NEW = """ if (result.code != 200) { + // 偶发的 502/504 不算断线,连续几次才算 + failStreak++; + if (failStreak >= OFFLINE_AFTER) { + online = false; + } + lastError = "轮询返回 " + result.code; + sleep(backoff); + backoff = Math.min(RETRY_MAX_MS, backoff * 2); + continue; + } + online = true; + failStreak = 0; + lastError = ""; + backoff = RETRY_MIN_MS;""" + +# 异常分支 +W_EXC_OLD = """ } catch (Exception exc) { + if (closed) { + return; + } + online = false; + lastError = "轮询出错:" + exc.getClass().getSimpleName(); + sleep(backoff); + backoff = Math.min(RETRY_MAX_MS, backoff * 2); + }""" +W_EXC_NEW = """ } catch (Exception exc) { + if (closed) { + return; + } + // 连接超时/重置很常见(隧道在重连),连续几次才算真断 + failStreak++; + if (failStreak >= OFFLINE_AFTER) { + online = false; + } + lastError = "轮询出错:" + exc.getClass().getSimpleName(); + sleep(backoff); + backoff = Math.min(RETRY_MAX_MS, backoff * 2); + }""" + +# ---------------------------------------------------------------- 电脑 +AGENT = ROOT / "windows/lanremote/relay.py" + +A_CONST_OLD = "RETRY_MAX = 20.0" +A_CONST_NEW = """RETRY_MAX = 20.0 +# 连续失败几次才判定"离线"。 +# 单次失败就报离线太敏感:4G 链路上有 2~4% 的偶发失败, +# 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。 +# 连续 3 次(至少 6 秒都不通)才判离线,真的断了仍然如实显示。 +OFFLINE_AFTER = 3""" + +A_FAIL_OLD = """ except Exception as exc: # noqa: BLE001 + self.online = False + self.last_error = f"{type(exc).__name__}: {exc}" + self._log(f"中转连接出问题:{self.last_error}")""" +A_FAIL_NEW = """ except Exception as exc: # noqa: BLE001 + # 偶发的连接超时/重置不算断线,连续几次才算 + self._fail_streak = getattr(self, "_fail_streak", 0) + 1 + if self._fail_streak >= OFFLINE_AFTER: + self.online = False + self.last_error = f"{type(exc).__name__}: {exc}" + self._log(f"中转连接出问题(第 {self._fail_streak} 次):" + f"{self.last_error}")""" + +A_OK_OLD = """ self.online = True + self.last_ok = time.time() + self.last_error = \"\"""" +A_OK_NEW = """ self.online = True + self._fail_streak = 0 + self.last_ok = time.time() + self.last_error = \"\"""" + + +def patch(text: str, old: str, new: str, label: str) -> tuple: + if old not in text: + if new.strip() and new.splitlines()[-1].strip() in text: + print(" • %s 已经是新值" % label) + return text, True + print(" ✗ 没找到:%s" % label) + return text, False + if text.count(old) != 1: + print(" ! 出现 %d 次:%s" % (text.count(old), label)) + return text, False + print(" ✓ %s" % label) + return text.replace(old, new, 1), True + + +def main() -> int: + ok_all = True + + print(" 手表 RelayTransport.java:") + raw = WATCH.read_bytes() + text = raw.decode("utf-8-sig") + for old, new, label in ( + (WATCH_CONST_OLD, WATCH_CONST_NEW, "加 OFFLINE_AFTER 常量"), + (WATCH_FIELD_OLD, WATCH_FIELD_NEW, "加 failStreak 字段"), + (W_OTHER_OLD, W_OTHER_NEW, "非 200 用连续计数"), + (W_EXC_OLD, W_EXC_NEW, "异常用连续计数"), + ): + text, ok = patch(text, old, new, label) + ok_all = ok_all and ok + + if ok_all: + WATCH.write_text(text, encoding="utf-8", newline="\n") + + print() + print(" 电脑 relay.py:") + araw = AGENT.read_bytes() + atext = araw.decode("utf-8-sig") + for old, new, label in ( + (A_CONST_OLD, A_CONST_NEW, "加 OFFLINE_AFTER 常量"), + (A_FAIL_OLD, A_FAIL_NEW, "异常用连续计数"), + (A_OK_OLD, A_OK_NEW, "成功时清零"), + ): + atext, ok = patch(atext, old, new, label) + ok_all = ok_all and ok + + if not ok_all: + print() + print(" 有失败项,没写回电脑端") + return 1 + + AGENT.write_text(atext, encoding="utf-8", newline="\n") + print() + print(" 两边都改好了") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/apply_best.sh b/_build/apply_best.sh new file mode 100644 index 0000000..b03876c --- /dev/null +++ b/_build/apply_best.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# 采用最优配置 quic + 10,并清理 MSS 规则。 +# +# v4 实测(MTU 1400,各 60 次): +# quic+6 建齐 10s 0 失败 100% 中位 1399 P90 2611 P99 13705 +# quic+8 建齐 16s 1 失败 100% 中位 1280 P90 2279 P99 3856 +# quic+10 建齐 13s 0 失败 100% 中位 1256 P90 2068 P99 2991 <- 最好 +# +# 注意:v4 脚本自己的排序逻辑有多行输出没处理干净,误选了 quic+8, +# 这里按数据手工改成 quic+10。 +# +# MSS 说明:接口 MTU 已经是 1400,内核自己算出来的 MSS 就是 1360; +# iptables 的 --set-mss 只能把 MSS 往小改,所以那条 1400 的规则 +# 现在实际是空转。统一改成 1360,避免以后 MTU 变回去时留下不一致。 +set -u + +SVC=/etc/systemd/system/lanremote-tunnel.service +UA='LanRemote/1.8.2' + +echo "════════ 1. 采用 quic + 10 ════════" +sed -i "s/--protocol [a-z0-9]*/--protocol quic/" "$SVC" +sed -i "s/--ha-connections [0-9]*/--ha-connections 10/" "$SVC" +grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+|--edge-ip-version [0-9]+|--retries [0-9]+' "$SVC" | sed 's/^/ /' + +echo +echo "════════ 2. 清理 MSS 规则(只留一条 1360)════════" +iptables -t mangle -S OUTPUT 2>/dev/null | grep TCPMSS | sed 's/^/ 清理前: /' +while iptables -t mangle -D OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1400 2>/dev/null; do :; done +while iptables -t mangle -D OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360 2>/dev/null; do :; done +iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360 +iptables -t mangle -S OUTPUT 2>/dev/null | grep TCPMSS | sed 's/^/ 清理后: /' + +echo +echo "════════ 3. MTU 和持久化 ════════" +echo " wlan0 MTU 现在: $(cat /sys/class/net/wlan0/mtu)" +grep -c 'mtu 1400' /etc/systemd/system/lanremote-nettune.service | sed 's/^/ net-tune 里的 MTU 设置行数: /' +echo " net-tune 服务: $(systemctl is-enabled lanremote-nettune) / $(systemctl is-active lanremote-nettune)" + +echo +echo "════════ 4. 重启隧道并等连接建齐 ════════" +systemctl daemon-reload +systemctl restart lanremote-tunnel +for i in $(seq 1 40); do + sleep 4 + n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') + [ "${n:-0}" -ge 10 ] && break +done +echo " 建齐 $n/10 条(等了约 $((i*4)) 秒)" +curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep 'server_locations.*} 1' | sed 's/^/ /' + +echo +echo "════════ 5. 等公网可用 ════════" +for i in $(seq 1 20); do + c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 -H "User-Agent: $UA" https://relay.xxmxxs.top/v1/health) + printf " 第 %d 次: %s\n" "$i" "$c" + [ "$c" = "200" ] && break + sleep 3 +done + +echo +echo "════════ 6. 快速验收(30 次)════════" +ok=0; times=() +for i in $(seq 1 30); do + out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 -H "User-Agent: $UA" https://relay.xxmxxs.top/v1/health) + c=$(echo "$out" | awk '{print $1}'); ms=$(echo "$out" | awk '{printf "%d", $2*1000}') + [ "$c" = "200" ] && { ok=$((ok+1)); times+=("$ms"); } +done +stats=$(printf '%s\n' "${times[@]:-0}" | sort -n | awk '{a[NR]=$1} END {printf "中位 %d P90 %d P99 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[int(NR*0.99)+1], a[NR]}') +echo " 可用率 $ok/30 = $((ok*100/30))% $stats ms" +echo +echo " 最终: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ') MTU=$(cat /sys/class/net/wlan0/mtu)" diff --git a/_build/bench_from_pc.py b/_build/bench_from_pc.py new file mode 100644 index 0000000..f0afba3 --- /dev/null +++ b/_build/bench_from_pc.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +r'''从这台 Windows 机器测中转 —— 这才是"真实用户"的视角。 + +== 为什么要换测量端 == + +之前所有测量都是在设备自己上跑的 curl。那等于: + 设备 4G --> Cloudflare 边缘 (第一段) + 边缘 --> 隧道 --> 设备 4G 源站 (第二段) +**两段都压在同一个 4G 链路上**,量出来的数字是双重打击。 + +真实用户(手机/电脑在 WiFi、宽带、5G 上): + 用户 --> Cloudflare 边缘 (第一段,好网络) + 边缘 --> 隧道 --> 设备 4G 源站 (第二段,只有这段是 4G) + +所以从这台机器量出来的,才是用户实际会遇到的延迟和可用率。 + +用法: python _build/bench_from_pc.py [标签] +''' +from __future__ import annotations + +import json +import statistics +import sys +import time +import urllib.error +import urllib.request +from pathlib import Path + +BASE = "https://relay.xxmxxs.top" +UA = "LanRemote/1.8.2" +OUT_DIR = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\_build") + + +def req(path: str, method: str = "GET", data: bytes | None = None, + headers: dict | None = None, timeout: float = 150.0): + """发一个请求,返回 (状态码, 字节数, 秒数, 响应头)。""" + hdrs = {"User-Agent": UA} + hdrs.update(headers or {}) + r = urllib.request.Request(BASE + path, data=data, headers=hdrs, + method=method) + t0 = time.perf_counter() + try: + with urllib.request.urlopen(r, timeout=timeout) as resp: + body = resp.read() + return resp.status, len(body), time.perf_counter() - t0, resp.headers + except urllib.error.HTTPError as exc: + try: + exc.read() + except Exception: # noqa: BLE001 + pass + return exc.code, 0, time.perf_counter() - t0, exc.headers + except Exception: # noqa: BLE001 + return 0, 0, time.perf_counter() - t0, {} + + +def pct(values: list, q: float) -> int: + if not values: + return 0 + s = sorted(values) + idx = min(int(len(s) * q), len(s) - 1) + return int(s[idx]) + + +def main() -> int: + label = sys.argv[1] if len(sys.argv) > 1 else "pc" + lines: list[str] = [] + + def say(text: str = "") -> None: + print(text) + lines.append(text) + + say("=" * 62) + say(" 从 PC 量中转 · %s · %s" % (label, time.strftime("%Y-%m-%d %H:%M:%S"))) + say("=" * 62) + + # ---------------- A. 公网可用率 ---------------- + say() + say("── A. 公网可用率(100 次 /v1/health)──") + ok, times, fails = 0, [], 0 + for i in range(100): + code, _n, secs, _h = req("/v1/health", timeout=25) + if code == 200: + ok += 1 + times.append(int(secs * 1000)) + else: + fails += 1 + time.sleep(0.3) + say(" 可用率 %d/100 = %d%% 失败 %d 次" % (ok, ok, fails)) + if times: + say(" 耗时 最小 %d 中位 %d P90 %d P99 %d 最大 %d ms" + % (min(times), int(statistics.median(times)), pct(times, 0.9), + pct(times, 0.99), max(times))) + + # ---------------- E. 静态资源 ---------------- + say() + say("── E. 静态资源(各 10 次)──") + for path in ("/", "/shot/pc-main.png", "/favicon.ico"): + o, ts = 0, [] + for _ in range(10): + code, _n, secs, _h = req(path, timeout=25) + if code == 200: + o += 1 + ts.append(int(secs * 1000)) + time.sleep(0.2) + med = int(statistics.median(ts)) if ts else 0 + say(" %-22s %2d/10 中位 %d 最大 %d ms" + % (path, o, med, max(ts) if ts else 0)) + + # ---------------- C. 下载 ---------------- + say() + say("── C. 三个成品下载(完整下载)──") + dl_results = {} + for name in ("ykxmz-setup-1.8.2.exe", "ykxmz-server-1.8.2.zip", + "ykxmz-watch-1.8.2.apk"): + code, size, secs, hdrs = req("/download/" + name, timeout=300) + kbps = (size / secs / 1024) if secs > 0 else 0 + cf = (hdrs.get("cf-cache-status") or "?") if hdrs else "?" + say(" %-30s HTTP %-4s %9d 字节 %6.1fs %7.0f KB/s cf=%s" + % (name, code, size, secs, kbps, cf)) + if code == 200 and size > 0: + dl_results[name] = kbps + + # ---------------- B. 长轮询 ---------------- + say() + say("── B. 长轮询(6 次真实 poll,每次挂 60 秒)──") + stamp = int(time.time()) + dev = "pcbench-%s-%d" % (label, stamp) + code6 = "%06d" % (stamp % 1000000) + payload = json.dumps({ + "deviceId": dev, "code": code6, "name": "pcbench", + "version": "1.8.2", + }, ensure_ascii=False).encode("utf-8") + status, _n, _s, _h = req("/v1/agent/register", method="POST", + data=payload, timeout=30, + headers={"Content-Type": "application/json"}) + token = "" + if status == 200: + # 重新取一次拿 body + r = urllib.request.Request( + BASE + "/v1/agent/register", data=payload, + headers={"User-Agent": UA, "Content-Type": "application/json"}, + method="POST") + try: + with urllib.request.urlopen(r, timeout=30) as resp: + token = json.loads(resp.read().decode("utf-8")).get("token", "") + except Exception: # noqa: BLE001 + pass + say(" 探针注册: %s" % ("成功" if token else "失败")) + + if token: + okb, btimes, bfails = 0, [], [] + for i in range(6): + code, _n, secs, _h = req( + "/v1/agent/poll", method="POST", data=b"", + headers={"X-Token": token}, timeout=150) + ms = int(secs * 1000) + if code == 200: + okb += 1 + btimes.append(ms) + say(" %d: 200 %6dms" % (i + 1, ms)) + else: + bfails.append("%d:%s" % (i + 1, code)) + say(" %d: %-4s **失败**" % (i + 1, code)) + say(" 长轮询成功 %d/6 = %d%% 失败: %s" + % (okb, okb * 100 // 6, " ".join(bfails) or "无")) + if btimes: + say(" 耗时 最小 %d 中位 %d P90 %d 最大 %d ms(理想 ≈ 61000)" + % (min(btimes), int(statistics.median(btimes)), + pct(btimes, 0.9), max(btimes))) + + say() + say("=" * 62) + out = OUT_DIR / ("bench_pc_%s.txt" % label) + out.write_text("\n".join(lines), encoding="utf-8") + say(" 存档: %s" % out) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/bench_pc_optimized.txt b/_build/bench_pc_optimized.txt new file mode 100644 index 0000000..2c87333 --- /dev/null +++ b/_build/bench_pc_optimized.txt @@ -0,0 +1,30 @@ +============================================================== + 从 PC 量中转 · optimized · 2026-10-07 16:01:23 +============================================================== + +── A. 公网可用率(100 次 /v1/health)── + 可用率 98/100 = 98% 失败 2 次 + 耗时 最小 765 中位 911 P90 1889 P99 23868 最大 23868 ms + +── E. 静态资源(各 10 次)── + / 9/10 中位 1016 最大 1223 ms + /shot/pc-main.png 10/10 中位 1160 最大 1914 ms + /favicon.ico 8/10 中位 1075 最大 1993 ms + +── C. 三个成品下载(完整下载)── + ykxmz-setup-1.8.2.exe HTTP 404 0 字节 0.8s 0 KB/s cf=BYPASS + ykxmz-server-1.8.2.zip HTTP 404 0 字节 0.8s 0 KB/s cf=BYPASS + ykxmz-watch-1.8.2.apk HTTP 404 0 字节 0.9s 0 KB/s cf=BYPASS + +── B. 长轮询(6 次真实 poll,每次挂 60 秒)── + 探针注册: 成功 + 1: 200 60817ms + 2: 0 **失败** + 3: 200 61758ms + 4: 200 62027ms + 5: 200 62060ms + 6: 200 61247ms + 长轮询成功 5/6 = 83% 失败: 2:0 + 耗时 最小 60817 中位 61758 P90 62060 最大 62060 ms(理想 ≈ 61000) + +============================================================== \ No newline at end of file diff --git a/_tests/out_app/com/lanremote/watch/RelayTransport$1.class b/_tests/out_app/com/lanremote/watch/RelayTransport$1.class index 28c7808..b3e981d 100644 Binary files a/_tests/out_app/com/lanremote/watch/RelayTransport$1.class and b/_tests/out_app/com/lanremote/watch/RelayTransport$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/RelayTransport$2.class b/_tests/out_app/com/lanremote/watch/RelayTransport$2.class index c6a5704..7004679 100644 Binary files a/_tests/out_app/com/lanremote/watch/RelayTransport$2.class and b/_tests/out_app/com/lanremote/watch/RelayTransport$2.class differ diff --git a/_tests/out_app/com/lanremote/watch/RelayTransport$3.class b/_tests/out_app/com/lanremote/watch/RelayTransport$3.class index ae46af6..526c9c4 100644 Binary files a/_tests/out_app/com/lanremote/watch/RelayTransport$3.class and b/_tests/out_app/com/lanremote/watch/RelayTransport$3.class differ diff --git a/_tests/out_app/com/lanremote/watch/RelayTransport$4.class b/_tests/out_app/com/lanremote/watch/RelayTransport$4.class index 3dc329e..8471ba0 100644 Binary files a/_tests/out_app/com/lanremote/watch/RelayTransport$4.class and b/_tests/out_app/com/lanremote/watch/RelayTransport$4.class differ diff --git a/_tests/out_app/com/lanremote/watch/RelayTransport$HttpResult.class b/_tests/out_app/com/lanremote/watch/RelayTransport$HttpResult.class index 214d537..dafcc5a 100644 Binary files a/_tests/out_app/com/lanremote/watch/RelayTransport$HttpResult.class and b/_tests/out_app/com/lanremote/watch/RelayTransport$HttpResult.class differ diff --git a/_tests/out_app/com/lanremote/watch/RelayTransport.class b/_tests/out_app/com/lanremote/watch/RelayTransport.class index 8da9d94..31dbae6 100644 Binary files a/_tests/out_app/com/lanremote/watch/RelayTransport.class and b/_tests/out_app/com/lanremote/watch/RelayTransport.class differ diff --git a/_tests/shots/1-屏幕中间.png b/_tests/shots/1-屏幕中间.png index b5a055c..2a74794 100644 Binary files a/_tests/shots/1-屏幕中间.png and b/_tests/shots/1-屏幕中间.png differ diff --git a/android/src/com/lanremote/watch/RelayTransport.java b/android/src/com/lanremote/watch/RelayTransport.java index 0bbcdbc..b759c48 100644 --- a/android/src/com/lanremote/watch/RelayTransport.java +++ b/android/src/com/lanremote/watch/RelayTransport.java @@ -53,6 +53,15 @@ public class RelayTransport implements Transport { /** 网络出错后的退避 */ private static final int RETRY_MIN_MS = 1500; private static final int RETRY_MAX_MS = 15000; + /** + * 连续失败几次才判定"离线"。 + * + *

单次失败就报离线太敏感:4G 链路上随手就有 2~4% 的偶发失败, + * 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。 + * 改成连续 3 次(至少 4.5 秒都不通)才判离线 —— + * 真的断了仍然会如实显示。 + */ + private static final int OFFLINE_AFTER = 3; /** 单次读到的字节上限 */ private static final int MAX_BODY = 8 * 1024 * 1024; @@ -79,6 +88,8 @@ public class RelayTransport implements Transport { private final Object pendingLock = new Object(); private Thread pollThread; + /** 连续失败次数(成功一次就清零)。 */ + private volatile int failStreak; private Thread sendThread; private final Object closingLock = new Object(); @@ -344,13 +355,18 @@ public class RelayTransport implements Transport { continue; } if (result.code != 200) { - online = false; + // 偶发的 502/504 不算断线,连续几次才算 + failStreak++; + if (failStreak >= OFFLINE_AFTER) { + online = false; + } lastError = "轮询返回 " + result.code; sleep(backoff); backoff = Math.min(RETRY_MAX_MS, backoff * 2); continue; } online = true; + failStreak = 0; lastError = ""; backoff = RETRY_MIN_MS; if (result.body.length > 0) { @@ -360,7 +376,11 @@ public class RelayTransport implements Transport { if (closed) { return; } - online = false; + // 连接超时/重置很常见(隧道在重连),连续几次才算真断 + failStreak++; + if (failStreak >= OFFLINE_AFTER) { + online = false; + } lastError = "轮询出错:" + exc.getClass().getSimpleName(); sleep(backoff); backoff = Math.min(RETRY_MAX_MS, backoff * 2); diff --git a/windows/lanremote/relay.py b/windows/lanremote/relay.py index bf18419..04731ea 100644 --- a/windows/lanremote/relay.py +++ b/windows/lanremote/relay.py @@ -43,6 +43,11 @@ MAX_BODY = 8 * 1024 * 1024 # 网络断了之后的退避 RETRY_MIN = 2.0 RETRY_MAX = 20.0 +# 连续失败几次才判定"离线"。 +# 单次失败就报离线太敏感:4G 链路上有 2~4% 的偶发失败, +# 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。 +# 连续 3 次(至少 6 秒都不通)才判离线,真的断了仍然如实显示。 +OFFLINE_AFTER = 3 # 多久没成功通信就算掉线(用于界面上显示状态) STALE_SECONDS = 90.0 @@ -521,9 +526,13 @@ class RelayLink: break continue except Exception as exc: # noqa: BLE001 - self.online = False + # 偶发的连接超时/重置不算断线,连续几次才算 + self._fail_streak = getattr(self, "_fail_streak", 0) + 1 + if self._fail_streak >= OFFLINE_AFTER: + self.online = False self.last_error = f"{type(exc).__name__}: {exc}" - self._log(f"中转连接出问题:{self.last_error}") + self._log(f"中转连接出问题(第 {self._fail_streak} 次):" + f"{self.last_error}") # 网络断了:把当前会话关掉,让上层重开 self._drop_socket() self.token = "" @@ -611,6 +620,7 @@ class RelayLink: raise self.online = True + self._fail_streak = 0 self.last_ok = time.time() self.last_error = ""