package main
import (
"context"
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
type WebSocketClient struct {
url string
conn *websocket.Conn
sendChan chan []byte
reconnectChan chan bool
closeChan chan struct{}
mutex sync.RWMutex
connected bool
pingInterval time.Duration
pongWait time.Duration
}
func NewWebSocketClient(url string) *WebSocketClient {
return &WebSocketClient{
url: url,
sendChan: make(chan []byte, 100),
reconnectChan: make(chan bool, 1),
closeChan: make(chan struct{}),
pingInterval: 30 * time.Second,
pongWait: 60 * time.Second,
}
}
func (wsc *WebSocketClient) Connect() error {
conn, _, err := websocket.DefaultDialer.Dial(wsc.url, nil)
if err != nil {
return fmt.Errorf("failed to connect: %v", err)
}
wsc.mutex.Lock()
wsc.conn = conn
wsc.connected = true
wsc.mutex.Unlock()
// 启动读取、写入和心跳协程
go wsc.readPump()
go wsc.writePump()
go wsc.heartbeat()
log.Println("WebSocket connected successfully")
return nil
}
func (wsc *WebSocketClient) readPump() {
defer func() {
wsc.mutex.Lock()
wsc.connected = false
wsc.conn.Close()
wsc.mutex.Unlock()
log.Println("Read pump stopped")
}()
// 设置读取超时
wsc.conn.SetReadDeadline(time.Now().Add(wsc.pongWait))
wsc.conn.SetPongHandler(func(string) error {
wsc.conn.SetReadDeadline(time.Now().Add(wsc.pongWait))
return nil
})
for {
select {
case <-wsc.closeChan:
return
default:
messageType, message, err := wsc.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("WebSocket error: %v", err)
}
wsc.triggerReconnect()
return
}
switch messageType {
case websocket.TextMessage:
log.Printf("Received text message: %s", string(message))
case websocket.BinaryMessage:
log.Printf("Received binary message: %d bytes", len(message))
case websocket.PingMessage:
wsc.conn.WriteMessage(websocket.PongMessage, []byte{})
}
}
}
}
func (wsc *WebSocketClient) writePump() {
ticker := time.NewTicker(wsc.pingInterval)
defer func() {
ticker.Stop()
wsc.conn.Close()
}()
for {
select {
case message, ok := <-wsc.sendChan:
wsc.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
wsc.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
err := wsc.conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
log.Printf("Write error: %v", err)
wsc.triggerReconnect()
return
}
case <-ticker.C:
wsc.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := wsc.conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
log.Printf("Ping error: %v", err)
wsc.triggerReconnect()
return
}
case <-wsc.closeChan:
wsc.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
}
}
func (wsc *WebSocketClient) heartbeat() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
wsc.SendPing()
case <-wsc.closeChan:
return
}
}
}
func (wsc *WebSocketClient) SendPing() {
wsc.mutex.RLock()
connected := wsc.connected
conn := wsc.conn
wsc.mutex.RUnlock()
if connected && conn != nil {
conn.WriteMessage(websocket.TextMessage, []byte("{\"type\":\"ping\",\"timestamp\":"+fmt.Sprintf("%d", time.Now().Unix())+"}"))
}
}
func (wsc *WebSocketClient) SendText(message string) error {
wsc.mutex.RLock()
connected := wsc.connected
wsc.mutex.RUnlock()
if !connected {
return fmt.Errorf("WebSocket is not connected")
}
select {
case wsc.sendChan <- []byte(message):
return nil
case <-time.After(5 * time.Second):
return fmt.Errorf("send timeout")
}
}
func (wsc *WebSocketClient) SendBinary(data []byte) error {
wsc.mutex.RLock()
connected := wsc.connected
wsc.mutex.RUnlock()
if !connected {
return fmt.Errorf("WebSocket is not connected")
}
select {
case wsc.sendChan <- data:
return nil
case <-time.After(5 * time.Second):
return fmt.Errorf("send timeout")
}
}
func (wsc *WebSocketClient) triggerReconnect() {
select {
case wsc.reconnectChan <- true:
default:
// 避免阻塞
}
}
func (wsc *WebSocketClient) reconnectLoop() {
maxReconnectAttempts := 10
reconnectDelay := 5 * time.Second
for attempt := 1; attempt <= maxReconnectAttempts; attempt++ {
log.Printf("Attempting to reconnect... (attempt %d/%d)", attempt, maxReconnectAttempts)
err := wsc.Connect()
if err == nil {
log.Println("Reconnected successfully")
return
}
log.Printf("Reconnect failed: %v, waiting %v before next attempt", err, reconnectDelay)
time.Sleep(reconnectDelay)
reconnectDelay = time.Duration(float64(reconnectDelay) * 1.5) // 指数退避
}
log.Printf("Max reconnect attempts reached (%d), giving up", maxReconnectAttempts)
}
func (wsc *WebSocketClient) Start() {
// 初始连接
if err := wsc.Connect(); err != nil {
log.Printf("Initial connection failed: %v, will attempt to reconnect", err)
wsc.reconnectLoop()
}
// 监听重连信号
go func() {
for {
select {
case <-wsc.reconnectChan:
wsc.reconnectLoop()
case <-wsc.closeChan:
return
}
}
}()
}
func (wsc *WebSocketClient) Close() {
close(wsc.closeChan)
wsc.mutex.Lock()
if wsc.conn != nil {
wsc.conn.Close()
}
wsc.connected = false
wsc.mutex.Unlock()
}
func (wsc *WebSocketClient) IsConnected() bool {
wsc.mutex.RLock()
connected := wsc.connected
wsc.mutex.RUnlock()
return connected
}
// 使用示例
func main() {
// 创建WebSocket客户端
client := NewWebSocketClient("ws://localhost:8080/ws")
// 启动客户端
go client.Start()
// 等待连接建立
time.Sleep(2 * time.Second)
// 发送消息示例
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
counter := 0
for {
select {
case <-ticker.C:
counter++
message := fmt.Sprintf("{\"type\":\"message\",\"id\":%d,\"content\":\"Hello from client\",\"timestamp\":%d}",
counter, time.Now().Unix())
if err := client.SendText(message); err != nil {
log.Printf("Failed to send message: %v", err)
} else {
log.Printf("Message sent: %s", message)
}
case <-client.closeChan:
return
}
}
}()
// 模拟程序运行
time.Sleep(60 * time.Second)
// 关闭客户端
client.Close()
}