如何让Go HTTP Client每次请求轮流用不同代理且复用TCP连接
解决方案:Go HTTP Client代理轮询+TCP连接复用
问题原因
http.Transport的连接池是基于目标主机+代理+TLS配置的组合键缓存连接的。原代码中,第一次请求调用Proxy函数选中代理后,后续同主机的请求会直接复用该代理的连接,不会再次触发Proxy函数,导致轮询策略失效。关闭DisableKeepAlives虽然能强制每次请求触发Proxy,但会完全失去TCP连接复用的优势,增加握手开销。
优雅解决方案:自定义RoundTripper
通过实现自定义RoundTripper,手动控制代理轮选逻辑,同时为每个代理维护独立的连接池,既实现轮询又保留连接复用。
完整代码示例
package main import ( "fmt" "net/http" "net/url" "sync" ) var client *http.Client func main() { roundRobin := NewRoundRobinProxy( "http://myproxy1:8888", "http://myproxy2:8888", "http://myproxy3:8888") // 基础Transport配置,保留连接复用相关参数 baseTransport := &http.Transport{ MaxConnsPerHost: 10, DisableKeepAlives: false, } // 使用自定义RoundTripper替换默认Transport client = &http.Client{ Transport: &RoundRobinTripper{ baseTransport: baseTransport, proxy: roundRobin, proxyTransports: sync.Map{}, }, } // 发起测试请求 sendReq("https://www.binance.com") sendReq("https://www.binance.com") sendReq("https://www.binance.com") sendReq("https://www.binance.com") } func sendReq(urlStr string) { req, err := http.NewRequest("GET", urlStr, nil) if err != nil { fmt.Printf("create request error: %v\n", err) return } resp, err := client.Do(req) if err != nil { fmt.Printf("send request error: %v\n", err) return } defer resp.Body.Close() fmt.Println("got resp from ", urlStr) } // RoundRobinProxy 实现轮询代理逻辑,保证并发安全 type RoundRobinProxy struct { urls []*url.URL cursor int mu sync.Mutex } func NewRoundRobinProxy(urls ...string) *RoundRobinProxy { p := &RoundRobinProxy{cursor: 0} for _, v := range urls { u, err := url.Parse(v) if err != nil { fmt.Printf("parse proxy url error: %v\n", err) continue } p.urls = append(p.urls, u) } return p } // Next 轮选下一个代理地址 func (p *RoundRobinProxy) Next() *url.URL { p.mu.Lock() defer p.mu.Unlock() if len(p.urls) == 0 { return nil } u := p.urls[p.cursor] p.cursor = (p.cursor + 1) % len(p.urls) fmt.Printf("selected proxy: %s\n", u.String()) return u } // RoundRobinTripper 自定义RoundTripper,实现代理轮询+连接复用 type RoundRobinTripper struct { baseTransport *http.Transport proxy *RoundRobinProxy proxyTransports sync.Map // 缓存每个代理对应的Transport实例 } func (rt *RoundRobinTripper) RoundTrip(req *http.Request) (*http.Response, error) { proxyURL := rt.proxy.Next() if proxyURL == nil { return nil, fmt.Errorf("no available proxy") } proxyKey := proxyURL.String() // 获取或创建当前代理对应的Transport实例 transport, ok := rt.proxyTransports.Load(proxyKey) if !ok { // 复制基础Transport配置,仅修改Proxy字段 newTransport := rt.baseTransport.Clone() newTransport.Proxy = func(*http.Request) (*url.URL, error) { return proxyURL, nil } transport, _ = rt.proxyTransports.LoadOrStore(proxyKey, newTransport) } // 使用对应代理的Transport执行请求 return transport.(*http.Transport).RoundTrip(req) }
方案优势
- 连接复用保留:每个代理对应独立的
http.Transport实例,各自维护TCP连接池,避免重复握手 - 轮询策略生效:每次请求都会主动轮选代理,不受连接池缓存影响
- 并发安全:通过
sync.Mutex和sync.Map保证多goroutine下的安全运行 - 配置复用:基于基础Transport的配置创建代理专属Transport,无需重复设置参数
内容的提问来源于stack exchange,提问作者Владимир Савостин
相关产品推荐
相关产品推荐

