package eh // 连接复用测试 // // 曾经的 bug:httpClient() 每次请求都新建 http.Transport,用完丢弃。 // Transport 被丢弃后,它持有的 keep-alive 连接不会立刻释放(要等 // IdleConnTimeout = 90s),于是每发一个请求就留下一条僵尸连接。 // // 实测踩坑现场:解析大画廊时 91 条 Established 连接,而解析是串行的, // 正常应该只有 1 条。连接堆积会吃代理的连接配额,也让新请求变慢。 import ( "context" "fmt" "net" "net/http" "net/http/httptest" "strings" "sync" "sync/atomic" "testing" "time" ) // buildGalleryHTML 造一页画廊 HTML func buildGalleryHTML(gid string, from, to, total, screen int) string { var sb strings.Builder if screen == 0 { sb.WriteString(`[Test] - E-Hentai Galleries`) sb.WriteString(``) sb.WriteString(`
`) } for i := from; i <= to; i++ { sb.WriteString(fmt.Sprintf( `
`, i, gid, i, i, i)) } sb.WriteString(fmt.Sprintf(`Showing %d - %d of %d images`, from, to, total)) if screen == 0 { sb.WriteString(``) } return sb.String() } // 统计服务端实际接受的连接数 func TestTransportIsReused(t *testing.T) { var conns int64 srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) })) srv.Config.ConnState = func(c net.Conn, state http.ConnState) { if state == http.StateNew { atomic.AddInt64(&conns, 1) } } srv.Start() defer srv.Close() c := NewClient(10*time.Second, "", "off", "auto") const n = 20 for i := 0; i < n; i++ { if _, err := c.getText(context.Background(), srv.URL+"/x"); err != nil { t.Fatalf("第 %d 次请求失败: %v", i+1, err) } } got := atomic.LoadInt64(&conns) // 连接复用后,20 次请求应该只用 1 条连接(串行)。 // 修复前是每请求一条 = 20 条。 if got > 3 { t.Errorf("%d 次请求开了 %d 条连接 —— Transport 没复用,连接在泄漏", n, got) } t.Logf("%d 次请求共用了 %d 条连接", n, got) } // 并发请求也应该复用连接,而不是每个 goroutine 一条 func TestTransportReusedUnderConcurrency(t *testing.T) { var conns int64 srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) })) srv.Config.ConnState = func(c net.Conn, state http.ConnState) { if state == http.StateNew { atomic.AddInt64(&conns, 1) } } srv.Start() defer srv.Close() c := NewClient(10*time.Second, "", "off", "auto") var wg sync.WaitGroup for i := 0; i < 20; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 5; j++ { c.getText(context.Background(), srv.URL+"/x") } }() } wg.Wait() got := atomic.LoadInt64(&conns) // 100 次并发请求,连接数应该远小于 100 if got > 60 { t.Errorf("100 次并发请求开了 %d 条连接,复用不充分", got) } t.Logf("100 次并发请求共用了 %d 条连接", got) } // Close() 要真的能关掉空闲连接 func TestClientCloseReleasesConnections(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) })) defer srv.Close() c := NewClient(10*time.Second, "", "off", "auto") for i := 0; i < 5; i++ { c.getText(context.Background(), srv.URL+"/x") } c.mu.RLock() tr := c.transport c.mu.RUnlock() if tr == nil { t.Fatal("Transport 应该是复用的,不该为 nil") } c.Close() c.mu.RLock() still := c.transport c.mu.RUnlock() if still != nil { t.Error("Close() 后 transport 应被清空") } } // 改代理配置后 Transport 要重建(否则还是走老代理) func TestSetProxyRebuildsTransport(t *testing.T) { c := NewClient(5*time.Second, "", "off", "auto") // 先建一个 c.getText(context.Background(), "http://127.0.0.1:1/never") // 必然失败,只为触发建 Transport c.mu.RLock() first := c.transport c.mu.RUnlock() if first == nil { t.Fatal("应该已经建好 Transport") } // 改代理 c.SetProxy("http://127.0.0.1:7890", "http") c.mu.RLock() second := c.transport c.mu.RUnlock() if second == first { t.Error("改代理后 Transport 必须重建,否则还走旧配置") } if second == nil { t.Error("重建后不该是 nil") } } // 解析进度回调要真的被调用 func TestParseProgressCallback(t *testing.T) { const gid, tok, total = "4239999", "abcdef1234", 45 pages := map[int]string{} for p := 0; p < 3; p++ { from, to := p*20+1, p*20+20 if to > total { to = total } page := p pages[page] = buildGalleryHTML(gid, from, to, total, page) } mux := http.NewServeMux() mux.HandleFunc("/g/"+gid+"/"+tok+"/", func(w http.ResponseWriter, r *http.Request) { p := 0 if q := r.URL.Query().Get("p"); q != "" { fmt.Sscanf(q, "%d", &p) } h, ok := pages[p] if !ok { h = buildGalleryHTML(gid, 0, 0, total, 9) } w.Write([]byte(h)) }) srv := httptest.NewServer(mux) defer srv.Close() old := EHBase EHBase = srv.URL defer func() { EHBase = old }() c := NewClient(5*time.Second, "", "off", "auto") var got []int var maxDone int g, err := c.FetchGalleryPage(context.Background(), gid, tok, func(done, tot, page int) { got = append(got, done) if done > maxDone { maxDone = done } }) if err != nil { t.Fatalf("解析失败: %v", err) } if len(got) < 3 { t.Errorf("应该至少回调 3 次(翻 3 屏),实际 %d 次", len(got)) } if maxDone != total { t.Errorf("最后一次进度应达到 %d,实际 %d", total, maxDone) } if g.TotalPages != total { t.Errorf("总页数不对: %d", g.TotalPages) } t.Logf("进度回调 %d 次: %v", len(got), got) } // 传 nil 回调也不该 panic func TestParseProgressNil(t *testing.T) { const gid, tok, total = "4239998", "abcdef1234", 20 mux := http.NewServeMux() mux.HandleFunc("/g/"+gid+"/"+tok+"/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(buildGalleryHTML(gid, 1, 20, total, 0))) }) srv := httptest.NewServer(mux) defer srv.Close() old := EHBase EHBase = srv.URL defer func() { EHBase = old }() c := NewClient(5*time.Second, "", "off", "auto") if _, err := c.FetchGalleryPage(context.Background(), gid, tok, nil); err != nil { t.Errorf("nil 回调不该出错: %v", err) } }