431 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			431 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package bitgetservice
 | ||
| 
 | ||
| import (
 | ||
| 	"context"
 | ||
| 	"encoding/json"
 | ||
| 	"errors"
 | ||
| 	"fmt"
 | ||
| 	"go-admin/services/proxy"
 | ||
| 	"strings"
 | ||
| 	"sync"
 | ||
| 	"time"
 | ||
| 
 | ||
| 	"github.com/bytedance/sonic"
 | ||
| 	log "github.com/go-admin-team/go-admin-core/logger"
 | ||
| 	"github.com/gorilla/websocket"
 | ||
| )
 | ||
| 
 | ||
| // BitgetMarketClient Bitget公开行情订阅客户端
 | ||
| // 基于官方SDK设计,专门用于公开市场数据订阅
 | ||
| type BitgetMarketClient struct {
 | ||
| 	config         *BitgetMarketConfig
 | ||
| 	conn           *websocket.Conn
 | ||
| 	subscriptions  map[SubscribeReq]OnReceive
 | ||
| 	allSubscribes  *Set
 | ||
| 	listener       OnReceive
 | ||
| 	errorListener  OnReceive
 | ||
| 	sendMutex      sync.Mutex
 | ||
| 	reconnectMutex sync.Mutex
 | ||
| 	ctx            context.Context
 | ||
| 	cancel         context.CancelFunc
 | ||
| 	connected      bool
 | ||
| 	lastPongTime   time.Time
 | ||
| 	proxyType      string
 | ||
| 	proxyAddress   string
 | ||
| 	reconnectCount int
 | ||
| }
 | ||
| 
 | ||
| // NewBitgetMarketClient 创建新的Bitget市场数据客户端
 | ||
