Files
exchange_go/services/bitgetservice/bitget_socketmanager.go

659 lines
16 KiB
Go
Raw Normal View History

2025-10-14 19:58:59 +08:00
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()
}
}