Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ DNS 怎么走:
4. 打开游戏。窗口每 5 秒显示一次经隧道到 Cloudflare 的延迟。
5. 不玩了就关闭窗口。

### 网络诊断(默认开启)

每次启动先花约 20 秒测三段,告诉你**卡在哪一段、该怎么办**,再开始加速:

| 测哪一段 | 怎么测 | 不稳说明什么 |
| :--- | :--- | :--- |
| 电脑 → 路由器 | ping 默认网关 | 家里的 Wi-Fi/网线/路由器有问题。**WARP 帮不上**,程序会给出具体做法(插网线、5GHz、关定位、关网卡节能、开 SQM) |
| → 运营商 | 先 traceroute 找到第一个公网路由器,再 ping 它 | 运营商接入有问题,WARP 也要经过这一段,**帮不上** |
| → 暴雪(美东) | 对 `overwatch.blizzard.com`(AWS 弗吉尼亚)做 TLS 握手计时 | 前两段正常而这段不稳 = 运营商到暴雪的线路问题,**这种情况 WARP 可能有帮助** |

用 `-v6only=false` 运行时,连上 WARP 后还会**经 WARP 再测一次暴雪**,和直连并排对比,直接告诉你开加速更稳还是直连更好。

说明:
- 测的是暴雪美东的网站服务器,不是对局服务器(对局服务器地址不公开),所以延迟数字只代表到暴雪美东的线路,和游戏里显示的不完全一样。
- 测暴雪用 TLS 握手而不是普通 TCP 连接:WARP 会在自己的边缘直接完成 TCP 握手,那样测出来只有几毫秒,是假的。
- 有些路由器不回应 ping,会显示“没有回应”,这不算故障。
- 不想每次都等,加 `-diag=false` 跳过。

### 开着与关闭

| 操作 | 结果 |
Expand All @@ -76,6 +94,7 @@ DNS 怎么走:

| 参数 | 作用 |
| :--- | :--- |
| `-diag=false` | 跳过启动时的网络诊断 |
| `-v6only=false` | 恢复 IPv4 出口(仅流量模式,DNS 用系统设置)。**玩只有 IPv4 的游戏(如守望先锋)时必须加** |
| `-ipv4` | 有 IPv6 时也同时测试 IPv4 入口(默认有 IPv6 就只用 IPv6 入口) |
| `-count 200` | 每次优选测试的入口数量 |
Expand Down Expand Up @@ -140,6 +159,7 @@ go build -trimpath -ldflags "-s -w" -o WarpGameBoost.exe .

版本号显示在窗口标题和第一行,也可以右键 exe →“属性”→“详细信息”查看。

- **v0.6.0**:新增网络诊断(默认开启,`-diag=false` 跳过):启动时测“电脑→路由器”“→运营商”“→暴雪美东”三段的延迟、抖动、丢包,指出问题在哪一段并给出做法;`-v6only=false` 时连上后再经 WARP 测一次暴雪,与直连对比,告诉你开不开加速更好。
- **v0.5.0**:新增出口仅 IPv6(`-v6only`,默认开启):IPv4 全部禁用且立即拒绝,DNS 改用经隧道的 Cloudflare IPv6 解析;只有 IPv4 的游戏(如守望先锋)要加 `-v6only=false`。修复:本机没有 IPv6 时,以前经隧道的 IPv6 实际用不上(Windows 只给程序 IPv4 地址),现在可以正常使用。
- **v0.4.0**:改为仅流量模式:虚拟网卡不再设置 Cloudflare DNS,DNS 保持系统设置。
- **v0.3.0**:程序附带版本号(窗口标题、启动第一行、`-version` 参数、文件属性);exe 自带管理员权限声明,双击时直接弹出权限确认。
Expand Down
9 changes: 6 additions & 3 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
新增**出口仅 IPv6**(`-v6only`,默认开启):网站只看到 Cloudflare 的 IPv6 地址,IPv4 全部禁用并立即拒绝(程序不会卡住等超时)。家里 Wi-Fi 只有 IPv4 也能用。DNS 在这个模式下改用经隧道的 Cloudflare IPv6 解析(`2606:4700:4700::1111`)。
新增**网络诊断**(默认开启,加 `-diag=false` 跳过)。

