package bitgetservice import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "errors" "fmt" "go-admin/pkg/utility" "net" "net/http" "net/url" "os" "os/signal" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/bytedance/sonic" log "github.com/go-admin-team/go-admin-core/logger" "github.com/gorilla/websocket" "golang.org/x/net/proxy" ) // BitgetWebSocketManager Bitget WebSocket管理器 // 类似于BinanceWebSocketManager,支持外部传入API密钥 type BitgetWebSocketManager struct { ws *websocket.Conn stopChannel chan struct{} url string /* 0-现货 1-合约 */ wsType int apiKey string apiSecret string passphrase string proxyType string proxyAddress string reconnect chan struct{} isStopped bool // 标记 WebSocket 是否已主动停止 mu sync.Mutex // 用于控制并发访问 isStopped cancelFunc context.CancelFunc reconnecting atomic.Bool // 防止重复重连 ConnectTime time.Time // 当前连接建立时间 } // 已有连接存储 var BitgetSpotSockets = map[string]*BitgetWebSocketManager{} var BitgetFutureSockets = map[string]*BitgetWebSocketManager{} // NewBitgetWebSocketManager 创建新的Bitget WebSocket管理器 func NewBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager { var wsUrl string switch wsType { case 0: // 现货 wsUrl = "wss://ws.bitget.com/spot/v1/stream" case 1: // 合约 wsUrl = "wss://ws.bitget.com/mix/v1/stream" default: wsUrl = "wss://ws.bitget.com/spot/v1/stream" } return &BitgetWebSocketManager{ stopChannel: make(chan struct{}, 10), reconnect: make(chan struct{}, 10), isStopped: false, url: wsUrl, wsType: wsType, apiKey: apiKey, apiSecret: apiSecret, passphrase: passphrase, proxyType: proxyType, proxyAddress: proxyAddress, } } // GetKey 获取API Key func (wm *BitgetWebSocketManager) GetKey() string { return wm.apiKey } // Start 启动WebSocket连接 func (wm *BitgetWebSocketManager) Start() { utility.SafeGo(wm.run) } // Restart 重启连接 func (wm *BitgetWebSocketManager) Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager { wm.mu.Lock() defer wm.mu.Unlock() wm.apiKey = apiKey wm.apiSecret = apiSecret wm.passphrase = passphrase wm.proxyType = proxyType wm.proxyAddress = proxyAddress if wm.isStopped { wm.isStopped = false utility.SafeGo(wm.run) } else { log.Warnf("调用Bitget WebSocket restart") wm.triggerReconnect(true) } return wm } // triggerReconnect 触发重连 func (wm *BitgetWebSocketManager) triggerReconnect(force bool) { if force { wm.reconnecting.Store(false) } if wm.reconnecting.CompareAndSwap(false, true) { log.Warnf("准备重连Bitget WebSocket key: %s wsType: %v", wm.apiKey, wm.wsType) select { case wm.reconnect <- struct{}{}: default: log.Debugf("Bitget reconnect 信号已存在,跳过 key:%s", wm.apiKey) } } } // run 主运行循环 func (wm *BitgetWebSocketManager) run() { ctx, cancel := context.WithCancel(context.Background()) wm.cancelFunc = cancel utility.SafeGo(wm.handleSignal) for { select { case <-ctx.Done(): return default: if err := wm.connect(ctx); err != nil { wm.handleConnectionError(err) if wm.isErrorCountExceeded() { log.Error("连接Bitget WebSocket时出错次数过多,停止WebSocket管理器: wsType=%v, key=%s", wm.wsType, wm.apiKey) wm.Stop() return } time.Sleep(5 * time.Second) continue } <-wm.stopChannel log.Info("停止Bitget WebSocket管理器... wsType=%s", getBitgetWsTypeName(wm.wsType)) wm.Stop() return } } } // handleConnectionError 处理连接错误 func (wm *BitgetWebSocketManager) handleConnectionError(err error) { // 这里可以添加错误记录到Redis的逻辑,类似binance的实现 log.Error("连接Bitget WebSocket时出错: wsType=%v, key=%v, 错误=%v", wm.wsType, wm.apiKey, err) } // isErrorCountExceeded 检查错误次数是否超过阈值 func (wm *BitgetWebSocketManager) isErrorCountExceeded() bool { // 简化版本,可以根据需要实现更复杂的错误计数逻辑 return false } // handleSignal 处理终止信号 func (wm *BitgetWebSocketManager) handleSignal() { ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt) <-ch wm.Stop() } // connect 建立WebSocket连接 func (wm *BitgetWebSocketManager) connect(ctx context.Context) error { dialer, err := wm.getDialer() if err != nil { return err } // 对于私有频道,需要进行身份验证 headers := wm.getAuthHeaders() wm.ws, _, err = dialer.Dial(wm.url, headers) if err != nil { return err } // 连接成功,更新连接时间 wm.ConnectTime = time.Now() log.Info("已连接到Bitget %s WebSocket key:%s", getBitgetWsTypeName(wm.wsType), wm.apiKey) // 设置Ping处理器 wm.ws.SetPingHandler(func(appData string) error { log.Info("收到Bitget Ping消息 wstype:%v key:%s data:%s", wm.wsType, wm.apiKey, appData) 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("Bitget回应Pong失败 次数:%d err:%v", x, err) time.Sleep(time.Second * 1) continue } break } return nil }) // 启动各种处理协程 utility.SafeGo(func() { wm.readMessages(ctx) }) utility.SafeGo(func() { wm.handleReconnect(ctx) }) utility.SafeGo(func() { wm.startPingLoop(ctx) }) // 发送登录认证消息 if err := wm.authenticate(); err != nil { return fmt.Errorf("bitget WebSocket认证失败: %v", err) } return nil } // getDialer 获取WebSocket拨号器 func (wm *BitgetWebSocketManager) 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) } switch proxyURL.Scheme { case "socks5": return wm.createSocks5Dialer(proxyURL) case "http", "https": return &websocket.Dialer{ Proxy: func(req *http.Request) (*url.URL, error) { return proxyURL, nil }, }, nil default: return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme) } } // createSocks5Dialer 创建SOCKS5代理拨号器 func (wm *BitgetWebSocketManager) 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, addr string) (net.Conn, error) { return socksDialer.Dial(network, addr) }, }, nil } // getAuthHeaders 获取认证头信息 func (wm *BitgetWebSocketManager) getAuthHeaders() http.Header { headers := http.Header{} if wm.apiKey != "" { timestamp := strconv.FormatInt(time.Now().Unix(), 10) sign := wm.generateSignature(timestamp) headers.Set("ACCESS-KEY", wm.apiKey) headers.Set("ACCESS-SIGN", sign) headers.Set("ACCESS-TIMESTAMP", timestamp) headers.Set("ACCESS-PASSPHRASE", wm.passphrase) } return headers } // generateSignature 生成签名 func (wm *BitgetWebSocketManager) generateSignature(timestamp string) string { message := timestamp + "GET" + "/user/verify" h := hmac.New(sha256.New, []byte(wm.apiSecret)) h.Write([]byte(message)) return base64.StdEncoding.EncodeToString(h.Sum(nil)) } // authenticate 发送认证消息 func (wm *BitgetWebSocketManager) authenticate() error { if wm.apiKey == "" { return nil // 公共频道不需要认证 } timestamp := strconv.FormatInt(time.Now().Unix(), 10) sign := wm.generateSignature(timestamp) authMsg := map[string]interface{}{ "op": "login", "args": []map[string]string{ { "apiKey": wm.apiKey, "passphrase": wm.passphrase, "timestamp": timestamp, "sign": sign, }, }, } msgBytes, err := sonic.Marshal(authMsg) if err != nil { return err } return wm.ws.WriteMessage(websocket.TextMessage, msgBytes) } // readMessages 读取消息 func (wm *BitgetWebSocketManager) readMessages(ctx context.Context) { defer func() { if r := recover(); r != nil { log.Error("Bitget WebSocket消息处理器panic: %v", r) } }() for { select { case <-ctx.Done(): return default: wm.mu.Lock() if wm.isStopped || wm.ws == nil { wm.mu.Unlock() return } _, message, err := wm.ws.ReadMessage() wm.mu.Unlock() if err != nil { log.Error("Bitget WebSocket读取消息错误: %v", err) wm.triggerReconnect(false) return } // 处理消息 wm.handleMessage(message) } } } // handleMessage 处理收到的消息 func (wm *BitgetWebSocketManager) handleMessage(message []byte) { // 解析消息并处理 var baseMsg map[string]interface{} if err := sonic.Unmarshal(message, &baseMsg); err != nil { log.Error("解析Bitget WebSocket消息失败: %v", err) return } // 检查是否是认证响应 if op, exists := baseMsg["op"]; exists && op == "login" { wm.handleAuthResponse(baseMsg) return } // 检查是否是订阅数据 if arg, exists := baseMsg["arg"]; exists { wm.handleSubscriptionData(baseMsg, arg) return } log.Info("收到Bitget WebSocket消息: %s", string(message)) } // handleAuthResponse 处理认证响应 func (wm *BitgetWebSocketManager) handleAuthResponse(msg map[string]interface{}) { if code, exists := msg["code"]; exists { if code == "0" || code == 0 { log.Info("Bitget WebSocket认证成功 key:%s", wm.apiKey) // 认证成功后可以订阅私有频道 wm.subscribePrivateChannels() } else { log.Error("Bitget WebSocket认证失败 key:%s code:%v", wm.apiKey, code) } } } // handleSubscriptionData 处理订阅数据 func (wm *BitgetWebSocketManager) handleSubscriptionData(msg map[string]interface{}, arg interface{}) { // 根据频道类型处理不同的数据 // 这里可以根据具体需求实现不同频道的数据处理逻辑 log.Info("收到Bitget订阅数据: %v", arg) } // subscribePrivateChannels 订阅私有频道 func (wm *BitgetWebSocketManager) subscribePrivateChannels() { // 订阅账户更新 wm.subscribeAccount() // 订阅订单更新 wm.subscribeOrders() // 订阅持仓更新 wm.subscribePositions() } // subscribeAccount 订阅账户更新 func (wm *BitgetWebSocketManager) subscribeAccount() { var channel string if wm.wsType == 0 { channel = "account" } else { channel = "account" } subMsg := map[string]interface{}{ "op": "subscribe", "args": []map[string]string{ { "instType": wm.getInstType(), "channel": channel, "instId": "default", }, }, } wm.sendMessage(subMsg) } // subscribeOrders 订阅订单更新 func (wm *BitgetWebSocketManager) subscribeOrders() { var channel string if wm.wsType == 0 { channel = "orders" } else { channel = "orders" } subMsg := map[string]interface{}{ "op": "subscribe", "args": []map[string]string{ { "instType": wm.getInstType(), "channel": channel, "instId": "default", }, }, } wm.sendMessage(subMsg) } // subscribePositions 订阅持仓更新(仅合约) func (wm *BitgetWebSocketManager) subscribePositions() { if wm.wsType != 1 { return // 只有合约才有持仓 } subMsg := map[string]interface{}{ "op": "subscribe", "args": []map[string]string{ { "instType": "umcbl", "channel": "positions", "instId": "default", }, }, } wm.sendMessage(subMsg) } // getInstType 获取产品类型 func (wm *BitgetWebSocketManager) getInstType() string { if wm.wsType == 0 { return "sp" // 现货 } return "umcbl" // U本位合约 } // SendMessage 发送消息(公开方法) func (wm *BitgetWebSocketManager) SendMessage(msg interface{}) error { msgBytes, err := sonic.Marshal(msg) if err != nil { return err } wm.mu.Lock() defer wm.mu.Unlock() if wm.ws == nil { return errors.New("WebSocket连接不可用") } return wm.ws.WriteMessage(websocket.TextMessage, msgBytes) } // sendMessage 发送消息(私有方法,保持向后兼容) func (wm *BitgetWebSocketManager) sendMessage(msg interface{}) error { return wm.SendMessage(msg) } // handleReconnect 处理重连 func (wm *BitgetWebSocketManager) handleReconnect(ctx context.Context) { for { select { case <-ctx.Done(): return case <-wm.reconnect: wm.reconnecting.Store(false) log.Info("开始重连Bitget WebSocket key:%s", wm.apiKey) wm.mu.Lock() if wm.ws != nil { wm.ws.Close() wm.ws = nil } wm.mu.Unlock() // 触发主循环重新连接 select { case wm.stopChannel <- struct{}{}: default: } } } } // startPingLoop 启动心跳循环 func (wm *BitgetWebSocketManager) startPingLoop(ctx context.Context) { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: wm.sendPing() } } } // sendPing 发送心跳 func (wm *BitgetWebSocketManager) sendPing() { pingMsg := "ping" wm.mu.Lock() defer wm.mu.Unlock() if wm.ws == nil { return } if err := wm.ws.WriteMessage(websocket.TextMessage, []byte(pingMsg)); err != nil { log.Error("Bitget WebSocket发送ping失败: %v", err) wm.triggerReconnect(false) } } // Stop 停止WebSocket连接 func (wm *BitgetWebSocketManager) Stop() { wm.mu.Lock() defer wm.mu.Unlock() if wm.isStopped { return } wm.isStopped = true if wm.cancelFunc != nil { wm.cancelFunc() } if wm.ws != nil { wm.ws.Close() wm.ws = nil } // 从全局map中移除 if wm.wsType == 0 { delete(BitgetSpotSockets, wm.apiKey) } else { delete(BitgetFutureSockets, wm.apiKey) } log.Info("Bitget WebSocket已停止 key:%s", wm.apiKey) } // getBitgetWsTypeName 获取WebSocket类型名称 func getBitgetWsTypeName(wsType int) string { switch wsType { case 0: return "现货" case 1: return "合约" default: return "未知" } } // 工具函数:获取或创建Bitget WebSocket管理器 func GetOrCreateBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager { var socketsMap map[string]*BitgetWebSocketManager if wsType == 0 { socketsMap = BitgetSpotSockets } else { socketsMap = BitgetFutureSockets } if manager, exists := socketsMap[apiKey]; exists { // 如果参数有变化,重启连接 if manager.apiSecret != apiSecret || manager.passphrase != passphrase || manager.proxyType != proxyType || manager.proxyAddress != proxyAddress { manager.Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress) } return manager } // 创建新的管理器 manager := NewBitgetWebSocketManager(wsType, apiKey, apiSecret, passphrase, proxyType, proxyAddress) socketsMap[apiKey] = manager return manager } // StopBitgetWebSocketManager 停止指定的WebSocket管理器 func StopBitgetWebSocketManager(wsType int, apiKey string) { var socketsMap map[string]*BitgetWebSocketManager if wsType == 0 { socketsMap = BitgetSpotSockets } else { socketsMap = BitgetFutureSockets } if manager, exists := socketsMap[apiKey]; exists { manager.Stop() } }