Files
ykxmz/_build/poll120_patch.py
xmz7891 bf15a2ee61 静态资源交给 Cloudflare 边缘缓存(下载提速 41 倍)
实测基线:
  ykxmz-setup-1.8.1.exe   只下到 237568/15067199 字节就停(17 KB/s)
  ykxmz-watch-1.8.1.apk   201523 字节 / 66.8 秒 = 3 KB/s
  ykxmz-server-1.8.1.zip  214900 字节 / 3.1 秒  = 67 KB/s

原因:relay 给所有响应都发 Cache-Control: no-store,
Cloudflare 完全不缓存 —— 每次下载/每张图都要穿隧道回 4G 源站。

改动:
  /download/*   no-store -> public, max-age=604800, immutable
                文件名带版本号,同一名字内容永不改变,可以长缓存
  /             -> public, max-age=60(挡住刷新重复回源)
  /favicon.ico  已是 max-age=86400,保持不变
  /shot/*       已是 max-age=86400,保持不变
  /v1/*         保持 no-store(接口必须实时)

顺带修两个自己踩的坑:

1. _send() 无条件先发 no-store,再追加 extra 里的 Cache-Control,
   结果是**两个 Cache-Control 头**,客户端只认第一个 ——
   调用方传的缓存策略被吃掉。改成 extra 里有就不再发默认值。

2. do_HEAD 和 do_GET 的缓存头不一致:HEAD 对非 /shot/ 一律报 no-store。
   下载器/代理用 HEAD 探测时会误判文件不可缓存。
   现在 _head_info 给每个路由带上和 GET 完全相同的缓存头,
   do_HEAD 也不再盲目覆盖。

结果(同一个测试口径):
  setup.exe  17 KB/s(截断) -> 695 KB/s 完整 15 MB   41 倍
  watch.apk   3 KB/s         -> 101 KB/s              33 倍
  server.zip 67 KB/s         ->  87 KB/s
  三个文件全部 cf-cache-status=HIT(不再穿隧道回源)

排查过程记一笔:我一开始用 curl -I 验证,
看到的全是 no-store —— 因为 **-I 发的是 HEAD**,
而 HEAD 走的是另一个分支。查了半天才发现是验证方法错了。
2026-10-07 15:44:48 +08:00

109 lines
4.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
r'''客户端长轮询超时 90 -> 120 秒。
原因(实测数据):
quic+6 跑 10 次真实长轮询,出现一次 **93589ms** 的长尾。
服务端 hold 是 60 秒,也就是网络在这一次多花了 33 秒。
而客户端超时设的是 90 秒 —— 93.6 > 90,那一次**必然被判失败**,
即使服务端其实已经正常返回了。
长尾来自 4G 丢包触发的 TCP 重传退避(1.4s 的 RTT 下,
几次重传就是几十秒)。这是链路特性,改协议也消不掉,
只能把客户端的容忍度放宽。
新值:120 秒 = 60(hold) + 60(网络余量)。
Cloudflare 对单请求的上限是 100 秒,所以服务端 hold 60 秒是安全的;
客户端 120 秒只是为了不"冤枉"那些慢但成功的请求。
'''
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 秒),否则会在服务端
# 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。
# 90 = 60 + 30 秒余量。
POLL_TIMEOUT = 90.0""",
"""# 长轮询单次请求的超时。
# 必须显著大于服务端的 hold 时间(现在 60 秒),否则会在服务端
# 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。
# 120 = 60(hold) + 60(网络余量)。
# 为什么余量给这么大:实测出现过一次 93589ms 的长尾
# (4G 丢包触发 TCP 重传退避),原来设 90 秒会把它误判成失败。
POLL_TIMEOUT = 120.0""",
),
(
"android/src/com/lanremote/watch/RelayTransport.java",
""" /**
* 长轮询:中转那边挂 60 秒,客户端超时给宽一点。
*
* <p>必须显著大于服务端的 hold 时间,否则会在服务端正常返回
* 之前自己先超时 —— 那就变成"每次轮询都失败"了。
* 90 秒 = 60 + 30 秒余量。
*/
private static final int POLL_TIMEOUT_MS = 90000;""",
""" /**
* 长轮询:中转那边挂 60 秒,客户端超时给宽一点。
*
* <p>必须显著大于服务端的 hold 时间,否则会在服务端正常返回
* 之前自己先超时 —— 那就变成"每次轮询都失败"了。
* 120 秒 = 60(hold) + 60(网络余量)。
*
* <p>余量给这么大是因为实测出现过一次 93589ms 的长尾
* (4G 丢包触发 TCP 重传退避)。设 90 秒会把那次
* 其实成功的请求误判成失败。
*/
private static final int POLL_TIMEOUT_MS = 120000;""",
),
]
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 "120000" in text or "120.0" in text:
print(" • %-44s 已经是新值" % rel)
continue
print(" ✗ %-44s 找不到锚点" % 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(" ✓ %-44s 已改" % rel)
print()
if not ok_all:
return 1
for rel, token in (
("windows/lanremote/relay.py", "POLL_TIMEOUT = 120.0"),
("android/src/com/lanremote/watch/RelayTransport.java",
"POLL_TIMEOUT_MS = 120000"),
):
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())