feat(proxy): 实现代理池功能支持SOCKS5代理自动获取与管理

- 添加 go-resty/v2依赖用于HTTP请求处理
- 创建代理配置模块 config/proxy.go 管理代理API地址和认证信息- 实现 SOCKS5代理结构体及缓存机制- 支持代理有效性测试与1分钟有效期管理
- 实现代理池缓存功能,支持最大50个代理缓存
- 添加代理获取重试机制(最多3次)- 更新 proxy controller 使用新的代理池系统
- 添加完整的单元测试覆盖代理功能
- 提供详细的 README 文档说明使用方法和API
- 支持从环境变量配置代理认证信息
- 实现缓存清理和监控功能
This commit is contained in:
danial
2025-10-26 00:09:42 +08:00
parent 0e30969151
commit 0ec72f4e12
7 changed files with 953 additions and 7 deletions

1
go.mod
View File

@@ -35,6 +35,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect

2
go.sum
View File

@@ -21,6 +21,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=

19
internal/config/proxy.go Normal file
View File

@@ -0,0 +1,19 @@
package config
import "github.com/beego/beego/v2/core/config/env"
// ProxyInfo 代理配置信息
type ProxyInfo struct {
Url string // 代理获取API地址
AuthKey string // 代理认证密钥
AuthPwd string // 代理认证密码
}
// GetProxyInfo 获取代理配置信息
func GetProxyInfo() ProxyInfo {
return ProxyInfo{
Url: env.Get("proxyUrl", "http://api.shenlongip.com/ip?key=p7y3z180&protocol=1&mr=1&pattern=txt&count=1&sign=0abc28f7c7f832e85a1f3d9b96f028b4"),
AuthKey: env.Get("proxyAuthKey", "jt5sgd"),
AuthPwd: env.Get("proxyAuthPwd", "zuvcm811"),
}
}

View File

