1、暂存
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
package excservice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-admin/models/futuresdto"
|
||||
"go-admin/pkg/utility"
|
||||
"go-admin/services/binanceservice"
|
||||
@ -15,27 +16,43 @@ import (
|
||||
- @msg 消息内容
|
||||
- @listenType 订阅类型 0-现货 1-合约
|
||||
*/
|
||||
func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, err error) {
|
||||
// ReceiveListen 处理用户订单订阅
|
||||
// 返回:
|
||||
// - reconnect: bool, 是否需要立即重连 (通常用于 listenKey 过期)
|
||||
// - fatal: bool, 是否是致命错误 (例如认证失败),需要停止整个服务
|
||||
// - err: error, 解析或处理过程中的错误
|
||||
func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, fatal bool, err error) {
|
||||
var dataMap map[string]interface{}
|
||||
err = sonic.Unmarshal(msg, &dataMap)
|
||||
|
||||
if err != nil {
|
||||
log.Error("接收ws 反序列化失败:", err)
|
||||
return
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
// 检查是否是币安的错误消息, e.g. {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."}
|
||||
if code, ok := dataMap["code"].(float64); ok && code != 0 {
|
||||
errMsg, _ := dataMap["msg"].(string)
|
||||
log.Errorf("收到币安错误码: %d, 消息: %s, API Key: %s", int(code), errMsg, apiKey)
|
||||
// -2015 是无效API Key的错误码,这是一个致命错误,不需要重连
|
||||
if int(code) == -2015 {
|
||||
return false, true, fmt.Errorf("币安认证失败 (code: %d): %s", int(code), errMsg)
|
||||
}
|
||||
// 其他错误码可能也需要特殊处理,但暂时我们只将-2015视为致命错误
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
event, exits := dataMap["e"]
|
||||
|
||||
if !exits {
|
||||
log.Error("不存在event")
|
||||
return
|
||||
// 如果没有 event 字段,但也不是一个错误消息,则可能是一个未知的消息格式
|
||||
log.Warn("收到的消息中不存在event字段: %s", string(msg))
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
switch event {
|
||||
//listenKey过期
|
||||
case "listenKeyExpired":
|
||||
log.Info("listenKey过期", string(msg))
|
||||
return true, nil
|
||||
return true, false, nil
|
||||
|
||||
//订单变更
|
||||
case "ORDER_TRADE_UPDATE":
|
||||
@ -101,11 +118,12 @@ func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, e
|
||||
|
||||
case "eventStreamTerminated":
|
||||
log.Info("账户数据流被终止 type:", getWsTypeName(listenType))
|
||||
return true, false, nil
|
||||
default:
|
||||
log.Info("未知事件 内容:", string(msg))
|
||||
log.Info("未知事件", event)
|
||||
return false, nil
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
@ -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/%d)key:%s,err: %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 ping,40s 足够
|
||||
} 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 上下文
|
||||
*/
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-admin/models"
|
||||
@ -19,6 +20,7 @@ type BinanceWs struct {
|
||||
proxyUrl string
|
||||
WorkType string
|
||||
wsConns []*WsConn
|
||||
mu sync.Mutex // 新增互斥锁
|
||||
tickerCallback func(models.Ticker24, string, string)
|
||||
forceCallback func(models.ForceOrder, string, string)
|
||||
depthCallback func(models.DepthBin, string, string)
|
||||
@ -70,11 +72,15 @@ func (bnWs *BinanceWs) subscribe(endpoint string, handle func(msg []byte) error)
|
||||
if wsConn == nil {
|
||||
return
|
||||
}
|
||||
bnWs.mu.Lock()
|
||||
bnWs.wsConns = append(bnWs.wsConns, wsConn)
|
||||
bnWs.mu.Unlock()
|
||||
go bnWs.exitHandler(wsConn)
|
||||
}
|
||||
|
||||
func (bnWs *BinanceWs) Close() {
|
||||
bnWs.mu.Lock()
|
||||
defer bnWs.mu.Unlock()
|
||||
for _, con := range bnWs.wsConns {
|
||||
con.CloseWs()
|
||||
}
|
||||
|
||||
218
services/excservice/bitget_receive.go
Normal file
218
services/excservice/bitget_receive.go
Normal file
@ -0,0 +1,218 @@
|
||||
package excservice
|
||||
|
||||
import (
|
||||
"go-admin/pkg/utility"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
)
|
||||
|
||||
/*
|
||||
Bitget用户订单订阅处理
|
||||
- @msg 消息内容
|
||||
- @listenType 订阅类型 0-现货 1-合约
|
||||
*/
|
||||
func ReceiveBitgetListen(msg []byte, listenType int, apiKey string) (reconnect bool, err error) {
|
||||
var dataMap map[string]interface{}
|
||||
err = sonic.Unmarshal(msg, &dataMap)
|
||||
|
||||
if err != nil {
|
||||
log.Error("接收Bitget ws 反序列化失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是操作响应
|
||||
if op, exists := dataMap["op"]; exists {
|
||||
switch op {
|
||||
case "login":
|
||||
return handleBitgetLogin(dataMap, apiKey)
|
||||
case "subscribe":
|
||||
return handleBitgetSubscribe(dataMap, apiKey)
|
||||
case "unsubscribe":
|
||||
return handleBitgetUnsubscribe(dataMap, apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否是数据推送
|
||||
if arg, exists := dataMap["arg"]; exists {
|
||||
return handleBitgetDataPush(dataMap, arg, listenType, apiKey)
|
||||
}
|
||||
|
||||
log.Info("未知Bitget事件 内容:", string(msg))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// handleBitgetLogin 处理登录响应
|
||||
func handleBitgetLogin(dataMap map[string]interface{}, apiKey string) (bool, error) {
|
||||
if code, exists := dataMap["code"]; exists {
|
||||
if code == "0" || code == 0 {
|
||||
log.Info("Bitget WebSocket登录成功 key:%s", apiKey)
|
||||
} else {
|
||||
log.Error("Bitget WebSocket登录失败 key:%s code:%v", apiKey, code)
|
||||
return true, nil // 需要重连
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// handleBitgetSubscribe 处理订阅响应
|
||||
func handleBitgetSubscribe(dataMap map[string]interface{}, apiKey string) (bool, error) {
|
||||
if code, exists := dataMap["code"]; exists {
|
||||
if code == "0" || code == 0 {
|
||||
log.Info("Bitget WebSocket订阅成功 key:%s", apiKey)
|
||||
} else {
|
||||
log.Error("Bitget WebSocket订阅失败 key:%s code:%v", apiKey, code)
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// handleBitgetUnsubscribe 处理取消订阅响应
|
||||
func handleBitgetUnsubscribe(dataMap map[string]interface{}, apiKey string) (bool, error) {
|
||||
if code, exists := dataMap["code"]; exists {
|
||||
if code == "0" || code == 0 {
|
||||
log.Info("Bitget WebSocket取消订阅成功 key:%s", apiKey)
|
||||
} else {
|
||||
log.Error("Bitget WebSocket取消订阅失败 key:%s code:%v", apiKey, code)
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// handleBitgetDataPush 处理数据推送
|
||||
func handleBitgetDataPush(dataMap map[string]interface{}, arg interface{}, listenType int, apiKey string) (bool, error) {
|
||||
argMap, ok := arg.(map[string]interface{})
|
||||
if !ok {
|
||||
log.Error("Bitget数据推送参数格式错误 key:%s", apiKey)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
channel, exists := argMap["channel"]
|
||||
if !exists {
|
||||
log.Error("Bitget数据推送缺少channel参数 key:%s", apiKey)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
switch channel {
|
||||
case "account":
|
||||
return handleBitgetAccount(dataMap, listenType, apiKey)
|
||||
case "orders":
|
||||
return handleBitgetOrders(dataMap, listenType, apiKey)
|
||||
case "positions":
|
||||
return handleBitgetPositions(dataMap, listenType, apiKey)
|
||||
case "balance":
|
||||
return handleBitgetBalance(dataMap, listenType, apiKey)
|
||||
default:
|
||||
log.Info("收到Bitget未处理的频道数据 channel:%v key:%s", channel, apiKey)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// handleBitgetAccount 处理账户更新
|
||||
func handleBitgetAccount(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) {
|
||||
log.Info("收到Bitget账户更新 listenType:%d key:%s", listenType, apiKey)
|
||||
|
||||
data, exists := dataMap["data"]
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 这里可以根据需要实现具体的账户更新处理逻辑
|
||||
// 类似于binanceservice.ChangeSpotOrder或binanceservice.ChangeFutureOrder
|
||||
|
||||
utility.SafeGo(func() {
|
||||
handleBitgetAccountUpdate(data, listenType, apiKey)
|
||||
})
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// handleBitgetOrders 处理订单更新
|
||||
func handleBitgetOrders(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) {
|
||||
log.Info("收到Bitget订单更新 listenType:%d key:%s", listenType, apiKey)
|
||||
|
||||
data, exists := dataMap["data"]
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
utility.SafeGo(func() {
|
||||
handleBitgetOrderUpdate(data, listenType, apiKey)
|
||||
})
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// handleBitgetPositions 处理持仓更新
|
||||
func handleBitgetPositions(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) {
|
||||
log.Info("收到Bitget持仓更新 listenType:%d key:%s", listenType, apiKey)
|
||||
|
||||
data, exists := dataMap["data"]
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
utility.SafeGo(func() {
|
||||
handleBitgetPositionUpdate(data, listenType, apiKey)
|
||||
})
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// handleBitgetBalance 处理余额更新
|
||||
func handleBitgetBalance(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) {
|
||||
log.Info("收到Bitget余额更新 listenType:%d key:%s", listenType, apiKey)
|
||||
|
||||
data, exists := dataMap["data"]
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
utility.SafeGo(func() {
|
||||
handleBitgetBalanceUpdate(data, listenType, apiKey)
|
||||
})
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 具体的处理函数,可以根据业务需求实现
|
||||
|
||||
// handleBitgetAccountUpdate 处理账户更新的具体逻辑
|
||||
func handleBitgetAccountUpdate(data interface{}, listenType int, apiKey string) {
|
||||
// TODO: 实现具体的账户更新处理逻辑
|
||||
log.Info("处理Bitget账户更新 listenType:%d key:%s data:%v", listenType, apiKey, data)
|
||||
}
|
||||
|
||||
// handleBitgetOrderUpdate 处理订单更新的具体逻辑
|
||||
func handleBitgetOrderUpdate(data interface{}, listenType int, apiKey string) {
|
||||
// TODO: 实现具体的订单更新处理逻辑
|
||||
log.Info("处理Bitget订单更新 listenType:%d key:%s data:%v", listenType, apiKey, data)
|
||||
|
||||
// 这里可以参考binanceservice中的订单处理逻辑
|
||||
// 例如:更新数据库中的订单状态、触发回调等
|
||||
}
|
||||
|
||||
// handleBitgetPositionUpdate 处理持仓更新的具体逻辑
|
||||
func handleBitgetPositionUpdate(data interface{}, listenType int, apiKey string) {
|
||||
// TODO: 实现具体的持仓更新处理逻辑
|
||||
log.Info("处理Bitget持仓更新 listenType:%d key:%s data:%v", listenType, apiKey, data)
|
||||
}
|
||||
|
||||
// handleBitgetBalanceUpdate 处理余额更新的具体逻辑
|
||||
func handleBitgetBalanceUpdate(data interface{}, listenType int, apiKey string) {
|
||||
// TODO: 实现具体的余额更新处理逻辑
|
||||
log.Info("处理Bitget余额更新 listenType:%d key:%s data:%v", listenType, apiKey, data)
|
||||
}
|
||||
|
||||
// getBitgetWsTypeName 获取WebSocket类型名称
|
||||
func getBitgetWsTypeName(listenType int) string {
|
||||
switch listenType {
|
||||
case 0:
|
||||
return "现货"
|
||||
case 1:
|
||||
return "合约"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user