package ws import ( "crypto/tls" "fmt" "log" "net/url" "time" "github.com/gorilla/websocket" ) type Client struct { backendURL string agentID string apiKey string insecure bool conn *websocket.Conn handler *Handler reconnectWait time.Duration } func NewClient(backendURL, agentID, apiKey string, insecure bool, handler *Handler) *Client { return &Client{ backendURL: backendURL, agentID: agentID, apiKey: apiKey, insecure: insecure, handler: handler, reconnectWait: 5 * time.Second, } } func (c *Client) Run(stop <-chan struct{}) { for { select { case <-stop: return default: } if err := c.connect(stop); err != nil { log.Printf("WS-Verbindung fehlgeschlagen: %v — erneuter Versuch in %v", err, c.reconnectWait) select { case <-stop: return case <-time.After(c.reconnectWait): } if c.reconnectWait < 60*time.Second { c.reconnectWait *= 2 } } else { c.reconnectWait = 5 * time.Second } } } func (c *Client) connect(stop <-chan struct{}) error { u, err := url.Parse(c.backendURL) if err != nil { return err } switch u.Scheme { case "https": u.Scheme = "wss" case "http": u.Scheme = "ws" } u.Path = "/api/v1/agent/ws" q := u.Query() q.Set("agent_id", c.agentID) q.Set("api_key", c.apiKey) u.RawQuery = q.Encode() dialer := websocket.DefaultDialer if c.insecure { dialer = &websocket.Dialer{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, HandshakeTimeout: 10 * time.Second, } } conn, _, err := dialer.Dial(u.String(), nil) if err != nil { return fmt.Errorf("Dial fehlgeschlagen: %w", err) } defer conn.Close() c.conn = conn log.Printf("WS verbunden mit %s", c.backendURL) conn.SetReadDeadline(time.Now().Add(70 * time.Second)) conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(70 * time.Second)) return nil }) done := make(chan struct{}) // Ping-Loop: alle 30s einen Ping senden um Verbindung am Leben zu halten go func() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } case <-done: return case <-stop: return } } }() go func() { defer close(done) for { _, msg, err := conn.ReadMessage() if err != nil { if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { log.Printf("WS Read-Fehler: %v", err) } return } if resp := c.handler.Handle(msg); resp != nil { conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) conn.WriteMessage(websocket.TextMessage, resp) } } }() select { case <-stop: conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) return nil case <-done: return fmt.Errorf("Verbindung getrennt") } } func (c *Client) SendMessage(data []byte) error { if c.conn == nil { return fmt.Errorf("nicht verbunden") } return c.conn.WriteMessage(websocket.TextMessage, data) }