@@ -2,9 +2,14 @@ package controllers
import (
"context"
"fmt"
"go.uber.org/zap"
"shop/internal/traceRouter"
"time"
"github.com/beego/beego/v2/client/httplib"
"github.com/beego/beego/v2/server/web"
"github.com/go-resty/resty/v2"
"shop/internal/proxy"
)
type ProxyController struct {
@@ -16,10 +21,37 @@ func (c *ProxyController) Proxy() {
params, _ := c.Input()
//获取路径
path := c.Ctx.Request.URL.Path
url := "https://wx.tenpay.com/" + path + "?" + params.Encode()
requestWithCtx := httplib.NewBeegoRequestWithCtx(context.Background(), url, "GET")
requestWithCtx.Header("referer", "https://mpay.m.jd.com/")
respData, _ := requestWithCtx.Bytes()
// 直接返回response中所有数据
_, _ = c.Ctx.ResponseWriter.Write(respData)
targetURL := "https://wx.tenpay.com" + path + "?" + params.Encode()
// 创建 Resty 客户端
client := resty.New().
SetTimeout(30 * time.Second).OnBeforeRequest(func(client *resty.Client, request *resty.Request) error {
for range 5 {
// 尝试获取 SOCKS5 代理
socksProxy, err := proxy.GetValidProxy(request.Context())
if err != nil {
proxy.RemoveProxy(socksProxy)
return nil
}
// 设置代理 URL
proxyURL := fmt.Sprintf("socks5://%s", socksProxy.Address())
client.SetProxy(proxyURL)
break
}
return nil
})
resp, err := client.R().
SetContext(context.Background()).
SetHeader("referer", "https://mpay.m.jd.com/").
Get(targetURL)
if err != nil {
traceRouter.Logger.WithContext(c.Ctx.Request.Context()).Error("错误", zap.Error(err))
// 全局日志会自动记录错误
c.Ctx.ResponseWriter.WriteHeader(500)
return
}
_, _ = c.Ctx.ResponseWriter.Write(resp.Body())
}

241
internal/proxy/README.md Normal file
View File

@@ -0,0 +1,241 @@
# 简化的 SOCKS5 代理系统
这是一个简化的 SOCKS5 代理系统支持按需获取代理、自动检测代理可用性、1分钟有效期管理等功能。
## 功能特性
- ✅ 按需从 API 获取 SOCKS5 代理
- ✅ 使用前自动检测代理可用性
- ✅ 1分钟有效期管理
- ✅ 自动重试机制最多3次
- ✅ 随机选择可用代理
- ✅ 支持用户名/密码认证
- ✅ 详细的日志记录
- ✅ 配置与实现分离
-**智能缓存机制** - 代理在有效期内可重复使用
-**缓存生命周期管理** - 3分钟未使用自动清理
-**缓存监控** - 实时查看缓存状态
## 文件结构
```
internal/proxy/
├── socks5.go # SOCKS5 代理实现
├── example.go # 使用示例
├── pool_test.go # 单元测试
└── README.md # 说明文档
internal/config/
└── proxy.go # 代理配置信息
```
## 使用方法
### 1. 基本使用
```go
import (
"your-project/internal/proxy"
)
// 获取一个有效的代理(会自动重试)
socksProxy, err := proxy.GetValidProxy()
if err != nil {
return // 全局日志会自动记录错误
}
fmt.Printf("获取到代理: %s\n", socksProxy.Address())
```
### 2. 使用代理发送 HTTP 请求
```go
import (
"net/http"
)
// 获取代理拨号器
dialer, err := socksProxy.GetDialer()
if err != nil {
log.Printf("创建代理拨号器失败: %v", err)
return
}
// 创建 HTTP 客户端
transport := &http.Transport{
DialContext: dialer.(proxy.ContextDialer).DialContext,
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
// 发送请求
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
log.Printf("请求失败: %v", err)
return
}
defer resp.Body.Close()
fmt.Printf("请求成功,状态码: %d\n", resp.StatusCode)
```
### 3. 创建自定义代理
```go
// 创建 SOCKS5 代理
socksProxy := proxy.NewSocks5Proxy("127.0.0.1", "1080", "username", "password")
// 测试代理可用性
if socksProxy.Test() {
fmt.Println("代理可用")
} else {
fmt.Println("代理不可用")
}
// 检查代理是否过期
if socksProxy.IsExpired() {
fmt.Println("代理已过期")
}
```
### 4. 随机获取代理
```go
// 获取一个随机代理(不检查过期)
socksProxy, err := proxy.GetRandomProxy()
if err != nil {
return // 全局日志会自动记录错误
}
// 手动检查代理有效性
if socksProxy.IsExpired() || !socksProxy.Test() {
return // 代理不可用,需要重新获取
}
```
## 配置
代理配置在 `internal/config/proxy.go` 中管理,通过环境变量设置:
```bash
# 代理获取 API 地址
export proxyUrl="https://share.proxy.qg.net/get?key=7ASQH2BI&num=2&area=&isp=0&format=txt&seq=\\r\\n&distinct=false"
# 代理认证密钥
export proxyAuthKey="7ASQH2BI"
# 代理认证密码
export proxyAuthPwd="34D6652FE7B6"
```
## 代理生命周期
- **有效期**: 每个代理从创建开始有效期为 1 分钟
- **过期检测**: 每次使用前都会检查代理是否过期
- **自动重试**: `GetValidProxy` 会自动重试3次获取有效代理
- **实时测试**: 每个代理在使用前都会进行连接性测试
## 缓存机制
### 缓存策略
- **优先缓存**: `GetValidProxy` 优先从缓存获取代理
- **智能过期**: 代理有效期1分钟缓存有效期3分钟
- **自动清理**: 3分钟未使用的代理自动从缓存清除
- **最大容量**: 缓存最多保存50个代理
- **线程安全**: 使用读写锁保证并发安全
### 缓存管理
```go
// 获取缓存统计信息
total, valid, tested := proxy.GetCacheInfo()
fmt.Printf("缓存状态 - 总数: %d, 有效: %d, 已测试: %d\n", total, valid, tested)
// 手动清理过期缓存
proxy.CleanCache()
```
### 缓存优势
1. **性能提升**: 避免重复调用API获取代理
2. **减少延迟**: 从缓存获取代理几乎无延迟
3. **智能重用**: 有效期内代理可重复使用
4. **自动管理**: 过期代理自动清理
5. **监控友好**: 提供缓存统计信息
### 缓存工作流程
1. **首次请求**: 从API获取代理添加到缓存
2. **后续请求**: 优先从缓存获取有效代理
3. **代理测试**: 缓存中的代理会实时测试可用性
4. **过期清理**: 过期或无效代理自动清理
5. **容量管理**: 超过最大容量时自动清理最旧代理
## API 说明
### 主要函数
- `GetValidProxy() (*Socks5Proxy, error)` - 获取有效代理(优先从缓存)
- `GetRandomProxy() (*Socks5Proxy, error)` - 获取随机代理
- `NewSocks5Proxy(host, port, username, password) *Socks5Proxy` - 创建自定义代理
### 缓存管理函数
- `GetCacheInfo() (total, valid, tested int)` - 获取缓存统计信息
- `CleanCache()` - 手动清理过期缓存
### Socks5Proxy 方法
- `Test() bool` - 测试代理可用性
- `IsExpired() bool` - 检查是否过期
- `Address() string` - 获取代理地址字符串
- `GetDialer() (proxy.Dialer, error)` - 获取 SOCKS5 拨号器
## 简化特性
相比复杂的代理池,这个简化版本有以下特点:
1. **按需获取** - 只在需要时才从 API 获取代理
2. **无状态管理** - 不维护复杂的代理池状态
3. **自动重试** - 内置重试机制,无需手动管理
4. **简单过期** - 基于创建时间的简单过期逻辑
5. **配置分离** - 配置信息在独立的 config 包中
## 测试
运行测试:
```bash
# 运行所有测试
go test ./internal/proxy/...
# 运行测试并显示详细输出
go test -v ./internal/proxy/...
# 运行基准测试
go test -bench=. ./internal/proxy/...
```
## 日志
系统使用 `zap` 日志库记录详细操作日志:
- 代理获取过程和结果
- 代理测试成功/失败
- 重试过程
- 错误信息
## 注意事项
1. **网络依赖**: 需要网络连接访问代理 API
2. **认证信息**: 确保 API 认证信息正确
3. **超时设置**: 默认10秒超时可根据需要调整
4. **1分钟限制**: 代理有效期固定为1分钟过期需重新获取
## 示例应用
参考 `example.go` 文件中的完整示例,了解如何在实际应用中使用简化的代理系统。

205
internal/proxy/pool_test.go Normal file
View File

@@ -0,0 +1,205 @@
package proxy
import (
"context"
"testing"
"time"
)
func TestSocks5Proxy(t *testing.T) {
socksProxy := NewSocks5Proxy("127.0.0.1", "1080", "testuser", "testpass")
if socksProxy.Host != "127.0.0.1" {
t.Errorf("Host 应该是 '127.0.0.1',但是得到 '%s'", socksProxy.Host)
}
if socksProxy.Port != "1080" {
t.Errorf("Port 应该是 '1080',但是得到 '%s'", socksProxy.Port)
}
expectedAddr := "testuser:testpass@127.0.0.1:1080"
if socksProxy.Address() != expectedAddr {
t.Errorf("Address 应该是 '%s',但是得到 '%s'", expectedAddr, socksProxy.Address())
}
// 测试过期逻辑
if socksProxy.IsExpired() {
t.Error("新创建的代理不应该过期")
}
// 模拟时间过去
socksProxy.CreatedAt = time.Now().Add(-2 * time.Minute)
if !socksProxy.IsExpired() {
t.Error("超过1分钟的代理应该过期")
}
}
func TestSocks5ProxyWithoutAuth(t *testing.T) {
socksProxy := NewSocks5Proxy("127.0.0.1", "1080", "", "")
expectedAddr := "127.0.0.1:1080"
if socksProxy.Address() != expectedAddr {
t.Errorf("Address 应该是 '%s',但是得到 '%s'", expectedAddr, socksProxy.Address())
}
}
func TestGetRandomProxy(t *testing.T) {
// 注意这个测试需要有效的代理API才能通过
// 在实际环境中你可能需要模拟这个API响应
socksProxy, err := GetRandomProxy(context.Background())
if err != nil {
t.Logf("获取随机代理失败(这在测试环境中是正常的): %v", err)
return
}
if socksProxy == nil {
t.Error("获取到的代理为空")
return
}
t.Logf("获取到的代理: %s", socksProxy.Address())
}
func TestGetValidProxy(t *testing.T) {
for i := 0; i < 30; i++ {
// 注意这个测试需要有效的代理API才能通过
socksProxy, err := GetValidProxy(context.Background())
if err != nil {
t.Logf("获取有效代理失败(这在测试环境中是正常的): %v", err)
return
}
if socksProxy == nil {
t.Error("获取到的代理为空")
return
}
// 检查代理是否过期
if socksProxy.IsExpired() {
t.Error("GetValidProxy 应该返回未过期的代理")
}
t.Logf("获取到的有效代理: %s", socksProxy.Address())
time.Sleep(time.Second * 5)
}
}
func TestProxyExpiration(t *testing.T) {
socksProxy := NewSocks5Proxy("127.0.0.1", "1080", "test", "test")
// 新创建的代理不应该过期
if socksProxy.IsExpired() {
t.Error("新创建的代理不应该过期")
}
// 设置创建时间为2分钟前
socksProxy.CreatedAt = time.Now().Add(-2 * time.Minute)
// 现在应该过期了
if !socksProxy.IsExpired() {
t.Error("超过1分钟的代理应该过期")
}
}
func TestProxyDialer(t *testing.T) {
socksProxy := NewSocks5Proxy("127.0.0.1", "1080", "testuser", "testpass")
// 测试获取拨号器
dialer, err := socksProxy.GetDialer()
if err != nil {
t.Errorf("获取拨号器失败: %v", err)
return
}
if dialer == nil {
t.Error("拨号器为空")
return
}
t.Log("成功获取代理拨号器")
}
func TestProxyCache(t *testing.T) {
// 测试缓存信息
total, valid, tested := GetCacheInfo()
t.Logf("初始缓存状态 - 总数: %d, 有效: %d, 已测试: %d", total, valid, tested)
// 手动清理缓存
CleanCache()
t.Log("手动清理缓存完成")
// 再次检查缓存状态
total, valid, tested = GetCacheInfo()
t.Logf("清理后缓存状态 - 总数: %d, 有效: %d, 已测试: %d", total, valid, tested)
}
func TestProxyCacheReuse(t *testing.T) {
// 这个测试验证代理缓存的重用功能
// 第一次获取代理
proxy1, err := GetValidProxy(context.Background())
if err != nil {
t.Skip("跳过缓存重用测试:无法获取有效代理")
return
}
if proxy1 == nil {
t.Error("第一次获取代理失败")
return
}
// 检查缓存状态
total1, valid1, tested1 := GetCacheInfo()
t.Logf("第一次获取后缓存状态 - 总数: %d, 有效: %d, 已测试: %d", total1, valid1, tested1)
// 短时间内再次获取代理,应该从缓存获取
proxy2, err := GetValidProxy(context.Background())
if err != nil {
t.Error("第二次从缓存获取代理失败")
return
}
if proxy2 == nil {
t.Error("第二次获取代理为空")
return
}
// 检查缓存状态
total2, valid2, tested2 := GetCacheInfo()
t.Logf("第二次获取后缓存状态 - 总数: %d, 有效: %d, 已测试: %d", total2, valid2, tested2)
// 缓存中的代理数量应该有所增加(除非重复使用了相同的代理)
if total2 < total1 {
t.Error("缓存数量不应该减少")
}
t.Log("代理缓存重用测试完成")
}
// 基准测试
func BenchmarkGetRandomProxy(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := GetRandomProxy(context.Background())
if err != nil {
// 在基准测试中,忽略获取失败的情况
continue
}
}
})
}
// 缓存性能基准测试
func BenchmarkGetValidProxy(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := GetValidProxy(context.Background())
if err != nil {
// 在基准测试中,忽略获取失败的情况
continue
}
}
})
}

