1、暂存

This commit is contained in:
2025-10-14 19:58:59 +08:00
parent 556a32cb7c
commit 643eab3496
60 changed files with 5244 additions and 657 deletions

View File

@ -2,7 +2,6 @@ package excservice
import (
"context"
"crypto/tls"
"errors"
"fmt"
"go-admin/common/global"
@ -11,12 +10,8 @@ import (
"go-admin/models/commondto"
"go-admin/pkg/jsonhelper"
"go-admin/pkg/utility"
"go-admin/services/proxy"
"math/rand"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"strconv"
"strings"
"sync"
@ -26,32 +21,135 @@ import (
"github.com/bytedance/sonic"
log "github.com/go-admin-team/go-admin-core/logger"
"github.com/gorilla/websocket"
"golang.org/x/net/proxy"
)
type BinanceWebSocketManager struct {
ws *websocket.Conn
stopChannel chan struct{}
url string
/* 0-现货 1-合约 */
wsType int
apiKey string
apiSecret string
proxyType string
proxyAddress string
reconnect chan struct{}
isStopped bool // 标记 WebSocket 是否已主动停止
mu sync.Mutex // 用于控制并发访问 isStopped
cancelFunc context.CancelFunc
listenKey string // 新增字段
reconnecting atomic.Bool // 防止重复重连
ConnectTime time.Time // 当前连接建立时间
mu sync.Mutex
stopOnce sync.Once // 新增确保Stop只执行一次
ws *websocket.Conn
stopChannel chan struct{}
url string
wsType int
apiKey string
apiSecret string
proxyType string
proxyAddress string
reconnect chan struct{}
isStopped bool
cancelFunc context.CancelFunc
listenKey string
reconnecting atomic.Bool
ConnectTime time.Time
lastPingTime atomic.Value
forceReconnectTimer *time.Timer
}
// ... (省略中间代码)
// sendPing 使用锁来确保发送ping时的线程安全
func (wm *BinanceWebSocketManager) sendPing() error {
wm.mu.Lock()
defer wm.mu.Unlock()
if wm.ws == nil {
return errors.New("websocket connection is nil")
}
wm.lastPingTime.Store(time.Now())
return wm.ws.WriteMessage(websocket.PingMessage, []byte("ping"))
}
// Stop 优雅地停止WebSocket管理器
func (wm *BinanceWebSocketManager) Stop() {
wm.stopOnce.Do(func() {
wm.mu.Lock()
if wm.isStopped {
wm.mu.Unlock()
return
}
wm.isStopped = true
wm.mu.Unlock()
if wm.cancelFunc != nil {
wm.cancelFunc()
}
wm.closeConn() // 统一调用带锁的关闭方法
// 尝试关闭stopChannel如果已经关闭则忽略
select {
case <-wm.stopChannel:
default:
close(wm.stopChannel)
}
log.Infof("WebSocket 已完全停止 key:%s", wm.apiKey)
})
}
// handleReconnect 处理重连逻辑
func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) {
const maxRetries = 10
backoff := time.Second
for {
select {
case <-wm.reconnect:
if !wm.reconnecting.CompareAndSwap(false, true) {
continue // 如果已经在重连,则忽略
}
// 在开始重连循环之前,先安全地关闭旧的连接
wm.closeConn()
retryCount := 0
for retryCount < maxRetries {
select {
case <-wm.stopChannel:
wm.reconnecting.Store(false)
return
default:
}
log.Infof("WebSocket (%s) 正在尝试重连,第 %d 次...", wm.wsType, retryCount+1)
if err := wm.connect(ctx); err == nil {
log.Infof("WebSocket (%s) 重连成功", wm.wsType)
wm.reconnecting.Store(false)
go wm.startDeadCheck(ctx) // 重连成功后,重新启动假死检测
break // 跳出重连循环
}
retryCount++
time.Sleep(backoff)
backoff *= 2
if backoff > time.Minute {
backoff = time.Minute
}
}
if retryCount >= maxRetries {
log.Errorf("WebSocket (%s) 重连失败次数过多,停止重连", wm.wsType)
wm.Stop()
return
}
case <-ctx.Done():
wm.reconnecting.Store(false)
return
}
}
}
// 已有连接
var SpotSockets = map[string]*BinanceWebSocketManager{}
var FutureSockets = map[string]*BinanceWebSocketManager{}
/**
* 创建新的Binance WebSocket管理器
* @param wsType WebSocket类型0-现货1-合约
* @param apiKey API密钥
* @param apiSecret API密钥
* @param proxyType 代理类型
* @param proxyAddress 代理地址
* @return WebSocket管理器实例
*/
func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager {
url := ""
@ -62,7 +160,7 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA
url = "wss://fstream.binance.com/ws"
}
return &BinanceWebSocketManager{
wm := &BinanceWebSocketManager{
stopChannel: make(chan struct{}, 10),
reconnect: make(chan struct{}, 10),
isStopped: false,
@ -73,18 +171,36 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA
proxyType: proxyType,
proxyAddress: proxyAddress,
}
// 初始化最后ping时间
wm.lastPingTime.Store(time.Now())
return wm
}
/**
* 获取API密钥
* @return API密钥
*/
func (wm *BinanceWebSocketManager) GetKey() string {
return wm.apiKey
}
/**
* 启动WebSocket连接
*/
func (wm *BinanceWebSocketManager) Start() {
utility.SafeGo(wm.run)
// wm.run()
}
// 重启连接
/**
* 重启连接,更新配置参数
* @param apiKey 新的API密钥
* @param apiSecret 新的API密钥
* @param proxyType 新的代理类型
* @param proxyAddress 新的代理地址
* @return WebSocket管理器实例
*/
func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager {
wm.mu.Lock()
defer wm.mu.Unlock()
@ -105,7 +221,10 @@ func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAd
return wm
}
// 触发重连
/**
* 触发重连机制
* @param force 是否强制重连
*/
func (wm *BinanceWebSocketManager) triggerReconnect(force bool) {
if force {
wm.reconnecting.Store(false) // 强制重置标志位
@ -122,15 +241,24 @@ func (wm *BinanceWebSocketManager) triggerReconnect(force bool) {
}
}
}
/**
* 外部重启函数
* @param wm WebSocket管理器实例
*/
func Restart(wm *BinanceWebSocketManager) {
log.Warnf("调用restart")
wm.triggerReconnect(true)
}
/**
* 主运行循环
*/
func (wm *BinanceWebSocketManager) run() {
ctx, cancel := context.WithCancel(context.Background())
wm.cancelFunc = cancel
utility.SafeGo(wm.handleSignal)
// utility.SafeGo(wm.handleSignal)
// 计算错误记录键
errKey := fmt.Sprintf(global.API_WEBSOCKET_ERR, wm.apiKey)
@ -163,7 +291,11 @@ func (wm *BinanceWebSocketManager) run() {
}
}
// handleConnectionError 处理 WebSocket 连接错误
/**
* 处理WebSocket连接错误
* @param errKey Redis错误记录键
* @param err 错误信息
*/
func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err error) {
// 从 Redis 获取错误记录
var errMessage commondto.WebSocketErr
@ -186,7 +318,11 @@ func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err erro
log.Error("连接 %s WebSocket 时出错: %v, 错误: %v", wm.wsType, wm.apiKey, err)
}
// isErrorCountExceeded 检查错误次数是否超过阈值
/**
* 检查错误次数是否超过阈值
* @param errKey Redis错误记录键
* @return 是否超过阈值
*/
func (wm *BinanceWebSocketManager) isErrorCountExceeded(errKey string) bool {
val, _ := helper.DefaultRedis.GetString(errKey)
if val == "" {
@ -201,16 +337,13 @@ func (wm *BinanceWebSocketManager) isErrorCountExceeded(errKey string) bool {
return errMessage.Count >= 5
}
// 处理终止信号
func (wm *BinanceWebSocketManager) handleSignal() {
ch := make(chan os.Signal)
signal.Notify(ch, os.Interrupt)
<-ch
wm.Stop()
}
/**
* 建立WebSocket连接
* @param ctx 上下文
* @return 错误信息
*/
func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
dialer, err := wm.getDialer()
dialer, err := proxy.GetDialer(wm.proxyType, wm.proxyAddress)
if err != nil {
return err
}
@ -227,8 +360,9 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
return err
}
// 连接成功,更新连接时间
// 连接成功,更新连接时间和ping时间
wm.ConnectTime = time.Now()
wm.lastPingTime.Store(time.Now())
log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey))
// Ping处理
@ -238,23 +372,25 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
for x := 0; x < 5; x++ {
if err := wm.ws.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second*10)); err != nil {
log.Error("binance 回应pong失败 次数:", strconv.Itoa(x), " err:", err)
time.Sleep(time.Second * 1)
continue
}
break
}
// 更新ping时间和Redis记录
wm.lastPingTime.Store(time.Now())
setLastTime(wm)
return nil
})
// 启动各种协程
utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) })
utility.SafeGo(func() { wm.readMessages(ctx) })
utility.SafeGo(func() { wm.handleReconnect(ctx) })
utility.SafeGo(func() { wm.startPingLoop(ctx) })
// utility.SafeGo(func() { wm.startDeadCheck(ctx) })
utility.SafeGo(func() { wm.startDeadCheck(ctx) }) // 连接成功后立即启动假死检测
utility.SafeGo(func() { wm.start24HourReconnectTimer(ctx) }) // 启动24小时强制重连
return nil
}
@ -278,7 +414,7 @@ func (wm *BinanceWebSocketManager) ReplaceConnection() error {
return fmt.Errorf("获取新 listenKey 失败: %w", err)
}
dialer, err := wm.getDialer()
dialer, err := proxy.GetDialer(wm.proxyType, wm.proxyAddress)
if err != nil {
return err
}
@ -360,54 +496,6 @@ func setLastTime(wm *BinanceWebSocketManager) {
}
func (wm *BinanceWebSocketManager) getDialer() (*websocket.Dialer, error) {
if wm.proxyAddress == "" {
return &websocket.Dialer{}, nil
}
if !strings.HasPrefix(wm.proxyAddress, "http://") && !strings.HasPrefix(wm.proxyAddress, "https://") && !strings.HasPrefix(wm.proxyAddress, "socks5://") {
wm.proxyAddress = wm.proxyType + "://" + wm.proxyAddress
}
proxyURL, err := url.Parse(wm.proxyAddress)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy URL: %v", err)
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
}
switch proxyURL.Scheme {
case "socks5":
return wm.createSocks5Dialer(proxyURL)
case "http", "https":
transport.Proxy = http.ProxyURL(proxyURL)
return &websocket.Dialer{Proxy: transport.Proxy, TLSClientConfig: transport.TLSClientConfig}, nil
default:
return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme)
}
}
func (wm *BinanceWebSocketManager) createSocks5Dialer(proxyURL *url.URL) (*websocket.Dialer, error) {
auth := &proxy.Auth{}
if proxyURL.User != nil {
auth.User = proxyURL.User.Username()
auth.Password, _ = proxyURL.User.Password()
}
socksDialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %v", err)
}
return &websocket.Dialer{
NetDialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return socksDialer.Dial(network, address)
},
}, nil
}
// 复用创建HTTP客户端的逻辑
func (wm *BinanceWebSocketManager) createBinanceClient() (*helper.BinanceClient, error) {
return helper.NewBinanceClient(wm.apiKey, wm.apiSecret, wm.proxyType, wm.proxyAddress)
@ -450,7 +538,6 @@ func (wm *BinanceWebSocketManager) getListenKey() (string, error) {
// 接收消息
func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
defer wm.ws.Close()
for {
select {
@ -462,20 +549,23 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
}
_, msg, err := wm.ws.ReadMessage()
if err != nil && strings.Contains(err.Error(), "websocket: close") {
if !wm.isStopped {
log.Error("收到关闭消息", err.Error())
wm.triggerReconnect(false)
if err != nil {
// 检查是否是由于我们主动关闭连接导致的错误
if wm.isStopped {
log.Infof("WebSocket read loop gracefully stopped for key: %s", wm.apiKey)
return
}
log.Error("websocket 关闭")
return
} else if err != nil {
log.Error("读取消息时出错: %v", err)
return
// 如果不是主动关闭,再判断是否是连接关闭错误,并触发重连
if strings.Contains(err.Error(), "websocket: close") {
log.Error("WebSocket connection closed unexpectedly, triggering reconnect for key: %s. Error: %v", wm.apiKey, err)
wm.triggerReconnect(false)
} else {
log.Error("Error reading message for key: %s. Error: %v", wm.apiKey, err)
}
return // 任何错误发生后都应退出读取循环
}
wm.handleOrderUpdate(msg)
}
}
}
@ -483,165 +573,32 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) {
setLastTime(wm)
if reconnect, _ := ReceiveListen(msg, wm.wsType, wm.apiKey); reconnect {
log.Errorf("收到重连请求")
if reconnect, fatal, err := ReceiveListen(msg, wm.wsType, wm.apiKey); err != nil {
log.Errorf("处理消息时出错: %v", err)
// 根据错误类型决定是否停止
if fatal {
log.Errorf("收到致命错误,将停止 WebSocket 管理器 for key: %s", wm.apiKey)
wm.Stop()
}
return
} else if fatal {
log.Errorf("收到致命错误,将停止 WebSocket 管理器 for key: %s", wm.apiKey)
wm.Stop()
return
} else if reconnect {
log.Warnf("收到重连请求,将触发重连 for key: %s", wm.apiKey)
wm.triggerReconnect(false)
}
}
// Stop 安全停止 WebSocket
func (wm *BinanceWebSocketManager) Stop() {
// closeConn 安全地关闭WebSocket连接
func (wm *BinanceWebSocketManager) closeConn() {
wm.mu.Lock()
defer wm.mu.Unlock()
if wm.isStopped {
return
}
wm.isStopped = true
select {
case <-wm.stopChannel:
default:
close(wm.stopChannel)
}
if wm.cancelFunc != nil {
wm.cancelFunc()
wm.cancelFunc = nil
}
if wm.ws != nil {
if err := wm.ws.Close(); err != nil {
log.Errorf("WebSocket Close 错误 key:%s err:%v", wm.apiKey, err)
}
_ = wm.ws.Close()
wm.ws = nil
}
log.Infof("WebSocket 已完全停止 key:%s", wm.apiKey)
wm.stopChannel = make(chan struct{}, 10)
}
// handleReconnect 使用指数退避并保持永不退出
func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) {
const maxRetries = 100
baseDelay := time.Second * 2
retryCount := 0
for {
select {
case <-ctx.Done():
log.Infof("handleReconnect context done: %s", wm.apiKey)
return
case <-wm.reconnect:
wm.mu.Lock()
if wm.isStopped {
wm.mu.Unlock()
return
}
wm.mu.Unlock()
log.Warnf("WebSocket 连接断开,准备重连 key:%s", wm.apiKey)
for {
wm.mu.Lock()
if wm.ws != nil {
_ = wm.ws.Close()
wm.ws = nil
}
if wm.cancelFunc != nil {
wm.cancelFunc()
wm.cancelFunc = nil
}
wm.mu.Unlock()
newCtx, cancel := context.WithCancel(context.Background())
wm.mu.Lock()
wm.cancelFunc = cancel
wm.mu.Unlock()
if err := wm.connect(newCtx); err != nil {
log.Errorf("🔌 重连失败(%d/%dkey:%serr: %v", retryCount+1, maxRetries, wm.apiKey, err)
cancel()
retryCount++
if retryCount >= maxRetries {
log.Errorf("❌ 重连失败次数过多,停止重连逻辑 key:%s", wm.apiKey)
wm.reconnecting.Store(false)
return
}
delay := baseDelay * time.Duration(1<<retryCount)
if delay > time.Minute*5 {
delay = time.Minute * 5
}
log.Warnf("等待 %v 后重试...", delay)
time.Sleep(delay)
continue
}
log.Infof("✅ 重连成功 key:%s", wm.apiKey)
retryCount = 0
wm.reconnecting.Store(false)
// ✅ 重连成功后开启假死检测
utility.SafeGo(func() { wm.startDeadCheck(newCtx) })
break
}
}
}
}
// startDeadCheck 替代 Start 中的定时器,绑定连接生命周期
func (wm *BinanceWebSocketManager) startDeadCheck(ctx context.Context) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if wm.isStopped {
return
}
wm.DeadCheck()
}
}
}
// 假死检测
func (wm *BinanceWebSocketManager) DeadCheck() {
subKey := fmt.Sprintf(global.USER_SUBSCRIBE, wm.apiKey)
val, _ := helper.DefaultRedis.GetString(subKey)
if val == "" {
log.Warnf("没有订阅信息,无法进行假死检测")
return
}
var data binancedto.UserSubscribeState
_ = sonic.Unmarshal([]byte(val), &data)
var lastTime *time.Time
if wm.wsType == 0 {
lastTime = data.SpotLastTime
} else {
lastTime = data.FuturesLastTime
}
// 定义最大静默时间(超出视为假死)
var timeout time.Duration
if wm.wsType == 0 {
timeout = 40 * time.Second // Spot 每 20s ping40s 足够
} else {
timeout = 6 * time.Minute // Futures 每 3 分钟 ping
}
if lastTime != nil && time.Since(*lastTime) > timeout {
log.Warnf("检测到假死连接 key:%s type:%v, 距离上次通信: %v, 触发重连", wm.apiKey, wm.wsType, time.Since(*lastTime))
wm.triggerReconnect(true)
}
}
// 主动心跳发送机制
@ -657,10 +614,10 @@ func (wm *BinanceWebSocketManager) startPingLoop(ctx context.Context) {
if wm.isStopped {
return
}
err := wm.ws.WriteMessage(websocket.PingMessage, []byte("ping"))
if err != nil {
if err := wm.sendPing(); err != nil {
log.Error("主动 Ping Binance 失败:", err)
wm.triggerReconnect(false)
return // 退出循环,等待重连
}
}
}
@ -784,3 +741,112 @@ func randomHex(n int) string {
}
return string(bytes)
}
/**
* 启动24小时强制重连定时器
* @param ctx 上下文
*/
func (wm *BinanceWebSocketManager) start24HourReconnectTimer(ctx context.Context) {
// Binance要求不到24小时就需要主动断开重连
// 设置为23小时50分钟留出一些缓冲时间
duration := 23*time.Hour + 50*time.Minute
wm.forceReconnectTimer = time.NewTimer(duration)
defer func() {
if wm.forceReconnectTimer != nil {
wm.forceReconnectTimer.Stop()
}
}()
select {
case <-ctx.Done():
return
case <-wm.forceReconnectTimer.C:
if !wm.isStopped {
log.Warnf("24小时强制重连触发 key:%s wsType:%v", wm.apiKey, wm.wsType)
wm.triggerReconnect(true)
}
}
}
/**
* 改进的假死检测机制
* @param ctx 上下文
*/
func (wm *BinanceWebSocketManager) startDeadCheck(ctx context.Context) {
// 缩短检测间隔,提高响应速度
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if wm.isStopped {
return
}
wm.DeadCheck()
}
}
}
/**
* 改进的假死检测逻辑
*/
func (wm *BinanceWebSocketManager) DeadCheck() {
// 检查最后ping时间
lastPing := wm.lastPingTime.Load().(time.Time)
// 定义最大静默时间根据WebSocket类型调整
var timeout time.Duration
if wm.wsType == 0 {
timeout = 3 * time.Minute // 现货3分钟无ping视为假死
} else {
timeout = 6 * time.Minute // 合约6分钟无ping视为假死
}
if time.Since(lastPing) > timeout {
log.Warnf("检测到假死连接(基于ping时间) key:%s type:%v, 距离上次ping: %v, 触发重连",
wm.apiKey, wm.wsType, time.Since(lastPing))
wm.triggerReconnect(true)
return
}
// 同时检查Redis中的记录作为备用检测
subKey := fmt.Sprintf(global.USER_SUBSCRIBE, wm.apiKey)
val, _ := helper.DefaultRedis.GetString(subKey)
if val == "" {
log.Debugf("没有订阅信息跳过Redis假死检测")
return
}
var data binancedto.UserSubscribeState
_ = sonic.Unmarshal([]byte(val), &data)
var lastTime *time.Time
if wm.wsType == 0 {
lastTime = data.SpotLastTime
} else {
lastTime = data.FuturesLastTime
}
// Redis记录的超时时间设置得更长一些
var redisTimeout time.Duration
if wm.wsType == 0 {
redisTimeout = 3 * time.Minute
} else {
redisTimeout = 6 * time.Minute
}
if lastTime != nil && time.Since(*lastTime) > redisTimeout {
log.Warnf("检测到假死连接(基于Redis记录) key:%s type:%v, 距离上次通信: %v, 触发重连",
wm.apiKey, wm.wsType, time.Since(*lastTime))
wm.triggerReconnect(true)
}
}
/**
* 优化的重连处理逻辑
* @param ctx 上下文
*/