**玩守望先锋等只有 IPv4 服务器的游戏,必须加 `-v6only=false`**(右键 exe 创建快捷方式,在“目标”最后加 ` -v6only=false`),否则游戏连不上。加了以后与 v0.4.0 相同(仅流量模式,DNS 用系统设置)。
启动时先花约 20 秒测三段的延迟、抖动、丢包:电脑→路由器、→运营商(traceroute 找到的第一个公网路由器)、→暴雪美东(`overwatch.blizzard.com` 的 TLS 握手计时)。然后告诉你问题在哪一段:
- 家里 Wi-Fi/路由器不稳:WARP 帮不上,给出插网线、5GHz、关定位、关网卡节能、开 SQM 等做法;
- 运营商接入不稳:WARP 也帮不上;
- 运营商到暴雪的线路不稳:WARP 可能有帮助。

修复:本机没有 IPv6 时,以前经隧道的 IPv6 实际用不上(Windows 只给程序 IPv4 地址),现在可以正常使用。
用 `-v6only=false` 运行(守望先锋需要)时,连上 WARP 后会**经 WARP 再测一次暴雪**,与直连并排对比,直接告诉你开加速更稳还是直连更好。
155 changes: 155 additions & 0 deletions diag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package main

import (
"fmt"
"sort"
"strings"
"time"
)

// series holds one diagnostic target's samples: a reply time per answered
// probe, and how many probes were sent.
type series struct {
name string
rtts []time.Duration
sent int
}

type stats struct {
avg, jitter, max time.Duration
loss float64 // 0..1
spikes int // replies more than 50 ms above the median
}

func (s *series) stats() stats {
var st stats
if s.sent == 0 {
return st
}
st.loss = 1 - float64(len(s.rtts))/float64(s.sent)
if len(s.rtts) == 0 {
return st
}
var sum, dsum time.Duration
for i, r := range s.rtts {
sum += r
st.max = max(st.max, r)
if i > 0 {
d := r - s.rtts[i-1]
if d < 0 {
d = -d
}
dsum += d
}
}
st.avg = sum / time.Duration(len(s.rtts))
if len(s.rtts) > 1 {
st.jitter = dsum / time.Duration(len(s.rtts)-1)
}
sorted := append([]time.Duration(nil), s.rtts...)
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
median := sorted[len(sorted)/2]
for _, r := range s.rtts {
if r > median+50*time.Millisecond {
st.spikes++
}
}
return st
}

// unstable reports whether a hop shows the kind of trouble a game feels: lost
// packets, jitter, or repeated spikes. Routers answer pings at low priority,
// so the intermediate ISP hop gets a looser bar than the endpoints.
func (st stats) unstable(lenient bool) bool {
lossBar, jitterBar := 0.03, 8*time.Millisecond
if lenient {
lossBar, jitterBar = 0.10, 15*time.Millisecond
}
return st.loss >= lossBar || st.jitter > jitterBar || st.spikes >= 2
}

func (st stats) String() string {
return fmt.Sprintf("延迟 %3d ms 抖动 %2d ms 最高 %3d ms 丢包 %2.0f%%",
st.avg.Milliseconds(), st.jitter.Milliseconds(), st.max.Milliseconds(), st.loss*100)
}

// verdict turns the three hops into advice. gw is the router, isp the first
// public hop, target a Blizzard server; any may be nil when it could not be
// found or measured.
func verdict(gw, isp, target *series, wifi bool) []string {
// A hop that never answered is unmeasured, not broken: plenty of
// routers ignore pings altogether.
measured := func(s *series) bool { return s != nil && len(s.rtts) > 0 }
bad := func(s *series, lenient bool) bool { return measured(s) && s.stats().unstable(lenient) }
link := "网线/路由器"
if wifi {
link = "Wi-Fi"
}
switch {
case bad(gw, false):
lines := []string{"问题在家里:电脑到路由器这一段就不稳(" + link + ")。WARP 加速帮不上,先解决这一段:"}
if wifi {
lines = append(lines,
" · 能插网线就插网线,效果最明显;不能的话连 5GHz、靠近路由器",
" · Windows 设置 → 隐私和安全性 → 定位,关闭(定位会让网卡定期扫描 Wi-Fi,造成延迟尖峰)",
" · 设备管理器 → 无线网卡 → 电源管理,取消“允许计算机关闭此设备以节约电源”")
} else {
lines = append(lines, " · 换一根网线或换个路由器口试试;重启路由器")
}
return append(lines, " · 家里有人在下载、看视频、传文件时最明显:路由器有 SQM/QoS 就打开")
case bad(target, false) && bad(isp, true):
return []string{
"问题在运营商接入:路由器正常,但出了家门到运营商的第一段就不稳。",
" · WARP 加速的流量也要先经过这一段,帮不上;可以打电话让运营商查线路,或换运营商",
}
case bad(target, false):
return []string{
"问题在运营商到暴雪的线路:家里和运营商接入都正常,往后才不稳。",
" · 这种情况 WARP 加速可能有帮助:守望先锋只有 IPv4,请用 -v6only=false 运行,开和不开各打几局对比",
}
case !measured(target):
return []string{"家里网络和运营商接入正常;暴雪服务器没测到,无法判断后面的线路。"}
default:
return []string{
"三段都正常,网络本身没有明显问题。",
" · 游戏里仍然卡,可能是游戏服务器或本机(后台下载、同步、杀毒扫描)",
" · 不一定需要加速;想试试 WARP,守望先锋请用 -v6only=false",
}
}
}