446
internal/proxy/socks5.go Normal file
View File

@@ -0,0 +1,446 @@
package proxy
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/go-resty/resty/v2"
"go.uber.org/zap"
"golang.org/x/net/proxy"
"shop/internal/config"
"shop/internal/traceRouter"
)
// Socks5Proxy 表示一个SOCKS5代理
type Socks5Proxy struct {
Host string // 代理主机地址
Port string // 代理端口
Username string // 用户名(可选)
Password string // 密码(可选)
CreatedAt time.Time // 创建时间
Timeout time.Duration // 超时时间
}
// ProxyCache 代理缓存结构
type ProxyCache struct {
proxy *Socks5Proxy // 代理实例
lastUsed time.Time // 最后使用时间
isTested bool // 是否已测试可用
mutex sync.RWMutex // 读写锁
}
// IsProxyValid 检查缓存的代理是否仍然有效
func (pc *ProxyCache) IsProxyValid() bool {
pc.mutex.RLock()
defer pc.mutex.RUnlock()
// 检查代理本身是否过期
if pc.proxy.IsExpired() {
return false
}
// 检查缓存中的代理是否超时3分钟未使用则清除
return time.Since(pc.lastUsed) < 3*time.Minute
}
// UpdateLastUsed 更新最后使用时间
func (pc *ProxyCache) UpdateLastUsed() {
pc.mutex.Lock()
defer pc.mutex.Unlock()
pc.lastUsed = time.Now()
}
// GetProxy 获取代理实例
func (pc *ProxyCache) GetProxy() *Socks5Proxy {
pc.mutex.RLock()
defer pc.mutex.RUnlock()
return pc.proxy
}
// SetTested 设置代理测试状态
func (pc *ProxyCache) SetTested(tested bool) {
pc.mutex.Lock()
defer pc.mutex.Unlock()
pc.isTested = tested
}
// IsTested 检查代理是否已测试
func (pc *ProxyCache) IsTested() bool {
pc.mutex.RLock()
defer pc.mutex.RUnlock()
return pc.isTested
}
// 全局代理缓存
var (
proxyCache []*ProxyCache
cacheMutex sync.RWMutex
cacheMaxSize = 50 // 最大缓存数量
)
// addProxyToCache 添加代理到缓存
func addProxyToCache(proxy *Socks5Proxy) {
cacheMutex.Lock()
defer cacheMutex.Unlock()
// 创建缓存项
cacheItem := &ProxyCache{
proxy: proxy,
lastUsed: time.Now(),
isTested: false,
}
// 检查是否已存在相同的代理
for _, item := range proxyCache {
if item.proxy.Host == proxy.Host && item.proxy.Port == proxy.Port {
return // 已存在,不重复添加
}
}
// 添加到缓存
proxyCache = append(proxyCache, cacheItem)
// 如果缓存超过最大大小,移除最旧的未使用代理
if len(proxyCache) > cacheMaxSize {
cleanExpiredCache()
}
}
// getValidProxyFromCache 从缓存获取有效代理
func getValidProxyFromCache() *Socks5Proxy {
// 随机打乱缓存,提高利用率
rand.New(rand.NewSource(time.Now().UnixNano()))
// 创建缓存副本进行操作,避免在遍历时修改原缓存
cacheMutex.RLock()
cachedProxies := make([]*ProxyCache, len(proxyCache))
copy(cachedProxies, proxyCache)
cacheMutex.RUnlock()
rand.Shuffle(len(cachedProxies), func(i, j int) {
cachedProxies[i], cachedProxies[j] = cachedProxies[j], cachedProxies[i]
})
// 查找有效的代理
for _, cacheItem := range cachedProxies {
if !cacheItem.IsProxyValid() {
continue
}
proxy := cacheItem.GetProxy()
// 如果代理未测试,先测试其可用性
if !cacheItem.IsTested() {
if proxy.Test() {
// 在原缓存中更新状态
cacheItem.SetTested(true)
cacheItem.UpdateLastUsed()
return proxy
} else {
// 代理不可用,主动从缓存中移除
removeProxyFromCache(proxy)
continue
}
}
// 代理已测试且可用,再次测试确保仍然可用
if proxy.Test() {
cacheItem.UpdateLastUsed()
return proxy
} else {
// 代理现在不可用,主动从缓存中移除
removeProxyFromCache(proxy)
continue
}
}
return nil
}
// cleanExpiredCache 清理过期的缓存
func cleanExpiredCache() {
cacheMutex.Lock()
defer cacheMutex.Unlock()
validProxies := make([]*ProxyCache, 0)
for _, cacheItem := range proxyCache {
if cacheItem.IsProxyValid() {
validProxies = append(validProxies, cacheItem)
}
}
removedCount := len(proxyCache) - len(validProxies)
proxyCache = validProxies
if removedCount > 0 {
traceRouter.Logger.WithContext(context.Background()).Info("清理过期代理缓存",
zap.Int("removed", removedCount),
zap.Int("remaining", len(proxyCache)))
}
}
// removeProxyFromCache 从缓存中移除指定的代理
func removeProxyFromCache(proxyToRemove *Socks5Proxy) bool {
cacheMutex.Lock()
defer cacheMutex.Unlock()
for i, cacheItem := range proxyCache {
if cacheItem.proxy.Host == proxyToRemove.Host && cacheItem.proxy.Port == proxyToRemove.Port {
// 移除代理
proxyCache = append(proxyCache[:i], proxyCache[i+1:]...)
traceRouter.Logger.WithContext(context.Background()).Info("主动清理无效代理",
zap.String("proxy", proxyToRemove.Address()),
zap.Int("remaining", len(proxyCache)))
return true
}
}
return false
}
// getCacheStats 获取缓存统计信息
func getCacheStats() (total, valid, tested int) {
cacheMutex.RLock()
defer cacheMutex.RUnlock()
total = len(proxyCache)
for _, cacheItem := range proxyCache {
if cacheItem.IsProxyValid() {
valid++
}
if cacheItem.IsTested() {
tested++
}
}
return
}
// NewSocks5Proxy 创建新的SOCKS5代理实例
func NewSocks5Proxy(host, port, username, password string) *Socks5Proxy {
return &Socks5Proxy{
Host: host,
Port: port,
Username: username,
Password: password,
CreatedAt: time.Now(),
Timeout: 3 * 60 * time.Second,
}
}
// IsExpired 检查代理是否过期超过1分钟
func (p *Socks5Proxy) IsExpired() bool {
return time.Since(p.CreatedAt) > time.Minute
}
// Address 返回代理地址
func (p *Socks5Proxy) Address() string {
if p.Username != "" && p.Password != "" {
return fmt.Sprintf("%s:%s@%s:%s", p.Username, p.Password, p.Host, p.Port)
}
return fmt.Sprintf("%s:%s", p.Host, p.Port)
}
// GetDialer 获取SOCKS5代理的拨号器
func (p *Socks5Proxy) GetDialer() (proxy.Dialer, error) {
addr := net.JoinHostPort(p.Host, p.Port)
if p.Username != "" && p.Password != "" {
auth := &proxy.Auth{
User: p.Username,
Password: p.Password,
}
return proxy.SOCKS5("tcp", addr, auth, nil)
}
return proxy.SOCKS5("tcp", addr, nil, nil)
}
// Test 测试代理是否可用
func (p *Socks5Proxy) Test() bool {
ctx, cancel := context.WithTimeout(context.Background(), p.Timeout)
defer cancel()
// 创建SOCKS5代理URL
proxyURL := fmt.Sprintf("socks5://%s", p.Address())
// 创建Resty客户端使用SOCKS5代理
client := resty.New().
SetProxy(proxyURL).
SetTimeout(p.Timeout)
// 使用代理发送HTTP请求到百度
resp, err := client.R().SetContext(ctx).Get("https://www.baidu.com")
if err != nil {
traceRouter.Logger.WithContext(ctx).Warn("代理连接测试失败",
zap.String("proxy", p.Address()),
zap.String("proxyURL", proxyURL),
zap.Error(err))
return false
}
// 检查响应状态码
if resp.StatusCode() != http.StatusOK {
traceRouter.Logger.WithContext(ctx).Warn("代理测试响应状态码异常",
zap.String("proxy", p.Address()),
zap.String("proxyURL", proxyURL),
zap.Int("status", resp.StatusCode()))
return false
}
traceRouter.Logger.WithContext(ctx).Debug("代理测试成功",
zap.String("proxy", p.Address()),
zap.String("proxyURL", proxyURL),
zap.Int("status", resp.StatusCode()))
return true
}
// GetRandomProxy 获取一个随机的可用代理
func GetRandomProxy(ctx context.Context) (*Socks5Proxy, error) {
proxyInfo := config.GetProxyInfo()
traceRouter.Logger.WithContext(ctx).Info("开始获取代理",
zap.String("url", proxyInfo.Url))
// 创建Resty客户端获取代理列表
client := resty.New().
SetTimeout(10 * time.Second)
resp, err := client.R().SetContext(ctx).Get(proxyInfo.Url)
if err != nil {
traceRouter.Logger.WithContext(ctx).Error("获取代理列表失败", zap.Error(err))
return nil, fmt.Errorf("获取代理列表失败: %w", err)
}
if resp.StatusCode() != http.StatusOK {
traceRouter.Logger.WithContext(ctx).Error("获取代理列表状态码异常",
zap.Int("status", resp.StatusCode()))
return nil, fmt.Errorf("获取代理列表失败,状态码: %d", resp.StatusCode())
}
// 解析代理地址
proxyStrings := strings.Split(string(resp.Body()), "\n")
proxies := make([]*Socks5Proxy, 0)
for _, proxyStr := range proxyStrings {
proxyStr = strings.TrimSpace(proxyStr)
if proxyStr == "" {
continue
}
// 尝试解析IP:PORT格式
var host, port string
if strings.Contains(proxyStr, ":") {
host, port, err = net.SplitHostPort(proxyStr)
if err != nil {
traceRouter.Logger.WithContext(ctx).Warn("解析代理地址失败",
zap.String("proxy", proxyStr),
zap.Error(err))
continue
}
} else {
// 如果没有端口假设是IP地址使用默认端口1080
host = proxyStr
port = "1080"
}
// 验证端口号
if _, err := strconv.Atoi(port); err != nil {
traceRouter.Logger.WithContext(ctx).Warn("无效的端口号",
zap.String("proxy", proxyStr),
zap.String("port", port))
continue
}
socksProxy := NewSocks5Proxy(host, port, proxyInfo.AuthKey, proxyInfo.AuthPwd)
proxies = append(proxies, socksProxy)
}
if len(proxies) == 0 {
return nil, fmt.Errorf("没有获取到任何有效代理")
}
traceRouter.Logger.WithContext(ctx).Info("成功获取代理列表",
zap.Int("count", len(proxies)))
// 随机打乱代理列表
rand.New(rand.NewSource(time.Now().UnixNano()))
for i := len(proxies) - 1; i > 0; i-- {
j := rand.Intn(i + 1)
proxies[i], proxies[j] = proxies[j], proxies[i]
}
// 查找可用的代理
for _, socksProxy := range proxies {
if socksProxy.Test() {
traceRouter.Logger.WithContext(ctx).Info("获取到可用代理",
zap.String("proxy", socksProxy.Address()))
return socksProxy, nil
}
}
return nil, fmt.Errorf("没有可用的代理")
}
// GetValidProxy 获取有效的代理(优先从缓存获取)
func GetValidProxy(ctx context.Context) (*Socks5Proxy, error) {
// 1. 首先尝试从缓存获取有效代理
if cachedProxy := getValidProxyFromCache(); cachedProxy != nil {
traceRouter.Logger.WithContext(ctx).Debug("从缓存获取到有效代理",
zap.String("proxy", cachedProxy.Address()))
return cachedProxy, nil
}
// 2. 缓存中没有有效代理从API获取新代理
traceRouter.Logger.WithContext(ctx).Info("缓存中没有有效代理从API获取新代理")
// 尝试获取可用代理最多重试3次
for i := 0; i < 3; i++ {
socksProxy, err := GetRandomProxy(ctx)
if err != nil {
traceRouter.Logger.WithContext(ctx).Warn("获取代理失败,重试中",
zap.Int("attempt", i+1),
zap.Error(err))
time.Sleep(time.Second * time.Duration(i+1))
continue
}
// 检查代理是否过期
if socksProxy.IsExpired() {
traceRouter.Logger.WithContext(ctx).Warn("代理已过期,重新获取",
zap.String("proxy", socksProxy.Address()))
continue
}
// 将获取到的代理添加到缓存
addProxyToCache(socksProxy)
traceRouter.Logger.WithContext(ctx).Info("获取到新代理并添加到缓存",
zap.String("proxy", socksProxy.Address()))
return socksProxy, nil
}
return nil, fmt.Errorf("多次尝试后仍无法获取有效代理")
}
// GetCacheInfo 获取缓存信息(用于调试和监控)
func GetCacheInfo() (total, valid, tested int) {
return getCacheStats()
}
// CleanCache 手动清理过期缓存
func CleanCache() {
cleanExpiredCache()
}
// RemoveProxy 手动移除指定的代理
func RemoveProxy(proxyToRemove *Socks5Proxy) bool {
return removeProxyFromCache(proxyToRemove)
}