修「局域网连接被中转劫持」+ 长轮询 15s→60s

用户报两个问题:
  1. 局域网能搜到电脑,IP/端口/配对码都对,点连接**没有任何反应**
  2. 走中转 1426ms 延时,一会儿就 502

========== 一、局域网被劫持(真 bug)==========

MainActivity.connect() 里:

    boolean usedLan = !relayUrl().isEmpty() ? false : true;
    if (!relay.isEmpty()) {
        reply = client.connectViaRelay(...);      // 走中转
    } else {
        reply = client.connect(host, port, ...);  // 才走局域网
    }

**只看「中转设置里填没填地址」,填了就一律走中转,
完全无视用户填的局域网 IP 和端口。**

而中转单次超时是 40 秒 —— 用户点完就卡在「正在连接…」
四十秒,在他看来就是"没有任何反应"。

影响两条路:
  * 「局域网连接」页面填 IP 直连(connectLanTyped)
  * 首页「已配对的电脑」点一下重连(quickConnect)

修法:给 connect() 加 preferLan 参数。明确要走局域网时
**先直连(6 秒超时,局域网本该是毫秒级),失败再退回中转**,
保证最终能连上。

顺带两个体验改进:
  * 设备列表里,保存的 IP 和手表不同网段时标「⚠ 地址可能已过期」
    (用户截图里那个 192.168.2.64 就是过期地址,电脑早换成
      192.168.10.103 了)
  * quickConnect 判断出地址过期就跳过直连,直接走中转查最新地址 ——
    省掉那 6 秒白等

========== 二、长轮询 15 秒 -> 60 秒 ==========

实测隧道每次请求要多花 1~2 秒(CF 边缘路由 + 回源):

    本机直连 60 秒的 poll  ->  60.0s
    走公网同一个 poll      ->  61.5~62.0s

这个开销是**按次数**收的:
    15 秒一循环 -> 一天 5760 次/端,光路费 2.4~3.2 小时
    60 秒一循环 -> 一天 1440 次/端,光费 0.6~0.8 小时
而且请求越频繁,撞上丢包/重连窗口的概率越大。

**不会让命令变慢**:长轮询是"有数据立刻返回"(device.event 唤醒),
拉长只是减少空转重开。实测 60 秒长轮询穿隧道 4/4 通过;
Cloudflare 单请求上限 100 秒,留了 40 秒余量。

三层同步改(客户端超时必须显著大于服务端 hold):
  * relay/relay.py                    RELAY_POLL 默认 25 -> 60
  * windows/lanremote/relay.py        POLL_TIMEOUT 40 -> 90
  * android .../RelayTransport.java   POLL_TIMEOUT_MS 40000 -> 90000

========== 三、A/B 实测(还在跑更严谨的一轮)==========

短测发现基线 http2+4 虽然 100% 成功,但 **P90 是 16.8 秒**
(每 10 次有 1 次卡十几秒 —— 正是用户说的"没反应"),
而 quic+6 的 P90 只有 2.8 秒。正在用真实长轮询模式
跑 10 分钟对照确认。
This commit is contained in:
xmz7891
2026-10-07 14:11:33 +08:00
parent 94252ca156
commit b1119d0da6
16 changed files with 1455 additions and 8 deletions
+78
View File
@@ -0,0 +1,78 @@
#!/bin/bash
# 验证长轮询能不能穿过 Cloudflare 隧道跑满 60 秒。
#
# 为什么要先验:
# Cloudflare 对单个 HTTP 请求有 **100 秒**上限,超了返回 524。
# 我们打算把长轮询从 15 秒拉到 60 秒 ——
# 如果隧道/CF 那边撑不住,agent 会一直断线重连,反而更糟。
# 所以先拿真实的 poll 接口量一遍。
#
# 做法:
# 1. 注册一台临时设备,拿到 token
# 2. 用 token 打 /v1/agent/poll(公网地址),量它挂了多久
# 3. 对比本机直连和中转两条路
set -u
BASE=https://relay.xxmxxs.top
LOCAL=http://127.0.0.1:8080
UA='LanRemote/1.8.0'
echo "════════ 1. 注册一台临时设备 ════════"
DEV="probe-$(date +%s)"
REG=$(curl -s --max-time 30 -X POST "$BASE/v1/agent/register" \
-H 'Content-Type: application/json' \
-H "User-Agent: $UA" \
-d "{\"deviceId\":\"$DEV\",\"code\":\"999999\",\"name\":\"poll-probe\",\"version\":\"1.8.0\"}")
echo " $REG" | head -c 200
echo
TOKEN=$(echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
if [ -z "$TOKEN" ]; then
echo " [失败] 没拿到 token,后面的测试没法做"
exit 1
fi
echo " token 长度 ${#TOKEN}"
echo
echo "════════ 2. 本机直连 poll(基准,应等于服务端设定的秒数)════════"
for i in 1 2; do
out=$(curl -s -o /tmp/poll_local.bin -w '%{http_code} %{time_total}' \
--max-time 120 -X POST "$LOCAL/v1/agent/poll" \
-H "X-Token: $TOKEN" -H "User-Agent: $UA")
size=$(wc -c < /tmp/poll_local.bin 2>/dev/null || echo 0)
echo " 第 $i 次: 码/耗时 = $(echo "$out" | awk '{printf "%s %.1fs", $1, $2}') 返回 $size 字节"
done
echo
echo "════════ 3. 走公网(Cloudflare 隧道)poll ════════"
echo " 这是关键:能不能挂满 60 秒不断"
ok=0
for i in 1 2 3; do
out=$(curl -s -o /tmp/poll_pub.bin -w '%{http_code} %{time_total}' \
--max-time 130 -X POST "$BASE/v1/agent/poll" \
-H "X-Token: $TOKEN" -H "User-Agent: $UA")
code=$(echo "$out" | awk '{print $1}')
secs=$(echo "$out" | awk '{printf "%.1f", $2}')
size=$(wc -c < /tmp/poll_pub.bin 2>/dev/null || echo 0)
mark=""
[ "$code" = "200" ] && mark="✓"
[ "$code" = "524" ] && mark="**CF 超时了**"
printf " 第 %d 次: 码 %s 耗时 %ss 返回 %s 字节 %s\n" "$i" "$code" "$secs" "$size" "$mark"
[ "$code" = "200" ] && ok=$((ok + 1))
sleep 1
done
echo " 成功 $ok/3"
echo
echo "════════ 4. 服务端当前的长轮询设定 ════════"
grep -E '^REMOTE_POLL|Environment=RELAY_POLL' /etc/systemd/system/lanremote-relay.service 2>/dev/null | sed 's/^/ /'
echo " 实际生效: $(curl -s --max-time 10 $LOCAL/v1/health | python3 -c 'import sys,json; d=json.load(sys.stdin); print("(health 没报这个字段)")' 2>/dev/null)"
tail -40 /root/lanremote-relay/relay.log 2>/dev/null | grep '长轮询' | tail -1 | sed 's/^/ /'
echo
echo "════════ 5. 清掉临时设备 ════════"
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
curl -s --max-time 20 -X POST "$LOCAL/v1/admin/delete" \
-H "X-Admin: $KEY" -H 'Content-Type: application/json' \
-d "{\"deviceId\":\"$DEV\"}" | head -c 120
echo