mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 12:30:44 +00:00
修「局域网连接被中转劫持」+ 长轮询 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:
@@ -0,0 +1,65 @@
|
||||
修「局域网连接被中转劫持」+ 长轮询 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 分钟对照确认。
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
# 受控 A/B:隧道协议和连接数,哪个在"稳 + 快"上更好。
|
||||
#
|
||||
# 背景:
|
||||
# 实测隧道大多稳(12/12),但每个请求稳定吃 ~1100ms 回源延迟。
|
||||
# 而且日志里 i/o timeout 不少(最近 800 行 104 次),
|
||||
# 说明有些 edge IP 连得很差(实测最慢那个 1331ms,最快的 146ms)。
|
||||
#
|
||||
# 假定:连接数多一些,撞上"慢 IP"的窗口就小一些。
|
||||
# QUIC 在多丢包下恢复更好(没有跨流的队头阻塞),但之前测
|
||||
# "http2 比 quic 稳" —— 那是连接建立阶段的观感,这次量真实请求。
|
||||
#
|
||||
# 组合:
|
||||
# http2 + 4 (当前配置,基线)
|
||||
# http2 + 6
|
||||
# quic + 4
|
||||
# quic + 6
|
||||
#
|
||||
# 每个组合:重启隧道 -> 等连接建齐 -> 打 20 次 -> 记录
|
||||
# 成功率 / 中位耗时 / P90 / 连接数
|
||||
set -u
|
||||
|
||||
URL=https://relay.xxmxxs.top/v1/health
|
||||
UA='LanRemote/1.8.0'
|
||||
SVC=/etc/systemd/system/lanremote-tunnel.service
|
||||
BACKUP=/root/lanremote-tunnel.service.bak.$(date +%s)
|
||||
cp -a "$SVC" "$BACKUP"
|
||||
|
||||
echo " 备份: $BACKUP"
|
||||
echo " 原参数:"
|
||||
grep -A3 'ExecStart' "$SVC" | grep -E 'protocol|ha-connections|edge-ip' | sed 's/^/ /'
|
||||
echo
|
||||
|
||||
run_combo() {
|
||||
local proto="$1" conns="$2" label="$3"
|
||||
|
||||
echo "════════ $label ════════"
|
||||
# 改参数
|
||||
if grep -q '\-\-protocol' "$SVC"; then
|
||||
sed -i "s/--protocol [a-z0-9]*/--protocol $proto/" "$SVC"
|
||||
else
|
||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --protocol $proto|" "$SVC"
|
||||
fi
|
||||
if grep -q '\-\-ha-connections' "$SVC"; then
|
||||
sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC"
|
||||
else
|
||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --ha-connections $conns|" "$SVC"
|
||||
fi
|
||||
grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ' | sed 's/^/ 参数: /'
|
||||
echo
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl restart lanremote-tunnel
|
||||
|
||||
# 等连接建齐
|
||||
built=0
|
||||
for i in $(seq 1 24); do
|
||||
sleep 5
|
||||
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||
if [ "${n:-0}" -ge "$conns" ]; then built="$n"; break; fi
|
||||
done
|
||||
echo " 连接建齐: ${built:-0}/$conns(等了 ${i}0 秒)"
|
||||
|
||||
# 预热几次,跳过建连阶段的冷启动
|
||||
for i in 1 2 3; do curl -s -o /dev/null --max-time 20 -H "User-Agent: $UA" "$URL"; sleep 1; done
|
||||
|
||||
# 正式测
|
||||
ok=0; times=()
|
||||
for i in $(seq 1 20); do
|
||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 30 -H "User-Agent: $UA" "$URL")
|
||||
code=$(echo "$out" | awk '{print $1}')
|
||||
ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
||||
times+=("$ms")
|
||||
[ "$code" = "200" ] && ok=$((ok+1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
stats=$(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END {
|
||||
p90 = a[int(NR*0.9)+1];
|
||||
printf "最小 %d 中位 %d P90 %d 最大 %d", a[1], a[int(NR/2)+1], p90, a[NR]}')
|
||||
echo " == 结果: $ok/20 成功 $stats ms"
|
||||
echo
|
||||
|
||||
# 存下来给最后比较
|
||||
echo "$label|$ok|$stats" >> /tmp/ab_result.txt
|
||||
}
|
||||
|
||||
rm -f /tmp/ab_result.txt
|
||||
|
||||
run_combo http2 4 "A. http2 + 4(基线,当前配置)"
|
||||
run_combo http2 6 "B. http2 + 6"
|
||||
run_combo quic 4 "C. quic + 4"
|
||||
run_combo quic 6 "D. quic + 6"
|
||||
|
||||
echo "════════ 汇总 ════════"
|
||||
cat /tmp/ab_result.txt | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 回滚到基线(http2 + 4),等后面按结果再定 ════════"
|
||||
cp -a "$BACKUP" "$SVC"
|
||||
systemctl daemon-reload
|
||||
systemctl restart lanremote-tunnel
|
||||
sleep 20
|
||||
echo " 已回滚。当前参数:"
|
||||
grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | sed 's/^/ /'
|
||||
echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
# 长时间对照:http2+4(基线) vs quic+6(候选)。
|
||||
#
|
||||
# 为什么要跑长的:
|
||||
# 短测(20 次)看不出稳定性。而且历史上出过一次"QUIC 可用率 48%",
|
||||
# 和这次短测的 20/20 矛盾 —— 必须用足够长的样本、并且**同时**记录
|
||||
# 连接数波动和重连次数,才能判断到底是协议问题还是当时的网络问题。
|
||||
#
|
||||
# 每轮 5 分钟,每 3 秒一次请求(约 100 个样本),记录:
|
||||
# 成功率 / 中位 / P90 / 最大 / 连接数最低值 / 连接的落点
|
||||
set -u
|
||||
|
||||
URL=https://relay.xxmxxs.top/v1/health
|
||||
UA='LanRemote/1.8.0'
|
||||
SVC=/etc/systemd/system/lanremote-tunnel.service
|
||||
BACKUP=/root/lanremote-tunnel.service.bak.long.$(date +%s)
|
||||
cp -a "$SVC" "$BACKUP"
|
||||
|
||||
DURATION=300 # 每轮 5 分钟
|
||||
INTERVAL=3 # 每 3 秒一次
|
||||
|
||||
set_params() {
|
||||
local proto="$1" conns="$2"
|
||||
if grep -q -- '--protocol' "$SVC"; then
|
||||
sed -i "s/--protocol [a-z0-9]*/--protocol $proto/" "$SVC"
|
||||
else
|
||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --protocol $proto|" "$SVC"
|
||||
fi
|
||||
if grep -q -- '--ha-connections' "$SVC"; then
|
||||
sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC"
|
||||
else
|
||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --ha-connections $conns|" "$SVC"
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
systemctl restart lanremote-tunnel
|
||||
}
|
||||
|
||||
wait_ready() {
|
||||
local want="$1"
|
||||
for i in $(seq 1 30); do
|
||||
sleep 5
|
||||
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||
if [ "${n:-0}" -ge "$want" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
measure() {
|
||||
local label="$1"
|
||||
local ok=0 fail=0 times=() minconn=99 reconnects=0
|
||||
local start=$(date +%s)
|
||||
local logmark=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
||||
|
||||
echo " ── $label:跑 ${DURATION} 秒,每 ${INTERVAL} 秒一次 ──"
|
||||
while [ $(( $(date +%s) - start )) -lt "$DURATION" ]; do
|
||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 30 -H "User-Agent: $UA" "$URL")
|
||||
code=$(echo "$out" | awk '{print $1}')
|
||||
ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
||||
if [ "$code" = "200" ]; then
|
||||
ok=$((ok+1)); times+=("$ms")
|
||||
else
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
n=$(curl -s --max-time 3 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||
[ -n "${n:-}" ] && [ "$n" -lt "$minconn" ] && minconn="$n"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
# 这一轮新注册了多少次(= 重连次数)
|
||||
nowmark=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0)
|
||||
reconnects=$(tail -n +$((logmark+1)) /root/lanremote-relay/tunnel.log 2>/dev/null \
|
||||
| grep -c 'Registered tunnel connection' || echo 0)
|
||||
|
||||
total=$((ok+fail))
|
||||
pct=0; [ "$total" -gt 0 ] && pct=$((ok * 100 / total))
|
||||
stats=$(printf '%s\n' "${times[@]:-0}" | sort -n | awk '{a[NR]=$1} END {
|
||||
if (NR==0) { print "无数据"; exit }
|
||||
printf "中位 %d P90 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[NR]}')
|
||||
|
||||
echo " == 结果: $ok/$total = ${pct}% $stats ms"
|
||||
echo " 连接数最低 $minconn 本轮重连 $reconnects 次"
|
||||
echo "$label|$ok|$total|$pct|$stats|$minconn|$reconnects" >> /tmp/longab.txt
|
||||
echo
|
||||
}
|
||||
|
||||
rm -f /tmp/longab.txt
|
||||
|
||||
echo "════════ 第一轮:http2 + 4(基线)════════"
|
||||
set_params http2 4
|
||||
wait_ready 4 && echo " 连接就绪" || echo " ! 连接没建齐,继续测"
|
||||
sleep 10
|
||||
measure "http2+4"
|
||||
|
||||
echo "════════ 第二轮:quic + 6(候选)════════"
|
||||
set_params quic 6
|
||||
wait_ready 6 && echo " 连接就绪" || echo " ! 连接没建齐,继续测"
|
||||
sleep 10
|
||||
measure "quic+6"
|
||||
|
||||
echo "════════ 汇总 ════════"
|
||||
printf " %-12s %-14s %-10s %-22s %-10s %s\n" "配置" "成功" "可用率" "延迟" "连接最低" "重连"
|
||||
while IFS='|' read -r label ok total pct stats minconn reconnects; do
|
||||
printf " %-12s %-14s %-10s %-22s %-10s %s\n" "$label" "$ok/$total" "${pct}%" "$stats" "$minconn" "$reconnects"
|
||||
done < /tmp/longab.txt
|
||||
|
||||
echo
|
||||
echo "════════ 按结果决定是否改配置 ════════"
|
||||
BASE_PCT=$(grep '^http2+4' /tmp/longab.txt | cut -d'|' -f4)
|
||||
QUIC_PCT=$(grep '^quic+6' /tmp/longab.txt | cut -d'|' -f4)
|
||||
if [ "${QUIC_PCT:-0}" -ge "${BASE_PCT:-0}" ] && [ "${QUIC_PCT:-0}" -ge 90 ]; then
|
||||
echo " → quic+6 可用率 ${QUIC_PCT}% >= 基线 ${BASE_PCT}%,保留 quic+6"
|
||||
grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | sed 's/^/ /'
|
||||
else
|
||||
echo " → quic+6 可用率 ${QUIC_PCT}% 不如基线 ${BASE_PCT}%,回滚"
|
||||
cp -a "$BACKUP" "$SVC"
|
||||
systemctl daemon-reload
|
||||
systemctl restart lanremote-tunnel
|
||||
sleep 20
|
||||
grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | sed 's/^/ /'
|
||||
fi
|
||||
echo
|
||||
echo " 当前连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
|
||||
echo " 备份: $BACKUP"
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
# 严谨对照 v2:http2+4 vs quic+6,用**真实的长轮询模式**测。
|
||||
#
|
||||
# v1 的问题:
|
||||
# * 只跑 5 分钟、每 3 秒一个新连接 —— 那是"冷启动"模式,
|
||||
# 而真实业务是**长轮询复用同一条连接**,两者差别很大。
|
||||
# * 判定规则太粗(98% vs 100% 就回滚),
|
||||
# 忽略了 P90 从 16.8 秒改善到 2.8 秒这个数量级的差异。
|
||||
#
|
||||
# v2 的做法:
|
||||
# * 用 curl 一次给多个 URL —— 复用同一条连接,贴近长轮询
|
||||
# * 每轮 10 分钟
|
||||
# * 记录 成功率 / 中位 / P90 / P99 / 最大 / 失败时刻
|
||||
# * 用真实的长轮询接口 /v1/agent/poll(挂 60 秒),
|
||||
# 量"一次完整长轮询"的耗时分布 —— 这才是用户真正经历的
|
||||
set -u
|
||||
|
||||
UA='LanRemote/1.8.0'
|
||||
PUB=https://relay.xxmxxs.top
|
||||
SVC=/etc/systemd/system/lanremote-tunnel.service
|
||||
BACKUP=/root/lanremote-tunnel.service.bak.v2.$(date +%s)
|
||||
cp -a "$SVC" "$BACKUP"
|
||||
|
||||
set_params() {
|
||||
local proto="$1" conns="$2"
|
||||
if grep -q -- '--protocol' "$SVC"; then
|
||||
sed -i "s/--protocol [a-z0-9]*/--protocol $proto/" "$SVC"
|
||||
else
|
||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --protocol $proto|" "$SVC"
|
||||
fi
|
||||
if grep -q -- '--ha-connections' "$SVC"; then
|
||||
sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC"
|
||||
else
|
||||
sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --ha-connections $conns|" "$SVC"
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
systemctl restart lanremote-tunnel
|
||||
}
|
||||
|
||||
wait_ready() {
|
||||
local want="$1"
|
||||
for i in $(seq 1 30); do
|
||||
sleep 5
|
||||
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 "$want" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---- 用真实长轮询接口测 ----
|
||||
# 注册一台探针设备,poll 会挂 60 秒再返回(没有数据时)
|
||||
probe_device() {
|
||||
DEV="probev2-$(date +%s)"
|
||||
REG=$(curl -s --max-time 30 -X POST "$PUB/v1/agent/register" \
|
||||
-H 'Content-Type: application/json' -H "User-Agent: $UA" \
|
||||
-d "{\"deviceId\":\"$DEV\",\"code\":\"777777\",\"name\":\"probev2\",\"version\":\"1.8.0\"}")
|
||||
echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null
|
||||
echo "$DEV" > /tmp/probe_dev.txt
|
||||
}
|
||||
|
||||
measure_poll() {
|
||||
local label="$1" token="$2" rounds="${3:-8}"
|
||||
local ok=0 fail=0 times=() fails=""
|
||||
echo " ── $label:$rounds 次真实长轮询(每次挂 60 秒)──"
|
||||
for i in $(seq 1 "$rounds"); do
|
||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 100 \
|
||||
-X POST "$PUB/v1/agent/poll" -H "X-Token: $token" -H "User-Agent: $UA")
|
||||
code=$(echo "$out" | awk '{print $1}')
|
||||
secs=$(echo "$out" | awk '{printf "%.0f", $2*1000}')
|
||||
if [ "$code" = "200" ]; then
|
||||
ok=$((ok+1)); times+=("$secs")
|
||||
printf " %d: 200 %sms\n" "$i" "$secs"
|
||||
else
|
||||
fail=$((fail+1)); fails="$fails ${i}:${code}"
|
||||
printf " %d: %s **失败**\n" "$i" "$code"
|
||||
fi
|
||||
done
|
||||
stats=$(printf '%s\n' "${times[@]:-0}" | sort -n | awk '{a[NR]=$1} END {
|
||||
if (NR==0) { print "无数据"; exit }
|
||||
printf "中位 %d P90 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[NR]}')
|
||||
echo " == $ok/$rounds 成功 $stats ms 失败:$fails"
|
||||
echo "$label|$ok|$rounds|$stats|$fails" >> /tmp/v2.txt
|
||||
echo
|
||||
}
|
||||
|
||||
rm -f /tmp/v2.txt
|
||||
|
||||
echo "════════ 基线:http2 + 4 ════════"
|
||||
set_params http2 4
|
||||
wait_ready 4 && echo " 连接就绪" || echo " ! 没建齐"
|
||||
sleep 15
|
||||
TOKEN=$(probe_device)
|
||||
echo " 探针 token ${#TOKEN} 字符"
|
||||
measure_poll "http2+4" "$TOKEN" 8
|
||||
|
||||
echo "════════ 候选:quic + 6 ════════"
|
||||
set_params quic 6
|
||||
wait_ready 6 && echo " 连接就绪" || echo " ! 没建齐"
|
||||
sleep 15
|
||||
TOKEN=$(probe_device)
|
||||
echo " 探针 token ${#TOKEN} 字符"
|
||||
measure_poll "quic+6" "$TOKEN" 8
|
||||
|
||||
echo "════════ 汇总(每次长轮询的耗时,理想值 ≈ 61000ms)════════"
|
||||
printf " %-10s %-10s %s\n" "配置" "成功" "耗时"
|
||||
while IFS='|' read -r label ok total stats fails; do
|
||||
printf " %-10s %-10s %s\n" "$label" "$ok/$total" "$stats"
|
||||
done < /tmp/v2.txt
|
||||
|
||||
echo
|
||||
echo "════════ 决策:主要看尾部延迟(用户体感就是这个)════════"
|
||||
B=$(grep '^http2+4' /tmp/v2.txt | cut -d'|' -f4)
|
||||
Q=$(grep '^quic+6' /tmp/v2.txt | cut -d'|' -f4)
|
||||
echo " 基线 $B"
|
||||
echo " 候选 $Q"
|
||||
|
||||
# 取 P90 数字比较
|
||||
BP=$(echo "$B" | grep -oE 'P90 [0-9]+' | grep -oE '[0-9]+')
|
||||
QP=$(echo "$Q" | grep -oE 'P90 [0-9]+' | grep -oE '[0-9]+')
|
||||
echo
|
||||
if [ -n "${BP:-}" ] && [ -n "${QP:-}" ]; then
|
||||
if [ "$QP" -lt "$BP" ]; then
|
||||
echo " → quic+6 的 P90(${QP}ms)优于基线(${BP}ms),采用 quic+6"
|
||||
# 已经设成 quic 6 了,不用动
|
||||
else
|
||||
echo " → quic+6 的 P90(${QP}ms)不如基线(${BP}ms),回滚"
|
||||
cp -a "$BACKUP" "$SVC"
|
||||
systemctl daemon-reload
|
||||
systemctl restart lanremote-tunnel
|
||||
fi
|
||||
else
|
||||
echo " → 数据不足,保守回滚"
|
||||
cp -a "$BACKUP" "$SVC"
|
||||
systemctl daemon-reload
|
||||
systemctl restart lanremote-tunnel
|
||||
fi
|
||||
|
||||
sleep 15
|
||||
echo
|
||||
echo " 最终参数: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ')"
|
||||
echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
|
||||
|
||||
# 清探针
|
||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
||||
DEV=$(cat /tmp/probe_dev.txt 2>/dev/null)
|
||||
[ -n "${DEV:-}" ] && curl -s --max-time 20 -X POST "http://127.0.0.1:8080/v1/admin/delete" \
|
||||
-H "X-Admin: $KEY" -H 'Content-Type: application/json' \
|
||||
-d "{\"deviceId\":\"$DEV\"}" > /dev/null 2>&1
|
||||
echo " 备份: $BACKUP"
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# 诊断中转为什么 502 / 延时 1426ms
|
||||
set -u
|
||||
|
||||
echo "════════ 1. 服务状态 ════════"
|
||||
for s in lanremote-relay lanremote-tunnel lanremote-nettune lanremote-watchdog.timer; do
|
||||
printf " %-26s %s\n" "$s" "$(systemctl is-active $s 2>&1)"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "════════ 2. 隧道连接数 · 落点 ════════"
|
||||
curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep -E '^cloudflared_tunnel_ha_connections|server_locations' | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 3. 本机中转响应(排除公网因素)════════"
|
||||
for i in 1 2 3; do
|
||||
printf " %d: HTTP %s %sms\n" "$i" \
|
||||
"$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 http://127.0.0.1:8080/v1/health)" \
|
||||
"$(curl -s -o /dev/null -w '%{time_total}' --max-time 8 http://127.0.0.1:8080/v1/health | awk '{printf "%d", $1*1000}')"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "════════ 4. 公网延时拆解(关键)════════"
|
||||
echo " a) 到 Cloudflare 边缘的裸 RTT:"
|
||||
for i in 1 2 3; do
|
||||
t=$(curl -s -o /dev/null -w '%{time_connect}' --max-time 15 https://relay.xxmxxs.top/v1/health 2>/dev/null)
|
||||
echo " 连接耗时 $(echo "$t" | awk '{printf "%.0f", $1*1000}') ms"
|
||||
done
|
||||
|
||||
echo
|
||||
echo " b) 完整请求耗时(含隧道回源):"
|
||||
for i in 1 2 3 4 5; do
|
||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_connect} %{time_starttransfer} %{time_total}' \
|
||||
--max-time 25 -H 'User-Agent: LanRemote/1.8.0' https://relay.xxmxxs.top/v1/health 2>/dev/null)
|
||||
echo " 码/连接/首字节/总计: $(echo "$out" | awk '{printf "%s %.0fms %.0fms %.0fms", $1, $2*1000, $3*1000, $4*1000}')"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo
|
||||
echo "════════ 5. 本机到 Cloudflare 的网络质量 ════════"
|
||||
ping -c 10 -W 2 1.1.1.1 2>/dev/null | tail -2 | sed 's/^/ /'
|
||||
echo " ---"
|
||||
ping -c 10 -W 2 104.21.54.45 2>/dev/null | tail -2 | sed 's/^/ /'
|
||||
echo " ---"
|
||||
ping -c 10 -W 2 198.41.200.43 2>/dev/null | tail -2 | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 6. 隧道日志最近的重连/错误 ════════"
|
||||
tail -400 /root/lanremote-relay/tunnel.log 2>/dev/null \
|
||||
| grep -E 'Registered|Lost connection|Retrying|i/o timeout|context deadline' \
|
||||
| tail -12 | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 7. 隧道连接数变化趋势(连打 30 次)════════"
|
||||
for i in $(seq 1 30); do
|
||||
c=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||
h=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 \
|
||||
-H 'User-Agent: LanRemote/1.8.0' https://relay.xxmxxs.top/v1/health)
|
||||
printf " %2d: 连接 %-3s 公网 %s\n" "$i" "${c:-?}" "$h"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo
|
||||
echo "════════ 8. watchdog 最近记录 ════════"
|
||||
tail -10 /root/lanremote-relay/watchdog.log 2>/dev/null | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 9. 资源占用 ════════"
|
||||
free -m | sed 's/^/ /'
|
||||
echo " 负载: $(cat /proc/loadavg | cut -d' ' -f1-3)"
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
# 看隧道当前配置,并做一轮协议/连接数的对照实测。
|
||||
set -u
|
||||
|
||||
echo "════════ 1. 隧道服务当前参数 ════════"
|
||||
grep -E 'ExecStart' /etc/systemd/system/lanremote-tunnel.service | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 2. cloudflared 版本 & 支持的参数 ════════"
|
||||
cloudflared --version 2>&1 | head -1 | sed 's/^/ /'
|
||||
echo " --- 和丢包恢复相关的参数:"
|
||||
cloudflared tunnel run --help 2>&1 | grep -E 'protocol|ha-connections|edge-ip-version|region|retries' | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 3. 当前落点 ════════"
|
||||
curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep 'server_locations.*} 1' | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 4. 测一轮公网(12 次,记录耗时分布)════════"
|
||||
times=()
|
||||
ok=0
|
||||
for i in $(seq 1 12); do
|
||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \
|
||||
-H 'User-Agent: LanRemote/1.8.0' https://relay.xxmxxs.top/v1/health)
|
||||
code=$(echo "$out" | awk '{print $1}')
|
||||
ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
|
||||
times+=("$ms")
|
||||
[ "$code" = "200" ] && ok=$((ok+1))
|
||||
printf " %2d: HTTP %-4s %5sms\n" "$i" "$code" "$ms"
|
||||
sleep 1
|
||||
done
|
||||
echo " 可用 $ok/12"
|
||||
echo " 耗时: $(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END {printf "最小 %dms 中位 %dms 最大 %dms", a[1], a[int(NR/2)+1], a[NR]}')"
|
||||
|
||||
echo
|
||||
echo "════════ 5. 每个候选 edge IP 的可达性(挑最快的)════════"
|
||||
python3 - <<'PY'
|
||||
import socket, time
|
||||
ips = ["198.41.192.47", "198.41.192.57", "198.41.192.67", "198.41.192.107",
|
||||
"198.41.192.167", "198.41.192.227", "198.41.200.23", "198.41.200.33",
|
||||
"198.41.200.43", "198.41.200.63", "198.41.200.113", "198.41.200.193"]
|
||||
rows = []
|
||||
for ip in ips:
|
||||
s = socket.socket(); s.settimeout(4)
|
||||
t = time.perf_counter()
|
||||
try:
|
||||
s.connect((ip, 7844))
|
||||
ms = (time.perf_counter() - t) * 1000
|
||||
rows.append((ms, ip, "通"))
|
||||
except OSError as e:
|
||||
rows.append((99999, ip, "不通"))
|
||||
finally:
|
||||
s.close()
|
||||
rows.sort()
|
||||
for ms, ip, st in rows:
|
||||
if st == "通":
|
||||
print(" %-16s %6.0f ms" % (ip, ms))
|
||||
else:
|
||||
print(" %-16s 不通" % ip)
|
||||
good = [r for r in rows if r[2] == "通"]
|
||||
print(" => %d/%d 可达,最快 %s" % (len(good), len(rows), good[0][1] if good else "无"))
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "════════ 6. 隧道日志里 i/o timeout 的最近频率 ════════"
|
||||
for kw in 'Registered tunnel connection' 'Lost connection' 'i/o timeout' 'context deadline'; do
|
||||
n=$(tail -800 /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c "$kw")
|
||||
printf " %-34s %s 次(最近 800 行)\n" "$kw" "$n"
|
||||
done
|
||||
@@ -0,0 +1,172 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''修「局域网连接被中转劫持」的 bug。
|
||||
|
||||
用户报的现象(手表截图):
|
||||
局域网能搜到电脑,IP / 端口 / 配对码都填对了,
|
||||
点连接**没有任何反应**,一点反应都没有。
|
||||
|
||||
根因(MainActivity.java 的 connect()):
|
||||
|
||||
boolean usedLan = !relayUrl().isEmpty() ? false : true;
|
||||
if (!relay.isEmpty()) {
|
||||
reply = client.connectViaRelay(...); // ← 走中转
|
||||
} else {
|
||||
reply = client.connect(host, port, ...); // ← 才走局域网
|
||||
}
|
||||
|
||||
**只看「中转设置里填没填地址」,填了就一律走中转,
|
||||
完全无视用户填的局域网 IP 和端口。**
|
||||
|
||||
而中转超时是 40 秒(40000ms)——
|
||||
于是用户点完之后界面卡在「正在连接…」四十秒,
|
||||
在他看来就是"没有任何反应"。
|
||||
|
||||
这个 bug 影响两条路:
|
||||
1. 「局域网连接」页面填 IP 直连(connectLanTyped)
|
||||
2. 首页「已配对的电脑」点一下重连(quickConnect)
|
||||
|
||||
修法:
|
||||
给 connect() 加一个 preferLan 参数。明确要走局域网时:
|
||||
先直连 host:port(超时给短一点,6 秒,局域网本来该是毫秒级)
|
||||
直连不成**再退回中转**(保证最终能连上,不让用户卡死)
|
||||
没有 preferLan 时保持原来的行为(走中转,连上后若同网段再切直连)。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SRC = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote"
|
||||
r"\android\src\com\lanremote\watch\MainActivity.java")
|
||||
|
||||
# ---------------------------------------------------------------- 1. 重载
|
||||
OLD_SIG = """ private void connect(final String host, final int port, final String code,
|
||||
final String token, final String key) {
|
||||
if (busy) {
|
||||
return;
|
||||
}"""
|
||||
|
||||
NEW_SIG = """ private void connect(final String host, final int port, final String code,
|
||||
final String token, final String key) {
|
||||
connect(host, port, code, token, key, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param preferLan 明确要求"先走局域网"。
|
||||
*
|
||||
* <p>用户点「局域网连接」填了 IP,或者点首页已配对电脑时,
|
||||
* 应该先按他给的地址直连。原来的实现只看中转地址填没填 ——
|
||||
* 填了就一律走中转,**完全无视用户填的局域网 IP**,
|
||||
* 而中转超时 40 秒,用户点完只看到「正在连接…」,以为没反应。
|
||||
*
|
||||
* <p>设 true 时:先直连(局域网该是毫秒级,超时给 6 秒),
|
||||
* 不成再退回中转,保证最终能连上。
|
||||
*/
|
||||
private void connect(final String host, final int port, final String code,
|
||||
final String token, final String key,
|
||||
final boolean preferLan) {
|
||||
if (busy) {
|
||||
return;
|
||||
}"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 2. 分支
|
||||
OLD_BRANCH = """ final String relay = relayUrl();
|
||||
Map<String, Object> reply;
|
||||
if (!relay.isEmpty()) {"""
|
||||
|
||||
NEW_BRANCH = """ final String relay = relayUrl();
|
||||
Map<String, Object> reply;
|
||||
if (preferLan) {
|
||||
// ---- 用户明确要走局域网:先直连,失败再退中转 ----
|
||||
// 局域网直连本该是毫秒级,所以超时给短一点。
|
||||
// 原来这里会走中转(40 秒超时),用户以为卡死了。
|
||||
appendLog("局域网直连 " + host + ":" + port + " …");
|
||||
reply = client.connect(host, port, code, token,
|
||||
MainActivity.this, 6000);
|
||||
ok = Json.bool(reply, "ok", false);
|
||||
if (ok) {
|
||||
usedLan = true;
|
||||
} else if (!relay.isEmpty()) {
|
||||
String lanWhy = Json.str(reply, "error",
|
||||
"局域网直连失败");
|
||||
appendLog("局域网直连不成(" + lanWhy
|
||||
+ "),改走中转…");
|
||||
ui.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setStatus("局域网连不上,改用中转…", C_WARN);
|
||||
}
|
||||
});
|
||||
reply = client.connectViaRelay(relay, code, token,
|
||||
MainActivity.this, 40000);
|
||||
usedLan = false;
|
||||
}
|
||||
} else if (!relay.isEmpty()) {"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 3. 调用方
|
||||
OLD_QUICK = """ connect(host, port, null, token, key);
|
||||
}"""
|
||||
|
||||
NEW_QUICK = """ // preferLan = true:先按记录里的地址直连。
|
||||
// 记录里的地址可能是旧的(换过网络),直连会失败 ——
|
||||
// 那时自动退回中转,中转会报回电脑的最新局域网地址并刷新记录。
|
||||
connect(host, port, null, token, key, true);
|
||||
}"""
|
||||
|
||||
OLD_TYPED = """ connect(host, port, code, null,
|
||||
DeviceStore.deviceKey(host, port, store.deviceId()));"""
|
||||
|
||||
NEW_TYPED = """ // preferLan = true:用户就是冲着"局域网直连"来的,
|
||||
// 必须真的直连,不能因为有中转地址就偷偷走中转。
|
||||
connect(host, port, code, null,
|
||||
DeviceStore.deviceKey(host, port, store.deviceId()), true);"""
|
||||
|
||||
|
||||
def patch(text: str, old: str, new: str, label: str) -> tuple:
|
||||
if new.split("\n")[0] in text and label != "分支":
|
||||
# 已经打过补丁的粗略判断
|
||||
pass
|
||||
if old not in text:
|
||||
print(" ✗ 没找到锚点:%s" % label)
|
||||
return text, False
|
||||
count = text.count(old)
|
||||
if count != 1:
|
||||
print(" ! 锚点出现 %d 次(期望 1):%s" % (count, label))
|
||||
return text, False
|
||||
text = text.replace(old, new, 1)
|
||||
print(" ✓ %s" % label)
|
||||
return text, True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
text = SRC.read_text(encoding="utf-8")
|
||||
original = text
|
||||
|
||||
print(" 打补丁:")
|
||||
ok_all = True
|
||||
for old, new, label in (
|
||||
(OLD_SIG, NEW_SIG, "connect() 加重载和 preferLan 参数"),
|
||||
(OLD_QUICK, NEW_QUICK, "quickConnect 传 preferLan=true"),
|
||||
(OLD_TYPED, NEW_TYPED, "connectLanTyped 传 preferLan=true"),
|
||||
(OLD_BRANCH, NEW_BRANCH, "走局域网的分支"),
|
||||
):
|
||||
text, ok = patch(text, old, new, label)
|
||||
ok_all = ok_all and ok
|
||||
|
||||
if not ok_all:
|
||||
print()
|
||||
print(" 有锚点没匹配,没有写回")
|
||||
return 1
|
||||
|
||||
SRC.write_text(text, encoding="utf-8", newline="\n")
|
||||
print()
|
||||
print(" 已写回,文件 %d -> %d 行"
|
||||
% (original.count("\n") + 1, text.count("\n") + 1))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''手表:过期 IP 给提示 + 跳过注定失败的直连尝试。
|
||||
|
||||
用户遇到的情况(截图):
|
||||
首页「已配对的电脑」显示 DESKTOP-RN5I625
|
||||
192.168.2.64:8765
|
||||
但那台电脑早就换网段了(现在是 192.168.10.103)。
|
||||
点一下 -> 先花 6 秒试直连(必然失败)-> 再走中转 40 秒 -> 用户以为卡死。
|
||||
|
||||
改两处:
|
||||
|
||||
1. refreshDeviceList:保存的 IP 如果和手表不在同一个网段,
|
||||
按钮上直接标出来"(地址已过期)"。用户一眼就知道该重新搜。
|
||||
|
||||
2. quickConnect:如果保存的 IP 明显不同网段,
|
||||
**跳过直连**,直接走中转 —— 省掉那 6 秒白等。
|
||||
中转连上后会报回电脑的最新局域网地址,记录也就自动刷新了。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SRC = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote"
|
||||
r"\android\src\com\lanremote\watch\MainActivity.java")
|
||||
|
||||
# ---------------------------------------------------------------- 1. 列表提示
|
||||
OLD_LIST = """ Button button = bigButton(label + "\\n" + host + ":" + port, C_CARD, 14);"""
|
||||
|
||||
NEW_LIST = """ // 保存的地址可能已经过期(电脑换了网络/网段)。
|
||||
// 和手表不在同一网段时直接在按钮上标出来 ——
|
||||
// 不然用户点下去只会等 6 秒直连失败 + 40 秒中转超时,
|
||||
// 完全不知道问题出在哪儿。
|
||||
String stale = "";
|
||||
if (!host.isEmpty() && !hostInMySubnet(host)) {
|
||||
stale = " ⚠ 地址可能已过期";
|
||||
}
|
||||
Button button = bigButton(label + "\\n" + host + ":" + port + stale,
|
||||
C_CARD, 14);"""
|
||||
|
||||
# ---------------------------------------------------------------- 2. 跳过死路
|
||||
OLD_QUICK = """ private void quickConnect(String host, int port, String token, String key) {
|
||||
// preferLan = true:先按记录里的地址直连。
|
||||
// 记录里的地址可能是旧的(换过网络),直连会失败 ——
|
||||
// 那时自动退回中转,中转会报回电脑的最新局域网地址并刷新记录。
|
||||
connect(host, port, null, token, key, true);
|
||||
}"""
|
||||
|
||||
NEW_QUICK = """ private void quickConnect(String host, int port, String token, String key) {
|
||||
// 先判断这个保存下来的地址还有没有意义。
|
||||
// 如果它和手表明显不在同一网段(电脑换网络了),
|
||||
// 直连必然失败 —— 那就别浪费那 6 秒,直接走中转。
|
||||
// 中转连上后会报回电脑的最新局域网地址,记录自动刷新。
|
||||
boolean sameNet = host.isEmpty() || hostInMySubnet(host);
|
||||
if (!sameNet) {
|
||||
appendLog("记录的地址 " + host + " 和手表不在同一网段"
|
||||
+ "(电脑可能换了网络),直接走中转查最新地址");
|
||||
toast("电脑地址已过期,正在通过中转查询最新地址…");
|
||||
}
|
||||
// preferLan = true 时先按记录里的地址直连,失败自动退回中转。
|
||||
connect(host, port, null, token, key, sameNet);
|
||||
}
|
||||
|
||||
/**
|
||||
* host 是不是和手表在同一个 /24 网段。
|
||||
*
|
||||
* <p>读不到手表自己的网卡时返回 true(保守:宁可按原地址试一次,
|
||||
* 也不要因为拿不到权限就误判成"过期")。
|
||||
*/
|
||||
private boolean hostInMySubnet(String host) {
|
||||
if (host == null || host.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
java.util.List<String> mine = localIpv4List();
|
||||
if (mine.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (String myIp : mine) {
|
||||
if (sameSubnet(host, myIp)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}"""
|
||||
|
||||
|
||||
def patch(text: str, old: str, new: str, label: str) -> tuple:
|
||||
if old not in text:
|
||||
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:
|
||||
text = SRC.read_text(encoding="utf-8")
|
||||
before = text.count("\n") + 1
|
||||
|
||||
print(" 打补丁:")
|
||||
ok_all = True
|
||||
for old, new, label in (
|
||||
(OLD_LIST, NEW_LIST, "设备列表标出过期地址"),
|
||||
(OLD_QUICK, NEW_QUICK, "quickConnect 跳过注定失败的直连"),
|
||||
):
|
||||
text, ok = patch(text, old, new, label)
|
||||
ok_all = ok_all and ok
|
||||
|
||||
if not ok_all:
|
||||
print()
|
||||
print(" 有锚点没匹配,没有写回")
|
||||
return 1
|
||||
|
||||
SRC.write_text(text, encoding="utf-8", newline="\n")
|
||||
print()
|
||||
print(" 已写回:%d -> %d 行" % (before, text.count("\n") + 1))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''把长轮询从 15 秒改成 60 秒(服务端 + 两个客户端同步改)。
|
||||
|
||||
== 为什么这是这一轮最有效的优化 ==
|
||||
|
||||
隧道每次请求要多花 1~2 秒(CF 边缘路由 + 回源)。实测:
|
||||
|
||||
本机直连 60 秒的 poll -> 60.0s
|
||||
走公网隧道同一个 poll -> 61.5~62.0s 只多 1.5~2 秒
|
||||
|
||||
看着不多,但**这个开销是按"次数"收的**:
|
||||
|
||||
15 秒一循环 -> 一天 5760 次/端 -> 光路费就 2.4~3.2 小时
|
||||
60 秒一循环 -> 一天 1440 次/端 -> 光路费就 0.6~0.8 小时
|
||||
|
||||
而且请求越频繁,"正好撞上丢包/重连窗口"的概率越大。
|
||||
拉长到 60 秒,暴露面直接降到 1/4。
|
||||
|
||||
== 为什么不会让命令变慢 ==
|
||||
|
||||
长轮询是**有数据立刻返回**(relay.py 里用 device.event 唤醒),
|
||||
不是"等满 60 秒才看一眼"。拉长超时只是减少"空转重开"的次数,
|
||||
真正下命令时的延迟不变。
|
||||
|
||||
实测 60 秒长轮询穿过隧道 4/4 成功;Cloudflare 单请求上限是 100 秒,
|
||||
60 秒留了 40 秒余量。
|
||||
|
||||
== 要同步改的三处 ==
|
||||
|
||||
1. relay/relay.py POLL_SECONDS 默认 25 -> 60
|
||||
2. windows/lanremote/relay.py POLL_TIMEOUT 40 -> 90
|
||||
3. android .../RelayTransport.java POLL_TIMEOUT_MS 40000 -> 90000
|
||||
|
||||
客户端超时必须显著大于服务端 hold 时间,否则会在服务端正常返回之前
|
||||
自己先超时 —— 那会变成"每次轮询都失败"的灾难。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
BOM = b"\xef\xbb\xbf"
|
||||
|
||||
EDITS = [
|
||||
(
|
||||
"relay/relay.py",
|
||||
'POLL_SECONDS = float(os.environ.get("RELAY_POLL", "25"))',
|
||||
'# 长轮询挂多久。
|
||||
# 25 -> 60:隧道每次请求要多花 1~2 秒(CF 边缘路由 + 回源),
|
||||
# 而这个开销是按"次数"收的。60 秒一循环把请求数降到 1/4,
|
||||
# 暴露在丢包/重连窗口里的概率也降到 1/4。
|
||||
# **不会让命令变慢**:有数据时立刻返回(靠 device.event 唤醒),
|
||||
# 拉长只是减少空转重开。
|
||||
# Cloudflare 单请求上限 100 秒,60 留了 40 秒余量(实测 4/4 通过)。
|
||||
POLL_SECONDS = float(os.environ.get("RELAY_POLL", "60"))',
|
||||
),
|
||||
(
|
||||
"windows/lanremote/relay.py",
|
||||
"POLL_TIMEOUT = 40.0",
|
||||
"# 长轮询单次请求的超时。
|
||||
# 必须显著大于服务端的 hold 时间(现在 60 秒),否则会在服务端
|
||||
# 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。
|
||||
# 90 = 60 + 30 秒余量。
|
||||
POLL_TIMEOUT = 90.0",
|
||||
),
|
||||
(
|
||||
"android/src/com/lanremote/watch/RelayTransport.java",
|
||||
" /** 长轮询:中转那边挂 25 秒,客户端超时给宽一点 */\n"
|
||||
" private static final int POLL_TIMEOUT_MS = 40000;",
|
||||
" /**\n"
|
||||
" * 长轮询:中转那边挂 60 秒,客户端超时给宽一点。\n"
|
||||
" *\n"
|
||||
" * <p>必须显著大于服务端的 hold 时间,否则会在服务端正常返回\n"
|
||||
" * 之前自己先超时 —— 那就变成\"每次轮询都失败\"了。\n"
|
||||
" * 90 秒 = 60 + 30 秒余量。\n"
|
||||
" */\n"
|
||||
" private static final int POLL_TIMEOUT_MS = 90000;",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(" 改这三处:")
|
||||
ok_all = True
|
||||
for rel, old, new in EDITS:
|
||||
path = ROOT / rel
|
||||
if not path.is_file():
|
||||
print(" ✗ %s 不存在" % rel)
|
||||
ok_all = False
|
||||
continue
|
||||
raw = path.read_bytes()
|
||||
had_bom = raw.startswith(BOM)
|
||||
text = raw.decode("utf-8-sig")
|
||||
|
||||
if old not in text:
|
||||
if new.split("\n")[-1] in text:
|
||||
print(" • %-44s 已经是新值了" % rel)
|
||||
continue
|
||||
print(" ✗ %-44s 找不到锚点" % rel)
|
||||
ok_all = False
|
||||
continue
|
||||
|
||||
count = text.count(old)
|
||||
if count != 1:
|
||||
print(" ! %-44s 锚点出现 %d 次" % (rel, count))
|
||||
ok_all = False
|
||||
continue
|
||||
|
||||
text = text.replace(old, new, 1)
|
||||
data = text.encode("utf-8")
|
||||
if had_bom or path.suffix == ".ps1":
|
||||
data = BOM + data
|
||||
path.write_bytes(data)
|
||||
print(" ✓ %-44s 已改" % rel)
|
||||
|
||||
print()
|
||||
if not ok_all:
|
||||
print(" 有失败项,请检查")
|
||||
return 1
|
||||
|
||||
# 复核
|
||||
print(" 复核:")
|
||||
checks = [
|
||||
("relay/relay.py", 'os.environ.get("RELAY_POLL", "60")', "服务端 60 秒"),
|
||||
("windows/lanremote/relay.py", "POLL_TIMEOUT = 90.0", "被控端 90 秒"),
|
||||
("android/src/com/lanremote/watch/RelayTransport.java",
|
||||
"POLL_TIMEOUT_MS = 90000", "手表 90 秒"),
|
||||
]
|
||||
for rel, token, label in checks:
|
||||
text = (ROOT / rel).read_text(encoding="utf-8-sig")
|
||||
mark = "✓" if token in text else "✗"
|
||||
print(" %s %s" % (mark, label))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''把长轮询从 15 秒改成 60 秒(服务端 + 两个客户端同步改)。
|
||||
|
||||
== 为什么这是这一轮最有效的优化 ==
|
||||
|
||||
隧道每次请求要多花 1~2 秒(CF 边缘路由 + 回源)。实测:
|
||||
|
||||
本机直连 60 秒的 poll -> 60.0s
|
||||
走公网隧道同一个 poll -> 61.5~62.0s 只多 1.5~2 秒
|
||||
|
||||
看着不多,但**这个开销是按"次数"收的**:
|
||||
|
||||
15 秒一循环 -> 一天 5760 次/端 -> 光路费就 2.4~3.2 小时
|
||||
60 秒一循环 -> 一天 1440 次/端 -> 光路费就 0.6~0.8 小时
|
||||
|
||||
而且请求越频繁,"正好撞上丢包/重连窗口"的概率越大。
|
||||
拉长到 60 秒,暴露面直接降到 1/4。
|
||||
|
||||
== 为什么不会让命令变慢 ==
|
||||
|
||||
长轮询是**有数据立刻返回**(relay.py 里用 device.event 唤醒),
|
||||
不是"等满 60 秒才看一眼"。拉长超时只是减少"空转重开"的次数,
|
||||
真正下命令时的延迟不变。
|
||||
|
||||
实测 60 秒长轮询穿过隧道 4/4 成功;Cloudflare 单请求上限是 100 秒,
|
||||
60 秒留了 40 秒余量。
|
||||
|
||||
== 要同步改的三处 ==
|
||||
|
||||
1. relay/relay.py POLL_SECONDS 默认 25 -> 60
|
||||
2. windows/lanremote/relay.py POLL_TIMEOUT 40 -> 90
|
||||
3. android .../RelayTransport.java POLL_TIMEOUT_MS 40000 -> 90000
|
||||
|
||||
客户端超时必须显著大于服务端 hold 时间,否则会在服务端正常返回之前
|
||||
自己先超时 —— 那会变成"每次轮询都失败"的灾难。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
BOM = b"\xef\xbb\xbf"
|
||||
|
||||
RELAY_OLD = 'POLL_SECONDS = float(os.environ.get("RELAY_POLL", "25"))'
|
||||
RELAY_NEW = """# 长轮询挂多久。
|
||||
# 25 -> 60:隧道每次请求要多花 1~2 秒(CF 边缘路由 + 回源),
|
||||
# 而这个开销是按"次数"收的。60 秒一循环把请求数降到 1/4,
|
||||
# 暴露在丢包/重连窗口里的概率也降到 1/4。
|
||||
# **不会让命令变慢**:有数据时立刻返回(靠 device.event 唤醒),
|
||||
# 拉长只是减少空转重开。
|
||||
# Cloudflare 单请求上限 100 秒,60 留了 40 秒余量(实测 4/4 通过)。
|
||||
POLL_SECONDS = float(os.environ.get("RELAY_POLL", "60"))"""
|
||||
|
||||
AGENT_OLD = "POLL_TIMEOUT = 40.0"
|
||||
AGENT_NEW = """# 长轮询单次请求的超时。
|
||||
# 必须显著大于服务端的 hold 时间(现在 60 秒),否则会在服务端
|
||||
# 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。
|
||||
# 90 = 60 + 30 秒余量。
|
||||
POLL_TIMEOUT = 90.0"""
|
||||
|
||||
WATCH_OLD = (
|
||||
" /** 长轮询:中转那边挂 25 秒,客户端超时给宽一点 */\n"
|
||||
" private static final int POLL_TIMEOUT_MS = 40000;"
|
||||
)
|
||||
WATCH_NEW = (
|
||||
" /**\n"
|
||||
" * 长轮询:中转那边挂 60 秒,客户端超时给宽一点。\n"
|
||||
" *\n"
|
||||
" * <p>必须显著大于服务端的 hold 时间,否则会在服务端正常返回\n"
|
||||
" * 之前自己先超时 —— 那就变成\"每次轮询都失败\"了。\n"
|
||||
" * 90 秒 = 60 + 30 秒余量。\n"
|
||||
" */\n"
|
||||
" private static final int POLL_TIMEOUT_MS = 90000;"
|
||||
)
|
||||
|
||||
EDITS = [
|
||||
("relay/relay.py", RELAY_OLD, RELAY_NEW, "服务端 60 秒"),
|
||||
("windows/lanremote/relay.py", AGENT_OLD, AGENT_NEW, "被控端 90 秒"),
|
||||
("android/src/com/lanremote/watch/RelayTransport.java",
|
||||
WATCH_OLD, WATCH_NEW, "手表 90 秒"),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(" 改这三处:")
|
||||
ok_all = True
|
||||
for rel, old, new, label in EDITS:
|
||||
path = ROOT / rel
|
||||
if not path.is_file():
|
||||
print(" ✗ %s 不存在" % rel)
|
||||
ok_all = False
|
||||
continue
|
||||
raw = path.read_bytes()
|
||||
had_bom = raw.startswith(BOM)
|
||||
text = raw.decode("utf-8-sig")
|
||||
|
||||
if old not in text:
|
||||
if new.strip().splitlines()[-1] in text:
|
||||
print(" • %-44s 已经是新值了" % rel)
|
||||
continue
|
||||
print(" ✗ %-44s 找不到锚点" % rel)
|
||||
ok_all = False
|
||||
continue
|
||||
|
||||
count = text.count(old)
|
||||
if count != 1:
|
||||
print(" ! %-44s 锚点出现 %d 次" % (rel, count))
|
||||
ok_all = False
|
||||
continue
|
||||
|
||||
text = text.replace(old, new, 1)
|
||||
data = text.encode("utf-8")
|
||||
if had_bom or path.suffix == ".ps1":
|
||||
data = BOM + data
|
||||
path.write_bytes(data)
|
||||
print(" ✓ %-44s 已改" % rel)
|
||||
|
||||
print()
|
||||
if not ok_all:
|
||||
print(" 有失败项,请检查")
|
||||
return 1
|
||||
|
||||
print(" 复核:")
|
||||
checks = [
|
||||
("relay/relay.py", 'os.environ.get("RELAY_POLL", "60")'),
|
||||
("windows/lanremote/relay.py", "POLL_TIMEOUT = 90.0"),
|
||||
("android/src/com/lanremote/watch/RelayTransport.java",
|
||||
"POLL_TIMEOUT_MS = 90000"),
|
||||
]
|
||||
for rel, token in checks:
|
||||
text = (ROOT / rel).read_text(encoding="utf-8-sig")
|
||||
print(" %s %s" % ("✓" if token in text else "✗", rel))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
# 把长轮询从 15 秒拉到 60 秒,实测能不能穿过隧道。
|
||||
#
|
||||
# 为什么这是核心优化:
|
||||
# 隧道每次请求额外加 1~2 秒(连接建立 + CF 内部路由)。
|
||||
# 15 秒一循环 -> 一天 5760 次/端,每次都要吃那 1~2 秒。
|
||||
# 60 秒一循环 -> 一天 1440 次/端,开销直接摊薄 4 倍。
|
||||
#
|
||||
# 而且**命令延迟不会变差**:长轮询是"有数据立刻返回",
|
||||
# 不是"等满 60 秒才返回"。拉长只是减少空转次数。
|
||||
#
|
||||
# Cloudflare 对单请求有 100 秒上限(超了 524),60 秒留 40 秒余量。
|
||||
set -u
|
||||
|
||||
BASE=https://relay.xxmxxs.top
|
||||
LOCAL=http://127.0.0.1:8080
|
||||
UA='LanRemote/1.8.0'
|
||||
SVC=/etc/systemd/system/lanremote-relay.service
|
||||
BACKUP=/root/lanremote-relay.service.bak.$(date +%s)
|
||||
|
||||
echo "════════ 1. 备份并改成 60 秒 ════════"
|
||||
cp -a "$SVC" "$BACKUP"
|
||||
echo " 备份: $BACKUP"
|
||||
sed -i 's/^Environment=RELAY_POLL=.*/Environment=RELAY_POLL=60/' "$SVC"
|
||||
grep 'RELAY_POLL' "$SVC" | sed 's/^/ /'
|
||||
systemctl daemon-reload
|
||||
systemctl restart lanremote-relay
|
||||
sleep 8
|
||||
echo " 中转: $(systemctl is-active lanremote-relay)"
|
||||
curl -s --max-time 10 "$LOCAL/v1/health" | head -c 90
|
||||
echo
|
||||
echo " 等隧道重新连上…"
|
||||
for i in $(seq 1 15); do
|
||||
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 -H "User-Agent: $UA" "$BASE/v1/health")
|
||||
printf " 第 %d 次: %s\n" "$i" "$c"
|
||||
[ "$c" = "200" ] && break
|
||||
sleep 3
|
||||
done
|
||||
|
||||
echo
|
||||
echo "════════ 2. 本机直连(基准,应该约 60 秒)════════"
|
||||
DEV="probe60-$(date +%s)"
|
||||
REG=$(curl -s --max-time 30 -X POST "$LOCAL/v1/agent/register" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"deviceId\":\"$DEV\",\"code\":\"888888\",\"name\":\"probe60\",\"version\":\"1.8.0\"}")
|
||||
TOKEN=$(echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
|
||||
echo " token ${#TOKEN} 字符"
|
||||
for i in 1 2; do
|
||||
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 120 \
|
||||
-X POST "$LOCAL/v1/agent/poll" -H "X-Token: $TOKEN")
|
||||
echo " 第 $i 次: $(echo "$out" | awk '{printf "码 %s 耗时 %.1fs", $1, $2}')"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "════════ 3. 走公网(关键)════════"
|
||||
ok=0
|
||||
for i in 1 2 3 4; do
|
||||
out=$(curl -s -o /dev/null -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}')
|
||||
printf " 第 %d 次: 码 %-4s 耗时 %ss" "$i" "$code" "$secs"
|
||||
if [ "$code" = "200" ]; then
|
||||
echo " ✓"
|
||||
ok=$((ok + 1))
|
||||
elif [ "$code" = "524" ]; then
|
||||
echo " **Cloudflare 100 秒上限挡了**"
|
||||
else
|
||||
echo " ✗"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo " 成功 $ok/4"
|
||||
|
||||
echo
|
||||
echo "════════ 4. 顺便测「有数据时立刻返回」有没有退化 ════════"
|
||||
echo " (往队列里塞点东西,看 poll 是不是马上就回来)"
|
||||
# 用 client 侧塞:先建会话太麻烦,改成直接看日志时间戳
|
||||
echo " 跳过(需要完整会话,后面用真机验证)"
|
||||
|
||||
echo
|
||||
echo "════════ 5. 结论 ════════"
|
||||
if [ "$ok" -ge 3 ]; then
|
||||
echo " ✓ 60 秒长轮询能在隧道上跑通,可以改成这个值"
|
||||
else
|
||||
echo " ✗ 60 秒撑不住,回滚到 15 秒"
|
||||
cp -a "$BACKUP" "$SVC"
|
||||
systemctl daemon-reload
|
||||
systemctl restart lanremote-relay
|
||||
echo " 已回滚"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo " 服务端设定现在是: $(grep -o 'RELAY_POLL=[0-9]*' "$SVC")"
|
||||
echo " 日志: $(tail -30 /root/lanremote-relay/relay.log | grep '长轮询' | tail -1)"
|
||||
|
||||
echo
|
||||
echo "════════ 6. 清掉临时设备 ════════"
|
||||
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 80
|
||||
echo
|
||||
@@ -658,7 +658,16 @@ public class MainActivity extends Activity implements RemoteClient.Listener {
|
||||
final String token = Json.str(device, "token", "");
|
||||
final String label = Json.str(device, "label", host);
|
||||
|
||||
Button button = bigButton(label + "\n" + host + ":" + port, C_CARD, 14);
|
||||
// 保存的地址可能已经过期(电脑换了网络/网段)。
|
||||
// 和手表不在同一网段时直接在按钮上标出来 ——
|
||||
// 不然用户点下去只会等 6 秒直连失败 + 40 秒中转超时,
|
||||
// 完全不知道问题出在哪儿。
|
||||
String stale = "";
|
||||
if (!host.isEmpty() && !hostInMySubnet(host)) {
|
||||
stale = " ⚠ 地址可能已过期";
|
||||
}
|
||||
Button button = bigButton(label + "\n" + host + ":" + port + stale,
|
||||
C_CARD, 14);
|
||||
button.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, dp(58));
|
||||
@@ -2191,8 +2200,10 @@ public class MainActivity extends Activity implements RemoteClient.Listener {
|
||||
lastCode = code;
|
||||
lanDirect = true; // 断了自动切回中转
|
||||
appendLog("局域网直连 " + host + ":" + port);
|
||||
// preferLan = true:用户就是冲着"局域网直连"来的,
|
||||
// 必须真的直连,不能因为有中转地址就偷偷走中转。
|
||||
connect(host, port, code, null,
|
||||
DeviceStore.deviceKey(host, port, store.deviceId()));
|
||||
DeviceStore.deviceKey(host, port, store.deviceId()), true);
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
@@ -2363,7 +2374,42 @@ public class MainActivity extends Activity implements RemoteClient.Listener {
|
||||
// 不再往输入框里写东西(那两个框已经删了)——
|
||||
// 直接连。host/port 对中转路径没有意义,
|
||||
// 但 key 仍然用来区分"哪台已配对的电脑"。
|
||||
connect(host, port, null, token, key);
|
||||
//
|
||||
// 先判断这个保存下来的地址还有没有意义:
|
||||
// 电脑换过网络的话(比如从 192.168.2.x 换到 192.168.10.x),
|
||||
// 直连必然失败。那就别浪费那 6 秒,直接走中转 ——
|
||||
// 中转连上后会报回电脑的最新局域网地址,记录自动刷新。
|
||||
boolean sameNet = host.isEmpty() || hostInMySubnet(host);
|
||||
if (!sameNet) {
|
||||
appendLog("记录的地址 " + host + " 和手表不在同一网段"
|
||||
+ "(电脑可能换了网络),直接走中转查最新地址");
|
||||
toast("电脑地址已过期,正在通过中转查询最新地址…");
|
||||
}
|
||||
// preferLan = true 时先按记录里的地址直连,失败自动退回中转。
|
||||
connect(host, port, null, token, key, sameNet);
|
||||
}
|
||||
|
||||
/**
|
||||
* host 是不是和手表在同一个 /24 网段。
|
||||
*
|
||||
* <p>读不到手表自己的网卡时返回 true(保守:宁可按原地址试一次,
|
||||
* 也不要因为拿不到权限就误判成"过期",那会让本可以直连的场景
|
||||
* 白走一趟中转)。
|
||||
*/
|
||||
private boolean hostInMySubnet(String host) {
|
||||
if (host == null || host.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
java.util.List<String> mine = localIpv4List();
|
||||
if (mine.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (String myIp : mine) {
|
||||
if (sameSubnet(host, myIp)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 手表自己在局域网里的 IPv4 地址(可能多张网卡)。拿不到就空。 */
|
||||
@@ -2462,6 +2508,23 @@ public class MainActivity extends Activity implements RemoteClient.Listener {
|
||||
|
||||
private void connect(final String host, final int port, final String code,
|
||||
final String token, final String key) {
|
||||
connect(host, port, code, token, key, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param preferLan 明确要求"先走局域网"。
|
||||
*
|
||||
* <p>用户点「局域网连接」填了 IP,或者点首页已配对电脑时,
|
||||
* 应该先按他给的地址直连。原来的实现只看中转地址填没填 ——
|
||||
* 填了就一律走中转,**完全无视用户填的局域网 IP**,
|
||||
* 而中转超时 40 秒,用户点完只看到「正在连接…」,以为没反应。
|
||||
*
|
||||
* <p>设 true 时:先直连(局域网该是毫秒级,超时给 6 秒),
|
||||
* 不成再退回中转,保证最终能连上。
|
||||
*/
|
||||
private void connect(final String host, final int port, final String code,
|
||||
final String token, final String key,
|
||||
final boolean preferLan) {
|
||||
if (busy) {
|
||||
return;
|
||||
}
|
||||
@@ -2495,7 +2558,32 @@ public class MainActivity extends Activity implements RemoteClient.Listener {
|
||||
try {
|
||||
final String relay = relayUrl();
|
||||
Map<String, Object> reply;
|
||||
if (!relay.isEmpty()) {
|
||||
if (preferLan) {
|
||||
// ---- 用户明确要走局域网:先直连,失败再退中转 ----
|
||||
// 局域网直连本该是毫秒级,所以超时给短一点。
|
||||
// 原来这里会走中转(40 秒超时),用户以为卡死了。
|
||||
appendLog("局域网直连 " + host + ":" + port + " …");
|
||||
reply = client.connect(host, port, code, token,
|
||||
MainActivity.this, 6000);
|
||||
ok = Json.bool(reply, "ok", false);
|
||||
if (ok) {
|
||||
usedLan = true;
|
||||
} else if (!relay.isEmpty()) {
|
||||
String lanWhy = Json.str(reply, "error",
|
||||
"局域网直连失败");
|
||||
appendLog("局域网直连不成(" + lanWhy
|
||||
+ "),改走中转…");
|
||||
ui.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setStatus("局域网连不上,改用中转…", C_WARN);
|
||||
}
|
||||
});
|
||||
reply = client.connectViaRelay(relay, code, token,
|
||||
MainActivity.this, 40000);
|
||||
usedLan = false;
|
||||
}
|
||||
} else if (!relay.isEmpty()) {
|
||||
// 跨网络:电脑和手表都在 NAT 后面,直连不可能,
|
||||
// 两边各自连出去、由中转按配对码对接。
|
||||
// 走公网要几个 HTTP 往返,超时给宽一点。
|
||||
|
||||
@@ -36,8 +36,14 @@ public class RelayTransport implements Transport {
|
||||
private static final int FLUSH_BYTES = 32 * 1024;
|
||||
/** 攒不住了也要发,别让命令卡住 */
|
||||
private static final int FLUSH_MILLIS = 40;
|
||||
/** 长轮询:中转那边挂 25 秒,客户端超时给宽一点 */
|
||||
private static final int POLL_TIMEOUT_MS = 40000;
|
||||
/**
|
||||
* 长轮询:中转那边挂 60 秒,客户端超时给宽一点。
|
||||
*
|
||||
* <p>必须显著大于服务端的 hold 时间,否则会在服务端正常返回
|
||||
* 之前自己先超时 —— 那就变成"每次轮询都失败"了。
|
||||
* 90 秒 = 60 + 30 秒余量。
|
||||
*/
|
||||
private static final int POLL_TIMEOUT_MS = 90000;
|
||||
/** 发送/接收的单次请求超时 */
|
||||
private static final int SEND_TIMEOUT_MS = 30000;
|
||||
/** 网络出错后的退避 */
|
||||
|
||||
+8
-1
@@ -68,7 +68,14 @@ BIND = os.environ.get("RELAY_BIND", "127.0.0.1")
|
||||
DATA_DIR = Path(os.environ.get("RELAY_DATA", "/root/lanremote-relay"))
|
||||
|
||||
# 长轮询最多挂多久。太长会占着 Cloudflare 的连接,太短则空转请求多。
|
||||
POLL_SECONDS = float(os.environ.get("RELAY_POLL", "25"))
|
||||
# 长轮询挂多久。
|
||||
# 25 -> 60:隧道每次请求要多花 1~2 秒(CF 边缘路由 + 回源),
|
||||
# 而这个开销是按"次数"收的。60 秒一循环把请求数降到 1/4,
|
||||
# 暴露在丢包/重连窗口里的概率也降到 1/4。
|
||||
# **不会让命令变慢**:有数据时立刻返回(靠 device.event 唤醒),
|
||||
# 拉长只是减少空转重开。
|
||||
# Cloudflare 单请求上限 100 秒,60 留了 40 秒余量(实测 4/4 通过)。
|
||||
POLL_SECONDS = float(os.environ.get("RELAY_POLL", "60"))
|
||||
|
||||
# 单个设备最多缓多少字节没发出去。手表半天不取就丢最旧的,保住内存。
|
||||
MAX_QUEUE_BYTES = int(os.environ.get("RELAY_QUEUE_BYTES",
|
||||
|
||||
@@ -31,7 +31,11 @@ import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
# 长轮询:中转那边挂 25 秒,客户端超时给宽一点
|
||||
POLL_TIMEOUT = 40.0
|
||||
# 长轮询单次请求的超时。
|
||||
# 必须显著大于服务端的 hold 时间(现在 60 秒),否则会在服务端
|
||||
# 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。
|
||||
# 90 = 60 + 30 秒余量。
|
||||
POLL_TIMEOUT = 90.0
|
||||
# 单次请求体上限(一个 256KB 分块 Base64 后约 350KB,给足余量)
|
||||
MAX_BODY = 8 * 1024 * 1024
|
||||
# 网络断了之后的退避
|
||||
|
||||
Reference in New Issue
Block a user