package ws import ( "encoding/json" "log/slog" "sync" "time" "github.com/gorilla/websocket" ) const ( writeWait = 10 * time.Second pongWait = 60 * time.Second pingPeriod = (pongWait * 9) / 10 maxMessageSize = 4096 sendBuffer = 32 ) // Client یک اتصال WebSocket یک بازیکن. type Client struct { hub *Hub conn *websocket.Conn send chan []byte UserID int64 Name string // done هنگام قطع اتصال بسته می‌شود تا writePump خارج شده و // trySend دیگر تلاش به ارسال نکند (send هیچ‌گاه close نمی‌شود تا panic رخ ندهد). done chan struct{} closeOnce sync.Once // توسط goroutine هاب ست می‌شوند (تک‌نویسنده) و فقط توسط آن خوانده می‌شوند. room *Room seat int tier string // نوع میزی که در صفش است (برای تسویه) } // close اتصال را یک‌بار به‌صورت امن می‌بندد. func (c *Client) close() { c.closeOnce.Do(func() { close(c.done) c.conn.Close() }) } // readPump پیام‌های ورودی را خوانده و به هاب می‌فرستد. func (c *Client) readPump() { defer func() { c.hub.unregister <- c c.close() }() c.conn.SetReadLimit(maxMessageSize) _ = c.conn.SetReadDeadline(time.Now().Add(pongWait)) c.conn.SetPongHandler(func(string) error { return c.conn.SetReadDeadline(time.Now().Add(pongWait)) }) for { _, raw, err := c.conn.ReadMessage() if err != nil { return } var msg inboundMsg if err := json.Unmarshal(raw, &msg); err != nil { c.trySend(mustJSON(errorMsg{Type: "error", Message: "invalid message"})) continue } c.hub.inbound <- inbound{client: c, msg: msg} } } // writePump پیام‌های خروجی و ping را به سوکت می‌نویسد. func (c *Client) writePump() { ticker := time.NewTicker(pingPeriod) defer func() { ticker.Stop() c.close() }() for { select { case <-c.done: return case msg := <-c.send: _ = c.conn.SetWriteDeadline(time.Now().Add(writeWait)) if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil { return } case <-ticker.C: _ = c.conn.SetWriteDeadline(time.Now().Add(writeWait)) if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } } } } // trySend ارسال غیرمسدودکننده؛ اگر اتصال بسته یا بافر پر بود پیام را می‌اندازد. // send هیچ‌گاه close نمی‌شود؛ پس از done صرفاً پیام دور ریخته می‌شود (بدون panic). func (c *Client) trySend(b []byte) { select { case <-c.done: case c.send <- b: default: slog.Warn("client send buffer full, dropping message", "user", c.UserID) } } func mustJSON(v any) []byte { b, _ := json.Marshal(v) return b }