659 lines
16 KiB
Go
659 lines
16 KiB
Go
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()
|
||
}
|
||
}
|