Initial commit
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package exchange
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// PriceConnector is a reusable WebSocket reconnector with backoff and keepalive.
|
||||
type PriceConnector struct {
|
||||
URL string
|
||||
Name string
|
||||
ReadTimeout time.Duration
|
||||
ReconnectBase time.Duration
|
||||
PingInterval time.Duration // 0 = no client-side pings
|
||||
|
||||
OnConnect func()
|
||||
OnMessage func([]byte)
|
||||
OnError func(error)
|
||||
|
||||
conn *websocket.Conn
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewPriceConnector(url, name string, readTimeout, reconnectBase time.Duration) *PriceConnector {
|
||||
return &PriceConnector{
|
||||
URL: url,
|
||||
Name: name,
|
||||
ReadTimeout: readTimeout,
|
||||
ReconnectBase: reconnectBase,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (pc *PriceConnector) Run() error {
|
||||
backoff := time.Second
|
||||
attempt := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pc.done:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
log.Printf("[%s WS] Connecting... (attempt %d)", pc.Name, attempt+1)
|
||||
|
||||
c, _, err := websocket.DefaultDialer.Dial(pc.URL, nil)
|
||||
if err != nil {
|
||||
log.Printf("[%s WS] Dial error: %v (retry in %v)", pc.Name, err, backoff)
|
||||
if pc.OnError != nil {
|
||||
pc.OnError(err)
|
||||
}
|
||||
time.Sleep(backoff)
|
||||
backoff *= 2
|
||||
if backoff > 30*time.Second {
|
||||
backoff = 30 * time.Second
|
||||
}
|
||||
attempt++
|
||||
continue
|
||||
}
|
||||
|
||||
backoff = time.Second
|
||||
attempt = 0
|
||||
pc.conn = c
|
||||
|
||||
// ── Ping/Pong keepalive ──
|
||||
c.SetReadDeadline(time.Now().Add(pc.ReadTimeout))
|
||||
|
||||
// Respond to server pings with pongs (extend deadline)
|
||||
c.SetPongHandler(func(appData string) error {
|
||||
c.SetReadDeadline(time.Now().Add(pc.ReadTimeout))
|
||||
return nil
|
||||
})
|
||||
|
||||
// Handle server-initiated PING frames (used by Bitget etc.)
|
||||
// Extend read deadline so the connection doesn't timeout
|
||||
c.SetPingHandler(func(appData string) error {
|
||||
c.SetReadDeadline(time.Now().Add(pc.ReadTimeout))
|
||||
return nil
|
||||
})
|
||||
|
||||
// Client-side ping sender
|
||||
pingStop := make(chan struct{})
|
||||
if pc.PingInterval > 0 {
|
||||
go func() {
|
||||
ticker := time.NewTicker(pc.PingInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := c.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
case <-pingStop:
|
||||
return
|
||||
case <-pc.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if pc.OnConnect != nil {
|
||||
pc.OnConnect()
|
||||
}
|
||||
|
||||
// Read loop
|
||||
readLoop:
|
||||
for {
|
||||
_, msg, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("[%s WS] Read error: %v", pc.Name, err)
|
||||
break readLoop
|
||||
}
|
||||
c.SetReadDeadline(time.Now().Add(pc.ReadTimeout))
|
||||
|
||||
if pc.OnMessage != nil {
|
||||
pc.OnMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
close(pingStop)
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (pc *PriceConnector) Stop() {
|
||||
close(pc.done)
|
||||
if pc.conn != nil {
|
||||
pc.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Done returns a channel that's closed when the connector is stopped.
|
||||
func (pc *PriceConnector) Done() <-chan struct{} {
|
||||
return pc.done
|
||||
}
|
||||
|
||||
// SendJSON sends a JSON message over the WebSocket.
|
||||
func (pc *PriceConnector) SendJSON(v interface{}) error {
|
||||
if pc.conn == nil {
|
||||
return nil
|
||||
}
|
||||
return pc.conn.WriteJSON(v)
|
||||
}
|
||||
|
||||
// Helper: parse float from string
|
||||
func parseFloat(s string) float64 {
|
||||
var f float64
|
||||
if _, err := fmt.Sscanf(s, "%f", &f); err != nil {
|
||||
return 0
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// Helper: convert "BTCUSDT" with suffix "USDT" to "BTC"
|
||||
func symbolToCoin(symbol, suffix string) string {
|
||||
if len(symbol) <= len(suffix) {
|
||||
return ""
|
||||
}
|
||||
if symbol[len(symbol)-len(suffix):] == suffix {
|
||||
return symbol[:len(symbol)-len(suffix)]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user