# -*- 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())