// compareVerdict weighs the direct route against WARP the way a game would:
// loss and jitter first, then latency.
func compareVerdict(direct, warp stats) string {
score := func(st stats) time.Duration {
return st.avg + 2*st.jitter + time.Duration(st.loss*1000)*time.Millisecond
}
d, w := score(direct), score(warp)
switch {
case w+5*time.Millisecond < d:
return "WARP 更稳,建议开着加速打游戏。"
case d+5*time.Millisecond < w:
return "直连更好:WARP 绕远了。建议关掉加速直接玩。"
default:
return "两者差不多,开不开都行;以游戏里的网络图(Ctrl+Shift+N)为准。"
}
}

func report(hops []*series, gw, isp, target *series, wifi bool) string {
var b strings.Builder
for _, s := range hops {
if s == nil {
continue
}
if s.sent == 0 || len(s.rtts) == 0 {
fmt.Fprintf(&b, " %-26s 没有回应\n", s.name)
continue
}
fmt.Fprintf(&b, " %-26s %s\n", s.name, s.stats())
}
b.WriteString("\n")
for _, l := range verdict(gw, isp, target, wifi) {
b.WriteString(l + "\n")
}
return b.String()
}
75 changes: 75 additions & 0 deletions diag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"strings"
"testing"
"time"
)

func steady(name string, ms int, n int) *series {
s := &series{name: name, sent: n}
for i := 0; i < n; i++ {
s.rtts = append(s.rtts, time.Duration(ms)*time.Millisecond)
}
return s
}

func lossy(name string, ms, n, lost int) *series {
s := steady(name, ms, n-lost)
s.sent = n
return s
}

func jittery(name string, n int) *series {
s := &series{name: name, sent: n}
for i := 0; i < n; i++ {
s.rtts = append(s.rtts, time.Duration(2+(i%2)*30)*time.Millisecond)
}
return s
}

func TestStats(t *testing.T) {
st := lossy("x", 10, 20, 2).stats()
if st.avg != 10*time.Millisecond || st.jitter != 0 || st.loss < 0.099 || st.loss > 0.101 {
t.Fatalf("%+v", st)
}
if j := jittery("x", 10).stats().jitter; j != 30*time.Millisecond {
t.Fatalf("jitter %v", j)
}
s := steady("x", 10, 20)
s.rtts[5], s.rtts[9] = 200*time.Millisecond, 300*time.Millisecond
if st := s.stats(); st.spikes != 2 || st.max != 300*time.Millisecond {
t.Fatalf("%+v", st)
}
}

func TestVerdict(t *testing.T) {
ok, gwBad, ispBad, bliz, blizBad := steady("gw", 2, 60), jittery("gw", 60), lossy("isp", 8, 60, 12), steady("b", 70, 30), lossy("b", 70, 30, 3)
cases := []struct {
gw, isp, target *series
wifi bool
want string
}{
{gwBad, ok, blizBad, true, "Wi-Fi"},
{ok, ispBad, blizBad, false, "运营商接入"},
{ok, ok, blizBad, false, "WARP 加速可能有帮助"},
{ok, ispBad, bliz, false, "三段都正常"}, // a hop that rate-limits pings alone is not a fault
{ok, nil, nil, false, "没测到"},
{&series{name: "gw", sent: 60}, ok, bliz, false, "三段都正常"}, // silent router is unmeasured
}
for i, c := range cases {
got := strings.Join(verdict(c.gw, c.isp, c.target, c.wifi), "\n")
if !strings.Contains(got, c.want) {
t.Errorf("case %d: want %q in\n%s", i, c.want, got)
}
}
}

func TestCompareVerdict(t *testing.T) {
good, bad := steady("", 20, 10).stats(), lossy("", 20, 10, 2).stats()
if !strings.Contains(compareVerdict(bad, good), "WARP 更稳") ||
!strings.Contains(compareVerdict(good, bad), "直连更好") ||
!strings.Contains(compareVerdict(good, good), "差不多") {
t.Fatal("compare verdict")
}
}
Loading
Loading