Files
back-hokm/internal/ws/client.go
T
2026-06-15 16:53:25 +03:30

113 lines
2.8 KiB
Go

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
}