| func NewBitgetMarketClient(config *BitgetMarketConfig) *BitgetMarketClient {
 | ||
| 	if config == nil {
 | ||
| 		config = DefaultMarketConfig()
 | ||
| 	}
 | ||
| 
 | ||
| 	ctx, cancel := context.WithCancel(context.Background())
 | ||
| 
 | ||
| 	return &BitgetMarketClient{
 | ||
| 		config:        config,
 | ||
| 		subscriptions: make(map[SubscribeReq]OnReceive),
 | ||
| 		allSubscribes: NewSet(),
 | ||
| 		ctx:           ctx,
 | ||
| 		cancel:        cancel,
 | ||
| 		connected:     false,
 | ||
| 		lastPongTime:  time.Now(),
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // SetProxy 设置代理
 | ||
| func (c *BitgetMarketClient) SetProxy(proxyType, proxyAddress string) {
 | ||
| 	c.proxyType = proxyType
 | ||
| 	c.proxyAddress = proxyAddress
 | ||
| }
 | ||
| 
 | ||
| // SetListeners 设置消息监听器
 | ||
| func (c *BitgetMarketClient) SetListeners(msgListener, errorListener OnReceive) {
 | ||
| 	c.listener = msgListener
 | ||
| 	c.errorListener = errorListener
 | ||
| }
 | ||
| 
 | ||
| // Connect 连接WebSocket
 | ||
| func (c *BitgetMarketClient) Connect() error {
 | ||
| 	c.reconnectMutex.Lock()
 | ||
| 	defer c.reconnectMutex.Unlock()
 | ||
| 
 | ||
| 	if c.connected {
 | ||
| 		return nil
 | ||
| 	}
 | ||
| 
 | ||
| 	// 创建WebSocket连接
 | ||
| 	dialer := websocket.DefaultDialer
 | ||
| 
 | ||
| 	// 设置代理
 | ||
| 	if c.proxyAddress != "" {
 | ||
| 		var err error
 | ||
| 		dialer, err = proxy.GetDialer(c.proxyType, c.proxyAddress)
 | ||
| 		if err != nil {
 | ||
| 			return fmt.Errorf("设置代理失败: %v", err)
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	dialer.HandshakeTimeout = 10 * time.Second
 | ||
| 	log.Infof("正在连接Bitget市场数据WebSocket: %s", c.config.WsUrl)
 | ||
| 	conn, _, err := dialer.Dial(c.config.WsUrl, nil)
 | ||
| 	if err != nil {
 | ||
| 		return fmt.Errorf("webSocket连接失败: %v", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	c.conn = conn
 | ||
| 	c.connected = true
 | ||
| 	c.lastPongTime = time.Now()
 | ||
| 	c.reconnectCount = 0
 | ||
| 
 | ||
| 	log.Info("Bitget市场数据WebSocket连接成功")
 | ||
| 
 | ||
| 	// 启动消息处理协程
 | ||
| 	go c.readLoop()
 | ||
| 	go c.pingLoop()
 | ||
| 	go c.monitorConnection()
 | ||
| 
 | ||
| 	return nil
 | ||
| }
 | ||
| 
 | ||
| // Subscribe 订阅市场数据
 | ||
| func (c *BitgetMarketClient) Subscribe(subscribeReqs []SubscribeReq, listener OnReceive) error {
 | ||
| 	if !c.connected {
 | ||
| 		if err := c.Connect(); err != nil {
 | ||
| 			return err
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	var args []interface{}
 | ||
| 	for _, req := range subscribeReqs {
 | ||
| 		// 标准化订阅请求
 | ||
| 		normalizedReq := c.normalizeSubscribeReq(req)
 | ||
| 		args = append(args, normalizedReq)
 | ||
| 
 | ||
| 		// 保存订阅信息
 | ||
| 		c.subscriptions[normalizedReq] = listener
 | ||
| 		c.allSubscribes.Add(normalizedReq)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 发送订阅消息
 | ||
| 	wsReq := WsBaseReq{
 | ||
| 		Op:   "subscribe",
 | ||
| 		Args: args,
 | ||
| 	}
 | ||
| 
 | ||
| 	return c.sendMessage(wsReq)
 | ||
| }
 | ||
| 
 | ||
| // Unsubscribe 取消订阅
 | ||
| func (c *BitgetMarketClient) Unsubscribe(subscribeReqs []SubscribeReq) error {
 | ||
| 	if !c.connected {
 | ||
| 		return errors.New("WebSocket未连接")
 | ||
| 	}
 | ||
| 
 | ||
| 	var args []interface{}
 | ||
| 	for _, req := range subscribeReqs {
 | ||
| 		normalizedReq := c.normalizeSubscribeReq(req)
 | ||
| 		args = append(args, normalizedReq)
 | ||
| 
 | ||
| 		// 移除订阅信息
 | ||
| 		delete(c.subscriptions, normalizedReq)
 | ||
| 		c.allSubscribes.Remove(normalizedReq)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 发送取消订阅消息
 | ||
| 	wsReq := WsBaseReq{
 | ||
| 		Op:   "unsubscribe",
 | ||
| 		Args: args,
 | ||
| 	}
 | ||
| 
 | ||
| 	return c.sendMessage(wsReq)
 | ||
| }
 | ||
| 
 | ||
| // normalizeSubscribeReq 标准化订阅请求
 | ||
| func (c *BitgetMarketClient) normalizeSubscribeReq(req SubscribeReq) SubscribeReq {
 | ||
| 	return SubscribeReq{
 | ||
| 		InstType: strings.ToUpper(req.InstType),
 | ||
| 		Channel:  strings.ToLower(req.Channel),
 | ||
| 		InstId:   strings.ToUpper(req.InstId),
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // sendMessage 发送消息
 | ||
| func (c *BitgetMarketClient) sendMessage(msg interface{}) error {
 | ||
| 	if c.conn == nil {
 | ||
| 		return errors.New("WebSocket连接不可用")
 | ||
| 	}
 | ||
| 
 | ||
| 	c.sendMutex.Lock()
 | ||
| 	defer c.sendMutex.Unlock()
 | ||
| 
 | ||
| 	data, err := sonic.Marshal(msg)
 | ||
| 	if err != nil {
 | ||
| 		return fmt.Errorf("序列化消息失败: %v", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	log.Info("发送消息: %s", string(data))
 | ||
| 
 | ||
| 	c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout))
 | ||
| 	return c.conn.WriteMessage(websocket.TextMessage, data)
 | ||
| }
 | ||
| 
 | ||
| // readLoop 消息读取循环
 | ||
| func (c *BitgetMarketClient) readLoop() {
 | ||
| 	defer func() {
 | ||
| 		if r := recover(); r != nil {
 | ||
| 			log.Error("readLoop panic: %v", r)
 | ||
| 		}
 | ||
| 	}()
 | ||
| 
 | ||
| 	for {
 | ||
| 		select {
 | ||
| 		case <-c.ctx.Done():
 | ||
| 			return
 | ||
| 		default:
 | ||
| 			if c.conn == nil {
 | ||
| 				time.Sleep(time.Second)
 | ||
| 				continue
 | ||
| 			}
 | ||
| 
 | ||
| 			c.conn.SetReadDeadline(time.Now().Add(c.config.ReadTimeout))
 | ||
| 			_, message, err := c.conn.ReadMessage()
 | ||
| 			if err != nil {
 | ||
| 				log.Error("读取消息失败: %v", err)
 | ||
| 				c.handleDisconnect()
 | ||
| 				continue
 | ||
| 			}
 | ||
| 
 | ||
| 			c.handleMessage(string(message))
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // handleMessage 处理接收到的消息
 | ||
| func (c *BitgetMarketClient) handleMessage(message string) {
 | ||
| 	log.Info("收到消息: %s", message)
 | ||
| 
 | ||
| 	// 处理pong消息
 | ||
| 	if message == "pong" {
 | ||
| 		c.lastPongTime = time.Now()
 | ||
| 		log.Debug("收到pong消息")
 | ||
| 		return
 | ||
| 	}
 | ||
| 
 | ||
| 	// 解析JSON消息
 | ||
| 	var jsonMap map[string]interface{}
 | ||
| 	if err := json.Unmarshal([]byte(message), &jsonMap); err != nil {
 | ||
| 		log.Error("解析JSON消息失败: %v", err)
 | ||
| 		if c.errorListener != nil {
 | ||
| 			c.errorListener(message)
 | ||
| 		}
 | ||
| 		return
 | ||
| 	}
 | ||
| 
 | ||
| 	// 检查错误码
 | ||
| 	if code, exists := jsonMap["code"]; exists {
 | ||
| 		if codeNum, ok := code.(float64); ok && int(codeNum) != 0 {
 | ||
| 			log.Error("收到错误消息: %s", message)
 | ||
| 			if c.errorListener != nil {
 | ||
| 				c.errorListener(message)
 | ||
| 			}
 | ||
| 			return
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	// 处理订阅确认消息
 | ||
| 	if event, exists := jsonMap["event"]; exists && event == "subscribe" {
 | ||
| 		log.Info("订阅确认: %s", message)
 | ||
| 		return
 | ||
| 	}
 | ||
| 
 | ||
| 	// 处理数据推送
 | ||
| 	if data, exists := jsonMap["data"]; exists && data != nil {
 | ||
| 		if arg, argExists := jsonMap["arg"]; argExists {
 | ||
| 			listener := c.getListener(arg)
 | ||
| 			if listener != nil {
 | ||
| 				listener(message)
 | ||
| 			}
 | ||
| 		}
 | ||
| 		return
 | ||
| 	}
 | ||
| 
 | ||
| 	// 默认处理
 | ||
| 	if c.listener != nil {
 | ||
| 		c.listener(message)
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // getListener 获取对应的监听器
 | ||
| func (c *BitgetMarketClient) getListener(argJson interface{}) OnReceive {
 | ||
| 	argMap, ok := argJson.(map[string]interface{})
 | ||
| 	if !ok {
 | ||
| 		return c.listener
 | ||
| 	}
 | ||
| 
 | ||
| 	subscribeReq := SubscribeReq{
 | ||
| 		InstType: fmt.Sprintf("%v", argMap["instType"]),
 | ||
| 		Channel:  fmt.Sprintf("%v", argMap["channel"]),
 | ||
| 		InstId:   fmt.Sprintf("%v", argMap["instId"]),
 | ||
| 	}
 | ||
| 
 | ||
| 	if listener, exists := c.subscriptions[subscribeReq]; exists {
 | ||
| 		return listener
 | ||
| 	}
 | ||
| 
 | ||
| 	return c.listener
 | ||
| }
 | ||
| 
 | ||
| // pingLoop Ping循环
 | ||
| func (c *BitgetMarketClient) pingLoop() {
 | ||
| 	ticker := time.NewTicker(c.config.PingInterval)
 | ||
| 	defer ticker.Stop()
 | ||
| 
 | ||
| 	for {
 | ||
| 		select {
 | ||
| 		case <-c.ctx.Done():
 | ||
| 			return
 | ||
| 		case <-ticker.C:
 | ||
| 			if c.connected && c.conn != nil {
 | ||
| 				c.sendMutex.Lock()
 | ||
| 				c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout))
 | ||
| 				err := c.conn.WriteMessage(websocket.TextMessage, []byte("ping"))
 | ||
| 				c.sendMutex.Unlock()
 | ||
| 
 | ||
| 				if err != nil {
 | ||
| 					log.Error("发送ping失败: %v", err)
 | ||
| 					c.handleDisconnect()
 | ||
| 				}
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // monitorConnection 监控连接状态
 | ||
| func (c *BitgetMarketClient) monitorConnection() {
 | ||
| 	ticker := time.NewTicker(30 * time.Second)
 | ||
| 	defer ticker.Stop()
 | ||
| 
 | ||
| 	for {
 | ||
| 		select {
 | ||
| 		case <-c.ctx.Done():
 | ||
| 			return
 | ||
| 		case <-ticker.C:
 | ||
| 			if c.connected && time.Since(c.lastPongTime) > 60*time.Second {
 | ||
| 				log.Warn("长时间未收到pong消息,触发重连")
 | ||
| 				c.handleDisconnect()
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // handleDisconnect 处理断开连接
 | ||
| func (c *BitgetMarketClient) handleDisconnect() {
 | ||
| 	c.reconnectMutex.Lock()
 | ||
| 	defer c.reconnectMutex.Unlock()
 | ||
| 
 | ||
| 	if !c.connected {
 | ||
| 		return
 | ||
| 	}
 | ||
| 
 | ||
| 	log.Warn("WebSocket连接断开,准备重连")
 | ||
| 	c.connected = false
 | ||
| 
 | ||
| 	if c.conn != nil {
 | ||
| 		c.conn.Close()
 | ||
| 		c.conn = nil
 | ||
| 	}
 | ||
| 
 | ||
| 	// 重连逻辑
 | ||
| 	go c.reconnect()
 | ||
| }
 | ||
| 
 | ||
| // reconnect 重连
 | ||
| func (c *BitgetMarketClient) reconnect() {
 | ||
| 	for c.reconnectCount < c.config.MaxReconnectTimes {
 | ||
| 		select {
 | ||
| 		case <-c.ctx.Done():
 | ||
| 			return
 | ||
| 		default:
 | ||
| 			c.reconnectCount++
 | ||
| 			log.Info("尝试重连 (%d/%d)", c.reconnectCount, c.config.MaxReconnectTimes)
 | ||
| 
 | ||
| 			time.Sleep(c.config.ReconnectInterval)
 | ||
| 
 | ||
| 			if err := c.Connect(); err != nil {
 | ||
| 				log.Error("重连失败: %v", err)
 | ||
| 				continue
 | ||
| 			}
 | ||
| 
 | ||
| 			// 重新订阅
 | ||
| 			if err := c.resubscribe(); err != nil {
 | ||
| 				log.Error("重新订阅失败: %v", err)
 | ||
| 				c.handleDisconnect()
 | ||
| 				continue
 | ||
| 			}
 | ||
| 
 | ||
| 			log.Info("重连成功")
 | ||
| 			return
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	log.Error("达到最大重连次数,停止重连")
 | ||
| }
 | ||
| 
 | ||
| // resubscribe 重新订阅
 | ||
| func (c *BitgetMarketClient) resubscribe() error {
 | ||
| 	if len(c.subscriptions) == 0 {
 | ||
| 		return nil
 | ||
| 	}
 | ||
| 
 | ||
| 	var args []interface{}
 | ||
| 	for req := range c.subscriptions {
 | ||
| 		args = append(args, req)
 | ||
| 	}
 | ||
| 
 | ||
| 	wsReq := WsBaseReq{
 | ||
| 		Op:   "subscribe",
 | ||
| 		Args: args,
 | ||
| 	}
 | ||
| 
 | ||
| 	return c.sendMessage(wsReq)
 | ||
| }
 | ||
| 
 | ||
| // Close 关闭客户端
 | ||
| func (c *BitgetMarketClient) Close() error {
 | ||
| 	c.cancel()
 | ||
| 	c.connected = false
 | ||
| 
 | ||
| 	if c.conn != nil {
 | ||
| 		return c.conn.Close()
 | ||
| 	}
 | ||
| 
 | ||
| 	return nil
 | ||
| }
 | ||
| 
 | ||
| // IsConnected 检查连接状态
 | ||
| func (c *BitgetMarketClient) IsConnected() bool {
 | ||
| 	return c.connected
 | ||
| }
 |