diff --git a/_build/poll180_patch.py b/_build/poll180_patch.py new file mode 100644 index 0000000..01ce1ca --- /dev/null +++ b/_build/poll180_patch.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +r'''客户端长轮询超时 120 -> 180 秒。 + +== 实测证据 == + +relay-stability 跑了 30 次真实长轮询,其中**第 1 次耗时 126604ms**: + + 服务端 hold 是 60 秒 + 剩下 66 秒全是网络停顿(4G 抖动 + 重传退避) + 而客户端超时写的是 120000ms + -> 126.6 > 120,那一次会被**误判成失败**,即使服务端已经正常返回了 + +其余 29 次都是 61~62 秒,很正常。所以不是 hold 太长,是网络尾巴太长。 + +== 为什么把超时放宽到 180 而不是把 hold 缩短 == + +两个选择: + A. hold 缩到 45 秒,超时保持 120 -> 最坏 45+66=111 < 120,安全 + 但请求数多 33%,而每次请求都要吃隧道那 1~2 秒开销 + B. hold 保持 60 秒,超时放宽到 180 -> 最坏 60+120=180,安全 + +选 B。理由:这条链路的根本问题是丢包和抖动, +**请求越少,撞上抖动窗口的概率越小**。 +放宽客户端超时只是"别把慢但成功的请求冤枉掉",不增加任何请求。 + +而且客户端已经有"连续失败 3 次才判离线"的保护 +(OFFLINE_AFTER=3),所以偶尔一次 180 秒的等待不会让界面闪"已断开"。 +''' +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 = [ + ( + "windows/lanremote/relay.py", + """# 长轮询单次请求的超时。 +# 必须显著大于服务端的 hold 时间(现在 60 秒),否则会在服务端 +# 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。 +# 120 = 60(hold) + 60(网络余量)。 +# 为什么余量给这么大:实测出现过一次 93589ms 的长尾 +# (4G 丢包触发 TCP 重传退避),原来设 90 秒会把它误判成失败。 +POLL_TIMEOUT = 120.0""", + """# 长轮询单次请求的超时。 +# 必须显著大于服务端的 hold 时间(现在 60 秒),否则会在服务端 +# 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。 +# 180 = 60(hold) + 120(网络余量)。 +# 余量一直在加大,因为实测的长尾一直在刷新: +# 90 秒 -> 实测 93589ms 不够 +# 120 秒 -> 实测 126604ms 不够(30 次里出现 1 次,其余都是 61~62 秒) +# 4G 丢包触发的重传退避能拖出一分钟以上的停顿,这是链路特性。 +# 放宽超时只是"别把慢但成功的请求冤枉掉",不增加请求数。 +# 而且客户端有"连续失败 3 次才判离线"的保护, +# 偶尔一次长等待不会让界面闪"已断开"。 +POLL_TIMEOUT = 180.0""", + ), + ( + "android/src/com/lanremote/watch/RelayTransport.java", + """ /** + * 长轮询:中转那边挂 60 秒,客户端超时给宽一点。 + * + *

必须显著大于服务端的 hold 时间,否则会在服务端正常返回 + * 之前自己先超时 —— 那就变成"每次轮询都失败"了。 + * 120 秒 = 60(hold) + 60(网络余量)。 + * + *

余量给这么大是因为实测出现过一次 93589ms 的长尾 + * (4G 丢包触发 TCP 重传退避)。设 90 秒会把那次 + * 其实成功的请求误判成失败。 + */ + private static final int POLL_TIMEOUT_MS = 120000;""", + """ /** + * 长轮询:中转那边挂 60 秒,客户端超时给宽一点。 + * + *

必须显著大于服务端的 hold 时间,否则会在服务端正常返回 + * 之前自己先超时 —— 那就变成"每次轮询都失败"了。 + * + *

180 秒 = 60(hold) + 120(网络余量)。 + * 余量一直在加大,因为实测的长尾一直在刷新: + * 90 秒时实测到 93589ms,120 秒时实测到 126604ms + * (30 次里 1 次,其余都是 61~62 秒)。 + * 4G 丢包触发的重传退避能拖出一分钟以上的停顿,这是链路特性。 + * 放宽超时只是"别把慢但成功的请求冤枉掉",不增加请求数。 + */ + private static final int POLL_TIMEOUT_MS = 180000;""", + ), +] + + +def main() -> int: + 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 "180.0" in text or "180000" in text: + print(" • %-46s 已经是新值" % rel) + continue + print(" ✗ %-46s 找不到锚点" % rel) + ok_all = False + continue + text = text.replace(old, new, 1) + data = text.encode("utf-8") + if had_bom: + data = BOM + data + path.write_bytes(data) + print(" ✓ %-46s 已改" % rel) + + print() + if not ok_all: + return 1 + for rel, token in ( + ("windows/lanremote/relay.py", "POLL_TIMEOUT = 180.0"), + ("android/src/com/lanremote/watch/RelayTransport.java", + "POLL_TIMEOUT_MS = 180000"), + ): + 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()) diff --git a/android/src/com/lanremote/watch/RelayTransport.java b/android/src/com/lanremote/watch/RelayTransport.java index b759c48..7908508 100644 --- a/android/src/com/lanremote/watch/RelayTransport.java +++ b/android/src/com/lanremote/watch/RelayTransport.java @@ -41,13 +41,15 @@ public class RelayTransport implements Transport { * *

必须显著大于服务端的 hold 时间,否则会在服务端正常返回 * 之前自己先超时 —— 那就变成"每次轮询都失败"了。 - * 120 秒 = 60(hold) + 60(网络余量)。 * - *

余量给这么大是因为实测出现过一次 93589ms 的长尾 - * (4G 丢包触发 TCP 重传退避)。设 90 秒会把那次 - * 其实成功的请求误判成失败。 + *

180 秒 = 60(hold) + 120(网络余量)。 + * 余量一直在加大,因为实测的长尾一直在刷新: + * 90 秒时实测到 93589ms,120 秒时实测到 126604ms + * (30 次里 1 次,其余都是 61~62 秒)。 + * 4G 丢包触发的重传退避能拖出一分钟以上的停顿,这是链路特性。 + * 放宽超时只是"别把慢但成功的请求冤枉掉",不增加请求数。 */ - private static final int POLL_TIMEOUT_MS = 120000; + private static final int POLL_TIMEOUT_MS = 180000; /** 发送/接收的单次请求超时 */ private static final int SEND_TIMEOUT_MS = 30000; /** 网络出错后的退避 */ diff --git a/windows/lanremote/relay.py b/windows/lanremote/relay.py index 04731ea..fb0f7cd 100644 --- a/windows/lanremote/relay.py +++ b/windows/lanremote/relay.py @@ -34,10 +34,15 @@ import urllib.request # 长轮询单次请求的超时。 # 必须显著大于服务端的 hold 时间(现在 60 秒),否则会在服务端 # 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。 -# 120 = 60(hold) + 60(网络余量)。 -# 为什么余量给这么大:实测出现过一次 93589ms 的长尾 -# (4G 丢包触发 TCP 重传退避),原来设 90 秒会把它误判成失败。 -POLL_TIMEOUT = 120.0 +# 180 = 60(hold) + 120(网络余量)。 +# 余量一直在加大,因为实测的长尾一直在刷新: +# 90 秒 -> 实测 93589ms 不够 +# 120 秒 -> 实测 126604ms 不够(30 次里出现 1 次,其余都是 61~62 秒) +# 4G 丢包触发的重传退避能拖出一分钟以上的停顿,这是链路特性。 +# 放宽超时只是"别把慢但成功的请求冤枉掉",不增加请求数。 +# 而且客户端有"连续失败 3 次才判离线"的保护, +# 偶尔一次长等待不会让界面闪"已断开"。 +POLL_TIMEOUT = 180.0 # 单次请求体上限(一个 256KB 分块 Base64 后约 350KB,给足余量) MAX_BODY = 8 * 1024 * 1024 # 网络断了之后的退避 @@ -243,12 +248,14 @@ class RelayLink: """开始连中转。 url 支持用**分号**分隔多个地址,按顺序尝试: - http://192.168.2.156:8080;https://relay.xxmxxs.top + http://192.168.1.10:8080;https://aaa.bbb.com 为什么需要多个: - * 电脑和随身 WiFi 常在同一局域网 —— 直连局域网中转更快更稳, + * 电脑和中转常在同一局域网 —— 直连局域网中转更快更稳, 而且地址永远不变(不用管隧道地址变来变去) * 电脑在外面时才走隧道 + + 注意:**地址由用户自己填**,代码里不内置任何默认中转地址。 """ candidates = [] for piece in (url or "").replace(",", ";").split(";"):