1、有更新,还没测完,暂时归档

This commit is contained in:
2025-11-05 16:26:21 +08:00
parent 643eab3496
commit bd7a6d3870
15 changed files with 536 additions and 296 deletions

View File

@ -11,8 +11,6 @@ import (
"go-admin/pkg/jsonhelper"
"go-admin/pkg/utility"
"go-admin/services/proxy"
"math/rand"
"strconv"
"strings"
"sync"
"sync/atomic"
@ -35,7 +33,7 @@ type BinanceWebSocketManager struct {
proxyType string
proxyAddress string
reconnect chan struct{}
isStopped bool
isStopped atomic.Bool
cancelFunc context.CancelFunc
listenKey string
reconnecting atomic.Bool
@ -44,8 +42,6 @@ type BinanceWebSocketManager struct {
forceReconnectTimer *time.Timer
}
// ... (省略中间代码)
// sendPing 使用锁来确保发送ping时的线程安全
func (wm *BinanceWebSocketManager) sendPing() error {
wm.mu.Lock()
@ -54,18 +50,19 @@ func (wm *BinanceWebSocketManager) sendPing() error {
return errors.New("websocket connection is nil")
}
wm.lastPingTime.Store(time.Now())
return wm.ws.WriteMessage(websocket.PingMessage, []byte("ping"))
// 使用控制帧发送 Ping避免与普通消息写入竞争
return wm.ws.WriteControl(websocket.PingMessage, []byte("ping"), time.Now().Add(10*time.Second))
}
// Stop 优雅地停止WebSocket管理器
func (wm *BinanceWebSocketManager) Stop() {
wm.stopOnce.Do(func() {
wm.mu.Lock()
if wm.isStopped {
if wm.isStopped.Load() {
wm.mu.Unlock()
return
}
wm.isStopped = true
wm.isStopped.Store(true)
wm.mu.Unlock()
if wm.cancelFunc != nil {
@ -92,9 +89,11 @@ func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) {
for {
select {
case <-wm.reconnect:
if !wm.reconnecting.CompareAndSwap(false, true) {
continue // 如果已经在重连,则忽略
}
// 当收到重连信号时,不再依赖 CompareAndSwap 直接跳过的逻辑。
// 之前的实现中triggerReconnect 已将 reconnecting 标记为 true
// 导致这里 CompareAndSwap 失败而 continue从而丢失首次重连信号。
// 修复:收到信号后直接置位为重连状态并继续执行重连流程,保证首次重连一定发生。
wm.reconnecting.Store(true)
// 在开始重连循环之前,先安全地关闭旧的连接
wm.closeConn()
@ -108,10 +107,11 @@ func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) {
default:
}
log.Infof("WebSocket (%s) 正在尝试重连,第 %d 次...", wm.wsType, retryCount+1)
log.Infof("WebSocket (%d) 正在尝试重连,第 %d 次...", wm.wsType, retryCount+1)
if err := wm.connect(ctx); err == nil {
log.Infof("WebSocket (%s) 重连成功", wm.wsType)
log.Infof("WebSocket (%d) 重连成功", wm.wsType)
wm.reconnecting.Store(false)
setLastTime(wm)
go wm.startDeadCheck(ctx) // 重连成功后,重新启动假死检测
break // 跳出重连循环
}
@ -163,7 +163,6 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA
wm := &BinanceWebSocketManager{
stopChannel: make(chan struct{}, 10),
reconnect: make(chan struct{}, 10),
isStopped: false,
url: url,
wsType: wsType,
apiKey: apiKey,
@ -174,6 +173,7 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA
// 初始化最后ping时间
wm.lastPingTime.Store(time.Now())
wm.isStopped.Store(false)
return wm
}
@ -210,8 +210,8 @@ func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAd
wm.proxyType = proxyType
wm.proxyAddress = proxyAddress
if wm.isStopped {
wm.isStopped = false
if wm.isStopped.Load() {
wm.isStopped.Store(false)
utility.SafeGo(wm.run)
} else {
log.Warnf("调用restart")
@ -265,6 +265,10 @@ func (wm *BinanceWebSocketManager) run() {
errMessage := commondto.WebSocketErr{Time: time.Now()}
helper.DefaultRedis.SetString(errKey, jsonhelper.ToJsonString(errMessage))
// 在主循环前统一启动重连与假死检测,避免重复启动
utility.SafeGo(func() { wm.handleReconnect(ctx) })
utility.SafeGo(func() { wm.startDeadCheck(ctx) })
for {
select {
case <-ctx.Done():
@ -274,7 +278,7 @@ func (wm *BinanceWebSocketManager) run() {
wm.handleConnectionError(errKey, err)
if wm.isErrorCountExceeded(errKey) {
log.Error("连接 %s WebSocket 时出错次数过多,停止 WebSocket 管理器: %v", wm.wsType, wm.apiKey)
log.Errorf("连接 %s WebSocket 时出错次数过多,停止 WebSocket 管理器: %v", wm.wsType, wm.apiKey)
wm.Stop()
return
}
@ -284,7 +288,7 @@ func (wm *BinanceWebSocketManager) run() {
}
<-wm.stopChannel
log.Info("停止 %s WebSocket 管理器...", getWsTypeName(wm.wsType))
log.Infof("停止 %s WebSocket 管理器...", getWsTypeName(wm.wsType))
wm.Stop()
return
}
@ -315,7 +319,7 @@ func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err erro
}
// 记录错误日志
log.Error("连接 %s WebSocket 时出错: %v, 错误: %v", wm.wsType, wm.apiKey, err)
log.Errorf("连接 %s WebSocket 时出错: %v, 错误: %v", wm.wsType, wm.apiKey, err)
}
/**
@ -363,34 +367,51 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
// 连接成功更新连接时间和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))
setLastTime(wm)
log.Infof("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey)
// 设置读超时
var readTimeout time.Duration
if wm.wsType == 0 {
readTimeout = 2 * time.Minute
} else {
readTimeout = 4 * time.Minute
}
_ = wm.ws.SetReadDeadline(time.Now().Add(readTimeout))
// Ping处理
wm.ws.SetPingHandler(func(appData string) error {
log.Info(fmt.Sprintf("收到 wstype: %v key:%s Ping 消息【%s】", wm.wsType, wm.apiKey, appData))
log.Infof("收到 wstype: %d key:%s Ping 消息【%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("binance 回应pong失败 次数:", strconv.Itoa(x), " err:", err)
log.Errorf("binance 回应pong失败 次数:%d err:%v", x, err)
time.Sleep(time.Second * 1)
continue
}
break
}
// 更新ping时间和Redis记录
// 更新ping时间和Redis记录,并刷新读超时
wm.lastPingTime.Store(time.Now())
setLastTime(wm)
_ = wm.ws.SetReadDeadline(time.Now().Add(readTimeout))
return nil
})
// 启动各种协程
// Pong处理收到服务端Pong时刷新心跳与读超时
wm.ws.SetPongHandler(func(appData string) error {
wm.lastPingTime.Store(time.Now())
setLastTime(wm)
_ = wm.ws.SetReadDeadline(time.Now().Add(readTimeout))
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.start24HourReconnectTimer(ctx) }) // 启动24小时强制重连
utility.SafeGo(func() { wm.start24HourReconnectTimer(ctx) }) // 启动23小时强制重连
return nil
}
@ -398,7 +419,7 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
// ReplaceConnection 创建新连接并关闭旧连接,实现无缝连接替换
func (wm *BinanceWebSocketManager) ReplaceConnection() error {
wm.mu.Lock()
if wm.isStopped {
if wm.isStopped.Load() {
wm.mu.Unlock()
return errors.New("WebSocket 已停止")
}
@ -430,7 +451,7 @@ func (wm *BinanceWebSocketManager) ReplaceConnection() error {
// 设置 ping handler
newConn.SetPingHandler(func(appData string) error {
log.Infof("收到 Ping新连接 key:%s msg:%s", wm.apiKey, appData)
// log.Infof("收到 Ping新连接 key:%s msg:%s", wm.apiKey, appData)
for x := 0; x < 5; x++ {
if err := newConn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(10*time.Second)); err != nil {
log.Errorf("Pong 失败 %d 次 err:%v", x, err)
@ -456,7 +477,6 @@ func (wm *BinanceWebSocketManager) ReplaceConnection() error {
// 步骤 4启动新连接协程
go wm.startListenKeyRenewal2(newCtx)
go wm.readMessages(newCtx)
go wm.handleReconnect(newCtx)
go wm.startPingLoop(newCtx)
// go wm.startDeadCheck(newCtx)
@ -529,8 +549,12 @@ func (wm *BinanceWebSocketManager) getListenKey() (string, error) {
return "", err
}
if listenKey, ok := dataMap["listenKey"]; ok {
return listenKey.(string), nil
if v, ok := dataMap["listenKey"]; ok {
s, ok2 := v.(string)
if !ok2 || s == "" {
return "", errors.New("listenKey 类型错误或为空")
}
return s, nil
}
return "", errors.New("listenKey 不存在")
@ -544,24 +568,24 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
case <-ctx.Done():
return
default:
if wm.isStopped {
if wm.isStopped.Load() {
return
}
_, msg, err := wm.ws.ReadMessage()
if err != nil {
// 检查是否是由于我们主动关闭连接导致的错误
if wm.isStopped {
if wm.isStopped.Load() {
log.Infof("WebSocket read loop gracefully stopped for key: %s", wm.apiKey)
return
}
// 如果不是主动关闭,再判断是否是连接关闭错误,并触发重连
if strings.Contains(err.Error(), "websocket: close") {
log.Error("WebSocket connection closed unexpectedly, triggering reconnect for key: %s. Error: %v", wm.apiKey, err)
log.Errorf("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)
log.Errorf("Error reading message for key: %s. Error: %v", wm.apiKey, err)
}
return // 任何错误发生后都应退出读取循环
}
@ -611,11 +635,11 @@ func (wm *BinanceWebSocketManager) startPingLoop(ctx context.Context) {
case <-ctx.Done():
return
case <-ticker.C:
if wm.isStopped {
if wm.isStopped.Load() {
return
}
if err := wm.sendPing(); err != nil {
log.Error("主动 Ping Binance 失败:", err)
log.Errorf("主动 Ping Binance 失败: %v", err)
wm.triggerReconnect(false)
return // 退出循环,等待重连
}
@ -623,24 +647,6 @@ func (wm *BinanceWebSocketManager) startPingLoop(ctx context.Context) {
}
}
// 定期删除listenkey 并重启ws
// func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) {
// time.Sleep(30 * time.Minute)
// select {
// case <-ctx.Done():
// return
// default:
// if err := wm.deleteListenKey(listenKey); err != nil {
// log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
// } else {
// log.Debug("Successfully delete listenKey")
// wm.triggerReconnect()
// }
// }
// }
// 定时续期
func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
ticker := time.NewTicker(30 * time.Minute)
@ -652,12 +658,12 @@ func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
for {
select {
case <-ticker.C:
if wm.isStopped {
if wm.isStopped.Load() {
return
}
if err := wm.renewListenKey(wm.listenKey); err != nil {
log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
log.Errorf("Failed to renew listenKey: ,type:%v key: %s err:%v", wm.wsType, wm.apiKey, err)
}
case <-ctx.Done():
return
@ -665,36 +671,6 @@ func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
}
}
/*
删除listenkey
*/
// func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error {
// client, err := wm.createBinanceClient()
// if err != nil {
// return err
// }
// var resp []byte
// switch wm.wsType {
// case 0:
// path := fmt.Sprintf("/api/v3/userDataStream")
// params := map[string]interface{}{
// "listenKey": listenKey,
// }
// resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params)
// log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
// case 1:
// resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil)
// log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
// default:
// return errors.New("unknown ws type")
// }
// return err
// }
func (wm *BinanceWebSocketManager) renewListenKey(listenKey string) error {
client, err := wm.createBinanceClient()
if err != nil {
@ -728,19 +704,6 @@ func getWsTypeName(wsType int) string {
return "unknown"
}
}
func getUUID() string {
return fmt.Sprintf("%s-%s-%s-%s-%s", randomHex(8), randomHex(4), randomHex(4), randomHex(4), randomHex(12))
}
func randomHex(n int) string {
rand.New(rand.NewSource(time.Now().UnixNano()))
hexChars := "0123456789abcdef"
bytes := make([]byte, n)
for i := 0; i < n; i++ {
bytes[i] = hexChars[rand.Intn(len(hexChars))]
}
return string(bytes)
}
/**
* 启动24小时强制重连定时器
@ -748,8 +711,9 @@ func randomHex(n int) string {
*/
func (wm *BinanceWebSocketManager) start24HourReconnectTimer(ctx context.Context) {
// Binance要求不到24小时就需要主动断开重连
// 设置为23小时50分钟,留出一些缓冲时间
duration := 23*time.Hour + 50*time.Minute
// 设置为23小时留出一些缓冲时间
// duration := 23 * time.Hour
duration := 10 * time.Minute
wm.forceReconnectTimer = time.NewTimer(duration)
defer func() {
@ -762,8 +726,8 @@ func (wm *BinanceWebSocketManager) start24HourReconnectTimer(ctx context.Context
case <-ctx.Done():
return
case <-wm.forceReconnectTimer.C:
if !wm.isStopped {
log.Warnf("24小时强制重连触发 key:%s wsType:%v", wm.apiKey, wm.wsType)
if !wm.isStopped.Load() {
log.Warnf("23小时强制重连触发 key:%s wsType:%v", wm.apiKey, wm.wsType)
wm.triggerReconnect(true)
}
}
@ -783,7 +747,7 @@ func (wm *BinanceWebSocketManager) startDeadCheck(ctx context.Context) {
case <-ctx.Done():
return
case <-ticker.C:
if wm.isStopped {
if wm.isStopped.Load() {
return
}
wm.DeadCheck()
@ -845,8 +809,3 @@ func (wm *BinanceWebSocketManager) DeadCheck() {
wm.triggerReconnect(true)
}
}
/**
* 优化的重连处理逻辑
* @param ctx 上下文
*/

View File

@ -97,26 +97,31 @@ func (bnWs *BinanceWs) Subscribe(streamName string, tradeSet models.TradeSet, ca
}
func (bnWs *BinanceWs) exitHandler(c *WsConn) {
pingTicker := time.NewTicker(1 * time.Minute)
pongTicker := time.NewTicker(30 * time.Second)
defer func() {
pingTicker.Stop()
pongTicker.Stop()
c.CloseWs()
// exitHandler 维护对底层连接的 Ping/Pong 心跳发送
// 修复点:监听 c.close 通道,当连接关闭时退出循环,防止 goroutine 长期驻留导致内存泄漏
pingTicker := time.NewTicker(1 * time.Minute)
pongTicker := time.NewTicker(30 * time.Second)
defer func() {
pingTicker.Stop()
pongTicker.Stop()
c.CloseWs()
if err := recover(); err != nil {
fmt.Printf("CloseWs, panic: %s\r\n", err)
}
}()
for {
select {
case t := <-pingTicker.C:
c.SendPingMessage([]byte(strconv.Itoa(int(t.UnixNano() / int64(time.Millisecond)))))
case t := <-pongTicker.C:
c.SendPongMessage([]byte(strconv.Itoa(int(t.UnixNano() / int64(time.Millisecond)))))
}
}
for {
select {
case <-c.close:
// 连接关闭,退出心跳协程
return
case t := <-pingTicker.C:
c.SendPingMessage([]byte(strconv.Itoa(int(t.UnixNano() / int64(time.Millisecond)))))
case t := <-pongTicker.C:
c.SendPongMessage([]byte(strconv.Itoa(int(t.UnixNano() / int64(time.Millisecond)))))
}
}
}
func parseJsonToMap(msg []byte) (map[string]interface{}, error) {

View File

@ -262,11 +262,13 @@ func (ws *WsConn) reconnect() {
}
}
// writeRequest 负责写出文本/控制帧以及心跳发送
// 修复点:在函数退出时停止 heartTimer避免底层定时器资源长时间存留
func (ws *WsConn) writeRequest() {
var (
heartTimer *time.Timer
err error
)
var (
heartTimer *time.Timer
err error
)
if ws.HeartbeatIntervalTime == 0 {
heartTimer = time.NewTimer(time.Hour)
@ -274,31 +276,37 @@ func (ws *WsConn) writeRequest() {
heartTimer = time.NewTimer(ws.HeartbeatIntervalTime)
}
for {
select {
case <-ws.close:
log.Info("[ws][" + ws.WsUrl + "] close websocket , exiting write message goroutine.")
return
case d := <-ws.writeBufferChan:
err = ws.c.WriteMessage(websocket.TextMessage, d)
case d := <-ws.pingMessageBufferChan:
err = ws.c.WriteMessage(websocket.PingMessage, d)
case d := <-ws.pongMessageBufferChan:
err = ws.c.WriteMessage(websocket.PongMessage, d)
case d := <-ws.closeMessageBufferChan:
err = ws.c.WriteMessage(websocket.CloseMessage, d)
case <-heartTimer.C:
if ws.HeartbeatIntervalTime > 0 {
err = ws.c.WriteMessage(websocket.TextMessage, ws.HeartbeatData())
heartTimer.Reset(ws.HeartbeatIntervalTime)
}
}
defer func() {
if heartTimer != nil {
heartTimer.Stop()
}
}()
if err != nil {
log.Info("[ws][" + ws.WsUrl + "] write message " + err.Error())
//time.Sleep(time.Second)
}
}
for {
select {
case <-ws.close:
log.Info("[ws][" + ws.WsUrl + "] close websocket , exiting write message goroutine.")
return
case d := <-ws.writeBufferChan:
err = ws.c.WriteMessage(websocket.TextMessage, d)
case d := <-ws.pingMessageBufferChan:
err = ws.c.WriteMessage(websocket.PingMessage, d)
case d := <-ws.pongMessageBufferChan:
err = ws.c.WriteMessage(websocket.PongMessage, d)
case d := <-ws.closeMessageBufferChan:
err = ws.c.WriteMessage(websocket.CloseMessage, d)
case <-heartTimer.C:
if ws.HeartbeatIntervalTime > 0 {
err = ws.c.WriteMessage(websocket.TextMessage, ws.HeartbeatData())
heartTimer.Reset(ws.HeartbeatIntervalTime)
}
}
if err != nil {
log.Info("[ws][" + ws.WsUrl + "] write message " + err.Error())
//time.Sleep(time.Second)
}
}
}
func (ws *WsConn) Subscribe(subEvent interface{}) error {