feat(proxy): 实现代理池功能支持SOCKS5代理自动获取与管理
- 添加 go-resty/v2依赖用于HTTP请求处理 - 创建代理配置模块 config/proxy.go 管理代理API地址和认证信息- 实现 SOCKS5代理结构体及缓存机制- 支持代理有效性测试与1分钟有效期管理 - 实现代理池缓存功能,支持最大50个代理缓存 - 添加代理获取重试机制(最多3次)- 更新 proxy controller 使用新的代理池系统 - 添加完整的单元测试覆盖代理功能 - 提供详细的 README 文档说明使用方法和API - 支持从环境变量配置代理认证信息 - 实现缓存清理和监控功能
This commit is contained in:
1
go.mod
1
go.mod
@@ -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
2
go.sum
@@ -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
19
internal/config/proxy.go
Normal 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"),
|
||||
}
|
||||
}
|
||||
@@ -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
241
internal/proxy/README.md
Normal 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
205
internal/proxy/pool_test.go
Normal 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
446
internal/proxy/socks5.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user