// Command loadtest یک استرس‌تستِ واقعیِ WebSocket برای سرورِ حکم است: N بازیکنِ // مصنوعی را می‌سازد، هرکدام join_queue می‌کند و بازیِ کاملِ حکم را به‌صورتِ خودکار // (انتخابِ حکم + بازیِ کارتِ قانونی) بازی می‌کند تا بارِ واقعیِ پخشِ وضعیت تولید شود. // // توکن‌ها با همان JWT_SECRET سرور ساخته می‌شوند (بدونِ نیاز به OTP/SMS). کاربرها // باید در DB وجود داشته باشند و برای tierِ انتخابی سکه‌ی کافی داشته باشند. // // مثال (روی نمونه‌ی محلی/استیجینگ — نه روی پروداکشنِ واقعی): // // go run ./cmd/loadtest -url ws://localhost:8080 -n 200 -tier beginner \ // -secret "$JWT_SECRET" -startid 1000 -ramp 50ms // // خروجی هر ثانیه: اتصال‌های فعال، پیام‌های وضعیت/ثانیه، حرکت‌ها/ثانیه، خطاها. package main import ( "flag" "fmt" "log" "math/rand" "net/http" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/golang-jwt/jwt/v5" "github.com/gorilla/websocket" ) var ( connected int64 stateMsgs int64 moves int64 errs int64 ) type state struct { Type string `json:"type"` Phase string `json:"phase"` YourSeat int `json:"your_seat"` Hakem int `json:"hakem"` Turn int `json:"turn"` TrickDone bool `json:"trick_done"` YourHand []string `json:"your_hand"` LeadSuit string `json:"lead_suit"` } var suitWord = map[byte]string{'H': "hearts", 'S': "spades", 'D': "diamonds", 'C': "clubs"} var wordSuit = map[string]byte{"hearts": 'H', "spades": 'S', "diamonds": 'D', "clubs": 'C'} func mintToken(secret string, uid int64, ttl time.Duration) string { claims := jwt.MapClaims{ "sub": strconv.FormatInt(uid, 10), "abl": "play", "exp": time.Now().Add(ttl).Unix(), "iat": time.Now().Unix(), } t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) s, _ := t.SignedString([]byte(secret)) return s } // pickCard یک کارتِ قانونی برمی‌گرداند (پیرویِ خال در صورتِ داشتن). func pickCard(hand []string, lead string) string { if lead != "" { want := wordSuit[lead] var same []string for _, c := range hand { if c[len(c)-1] == want { same = append(same, c) } } if len(same) > 0 { return same[rand.Intn(len(same))] } } return hand[rand.Intn(len(hand))] } func runClient(wsURL, secret, tier string, uid int64, playDelay time.Duration, wg *sync.WaitGroup) { defer wg.Done() tok := mintToken(secret, uid, time.Hour) u := wsURL + "/ws?token=" + tok c, _, err := websocket.DefaultDialer.Dial(u, http.Header{}) if err != nil { atomic.AddInt64(&errs, 1) return } atomic.AddInt64(&connected, 1) defer func() { atomic.AddInt64(&connected, -1); c.Close() }() _ = c.WriteJSON(map[string]any{"type": "join_queue", "tier": tier}) for { var raw map[string]any if err := c.ReadJSON(&raw); err != nil { atomic.AddInt64(&errs, 1) return } if raw["type"] != "state" { continue } atomic.AddInt64(&stateMsgs, 1) // دوباره به‌صورتِ typed decode کن. var s state if b, ok := raw["your_hand"].([]any); ok { for _, x := range b { s.YourHand = append(s.YourHand, x.(string)) } } s.Phase, _ = raw["phase"].(string) s.LeadSuit, _ = raw["lead_suit"].(string) s.YourSeat = intOf(raw["your_seat"]) s.Hakem = intOf(raw["hakem"]) s.Turn = intOf(raw["turn"]) s.TrickDone, _ = raw["trick_done"].(bool) if s.Turn != s.YourSeat || s.TrickDone { continue } time.Sleep(playDelay) // شبیه‌سازیِ زمانِ فکرِ انسان switch s.Phase { case "choose_trump": if s.Hakem == s.YourSeat { _ = c.WriteJSON(map[string]any{"type": "choose_trump", "suit": bestSuit(s.YourHand)}) atomic.AddInt64(&moves, 1) } case "playing": if len(s.YourHand) > 0 { _ = c.WriteJSON(map[string]any{"type": "play_card", "card": pickCard(s.YourHand, s.LeadSuit)}) atomic.AddInt64(&moves, 1) } } } } func bestSuit(hand []string) string { cnt := map[byte]int{} for _, c := range hand { cnt[c[len(c)-1]]++ } best, n := byte('S'), -1 for s, k := range cnt { if k > n { best, n = s, k } } return suitWord[best] } func intOf(v any) int { if f, ok := v.(float64); ok { return int(f) } return 0 } func main() { url := flag.String("url", "ws://localhost:8080", "ws base url (no /ws)") n := flag.Int("n", 100, "concurrent players") tier := flag.String("tier", "beginner", "table tier id") secret := flag.String("secret", "", "JWT_SECRET of the target server") startID := flag.Int64("startid", 1000, "first synthetic user id (must exist in DB w/ coins)") ramp := flag.Duration("ramp", 30*time.Millisecond, "delay between client starts") play := flag.Duration("play", 300*time.Millisecond, "think time before each move") flag.Parse() if *secret == "" { log.Fatal("-secret (JWT_SECRET) is required") } *url = strings.TrimRight(*url, "/") var wg sync.WaitGroup go func() { for range time.Tick(time.Second) { fmt.Printf("conns=%d state/s=%d moves/s=%d errs=%d\n", atomic.LoadInt64(&connected), atomic.SwapInt64(&stateMsgs, 0), atomic.SwapInt64(&moves, 0), atomic.LoadInt64(&errs)) } }() for i := 0; i < *n; i++ { wg.Add(1) go runClient(*url, *secret, *tier, *startID+int64(i), *play, &wg) time.Sleep(*ramp) } fmt.Printf("launched %d clients; Ctrl-C to stop\n", *n) wg.Wait() }