将存储部分迁移到Redis

This commit is contained in:
wtz
2026-02-20 19:41:02 +08:00
parent 13d2b0e1dc
commit a272dad5f1
9 changed files with 704 additions and 147 deletions

View File

@@ -35,6 +35,7 @@
- 后端: Go 1.21
- 前端: HTML + CSS + JavaScript
- 通信: WebSocket
- 存储: Redis
- 代理: Nginx
- 部署: Docker Compose
@@ -48,6 +49,7 @@ doudizhu-server/
│ │ ├── models/ # 数据模型
│ │ ├── game/ # 游戏逻辑
│ │ ├── handlers/ # HTTP处理器
│ │ ├── redis/ # Redis客户端封装
│ │ └── ws/ # WebSocket处理
│ ├── Dockerfile
│ └── go.mod
@@ -87,6 +89,10 @@ docker compose up -d --build
### 本地运行
```bash
# 启动Redis (Docker)
docker run -d --name redis -p 6379:6379 redis:7-alpine
# 运行后端
cd doudizhu-server/app
go mod tidy
go run ./cmd

View File

@@ -1,15 +1,37 @@
package main
import (
"context"
"doudizhu-server/internal/game"
"doudizhu-server/internal/handlers"
"doudizhu-server/internal/redis"
"doudizhu-server/internal/ws"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
gameMgr := game.NewGameManager()
redisAddr := os.Getenv("REDIS_ADDR")
if redisAddr == "" {
redisAddr = "localhost:6379"
}
redisClient, err := redis.NewClient(redis.Config{
Addr: redisAddr,
Password: "",
DB: 0,
})
if err != nil {
log.Fatalf("Failed to connect to Redis: %v", err)
}
defer redisClient.Close()
log.Println("Connected to Redis:", redisAddr)
gameMgr := game.NewGameManager(redisClient)
hub := ws.NewHub(gameMgr)
go hub.Run()
@@ -32,8 +54,31 @@ func main() {
})
mux.HandleFunc("/api/ws", h.WebSocket)
log.Println("App server starting on :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
go func() {
log.Println("App server starting on :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Server shutdown error: %v", err)
}
hub.Stop()
gameMgr.Stop()
log.Println("Server stopped")
}

View File

@@ -2,6 +2,13 @@ module doudizhu-server
go 1.21
require github.com/gorilla/websocket v1.5.1
require (
github.com/gorilla/websocket v1.5.1
github.com/redis/go-redis/v9 v9.5.1
)
require golang.org/x/net v0.17.0 // indirect
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
golang.org/x/net v0.17.0 // indirect
)

View File

@@ -1,4 +1,14 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=

View File

@@ -1,34 +0,0 @@
package game
import (
"crypto/rand"
"doudizhu-server/internal/models"
"encoding/hex"
"errors"
"fmt"
)
var (
ErrRoomNotFound = errors.New("room not found")
ErrRoomFull = errors.New("room is full")
ErrGameStarted = errors.New("game already started")
ErrGameNotStarted = errors.New("game not started")
ErrPlayerNotFound = errors.New("player not found")
ErrNotEnoughPlayers = errors.New("not enough players")
ErrPlayerNotReady = errors.New("player not ready")
ErrNotYourTurn = errors.New("not your turn")
ErrCardsNotInHand = errors.New("cards not in hand")
ErrInvalidCardType = errors.New("invalid card type")
ErrCannotBeat = errors.New("cannot beat last play")
ErrCannotPass = errors.New("cannot pass")
)
func generateID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}
func cardKey(c models.Card) string {
return fmt.Sprintf("%d_%d", c.Suit, c.Value)
}

View File

@@ -1,13 +1,31 @@
package game
import (
"context"
"doudizhu-server/internal/models"
"doudizhu-server/internal/redis"
"encoding/json"
"errors"
"math/rand"
"sort"
"sync"
"time"
)
var (
ErrRoomNotFound = errors.New("room not found")
ErrRoomFull = errors.New("room is full")
ErrGameStarted = errors.New("game already started")
ErrGameNotStarted = errors.New("game not started")
ErrNotYourTurn = errors.New("not your turn")
ErrPlayerNotFound = errors.New("player not found")
ErrCardsNotInHand = errors.New("cards not in hand")
ErrInvalidCardType = errors.New("invalid card type")
ErrCannotBeat = errors.New("cannot beat last play")
ErrCannotPass = errors.New("cannot pass")
ErrNotEnoughPlayers = errors.New("not enough players")
)
type CardLogic struct{}
func NewCardLogic() *CardLogic {
@@ -227,30 +245,31 @@ func (cl *CardLogic) getMainValue(cards []models.Card) int {
}
type GameManager struct {
rooms map[string]*models.Room
players map[string]*models.Player
deck map[string][]models.Card
discard map[string][]models.Card
mu sync.RWMutex
cl *CardLogic
rdb *redis.Client
cl *CardLogic
mu sync.RWMutex
stopCleanup chan struct{}
stopOnce sync.Once
}
func NewGameManager() *GameManager {
return &GameManager{
rooms: make(map[string]*models.Room),
players: make(map[string]*models.Player),
deck: make(map[string][]models.Card),
discard: make(map[string][]models.Card),
cl: NewCardLogic(),
func NewGameManager(rdb *redis.Client) *GameManager {
gm := &GameManager{
rdb: rdb,
cl: NewCardLogic(),
stopCleanup: make(chan struct{}),
}
go gm.cleanupLoop()
return gm
}
func (gm *GameManager) CreateRoom(playerName string, maxPlayers int) (*models.Room, *models.Player) {
gm.mu.Lock()
defer gm.mu.Unlock()
ctx := context.Background()
roomID := generateID()
playerID := generateID()
player := &models.Player{ID: playerID, Name: playerName, IsOnline: true}
room := &models.Room{
ID: roomID,
@@ -262,10 +281,9 @@ func (gm *GameManager) CreateRoom(playerName string, maxPlayers int) (*models.Ro
deck := gm.cl.CreateDeck()
gm.cl.Shuffle(deck)
gm.rooms[roomID] = room
gm.players[playerID] = player
gm.deck[roomID] = deck
gm.discard[roomID] = make([]models.Card, 0)
gm.saveRoom(ctx, room)
gm.saveDeck(ctx, roomID, deck)
gm.saveDiscard(ctx, roomID, []models.Card{})
return room, player
}
@@ -274,8 +292,9 @@ func (gm *GameManager) JoinRoom(roomID, playerName string) (*models.Room, *model
gm.mu.Lock()
defer gm.mu.Unlock()
room, ok := gm.rooms[roomID]
if !ok {
ctx := context.Background()
room, err := gm.loadRoom(ctx, roomID)
if err != nil {
return nil, nil, ErrRoomNotFound
}
if len(room.Players) >= room.MaxPlayers {
@@ -288,8 +307,8 @@ func (gm *GameManager) JoinRoom(roomID, playerName string) (*models.Room, *model
playerID := generateID()
player := &models.Player{ID: playerID, Name: playerName, IsOnline: true}
room.Players = append(room.Players, player)
gm.players[playerID] = player
gm.saveRoom(ctx, room)
return room, player, nil
}
@@ -297,19 +316,20 @@ func (gm *GameManager) LeaveRoom(roomID, playerID string) error {
gm.mu.Lock()
defer gm.mu.Unlock()
room, ok := gm.rooms[roomID]
if !ok {
ctx := context.Background()
room, err := gm.loadRoom(ctx, roomID)
if err != nil {
return ErrRoomNotFound
}
for i, p := range room.Players {
if p.ID == playerID {
room.Players = append(room.Players[:i], room.Players[i+1:]...)
delete(gm.players, playerID)
if len(room.Players) == 0 {
delete(gm.rooms, roomID)
delete(gm.deck, roomID)
delete(gm.discard, roomID)
gm.deleteRoomData(ctx, roomID)
} else {
gm.saveRoom(ctx, room)
}
return nil
}
@@ -317,25 +337,68 @@ func (gm *GameManager) LeaveRoom(roomID, playerID string) error {
return ErrPlayerNotFound
}
func (gm *GameManager) LeaveRoomWithTTL(roomID, playerID string) error {
gm.mu.Lock()
defer gm.mu.Unlock()
ctx := context.Background()
room, err := gm.loadRoom(ctx, roomID)
if err != nil {
return ErrRoomNotFound
}
for _, p := range room.Players {
if p.ID == playerID {
p.IsOnline = false
gm.saveRoom(ctx, room)
gm.rdb.Set(ctx, redis.PlayerTTLKey(playerID), time.Now().Add(redis.PlayerTTL).Unix(), redis.PlayerTTL)
return nil
}
}
return ErrPlayerNotFound
}
func (gm *GameManager) MarkPlayerOnline(roomID, playerID string) {
gm.mu.Lock()
defer gm.mu.Unlock()
ctx := context.Background()
room, err := gm.loadRoom(ctx, roomID)
if err != nil {
return
}
for _, p := range room.Players {
if p.ID == playerID {
p.IsOnline = true
gm.saveRoom(ctx, room)
gm.rdb.Delete(ctx, redis.PlayerTTLKey(playerID))
return
}
}
}
func (gm *GameManager) SetReady(roomID, playerID string, ready bool) error {
gm.mu.Lock()
defer gm.mu.Unlock()
room, ok := gm.rooms[roomID]
if !ok {
ctx := context.Background()
room, err := gm.loadRoom(ctx, roomID)
if err != nil {
return ErrRoomNotFound
}
if room.State == models.RoomStatePlaying {
return ErrGameStarted
}
for _, p := range room.Players {
if p.ID == playerID {
p.IsReady = ready
if room.State == models.RoomStateFinished {
room.State = models.RoomStateWaiting
}
gm.saveRoom(ctx, room)
return nil
}
}
@@ -346,8 +409,9 @@ func (gm *GameManager) StartGame(roomID string) error {
gm.mu.Lock()
defer gm.mu.Unlock()
room, ok := gm.rooms[roomID]
if !ok {
ctx := context.Background()
room, err := gm.loadRoom(ctx, roomID)
if err != nil {
return ErrRoomNotFound
}
if len(room.Players) < 2 {
@@ -357,6 +421,8 @@ func (gm *GameManager) StartGame(roomID string) error {
return ErrGameStarted
}
lastWinner := room.LastWinner
for _, p := range room.Players {
p.Cards = make([]models.Card, 0)
p.CardCount = 0
@@ -365,29 +431,40 @@ func (gm *GameManager) StartGame(roomID string) error {
deck := gm.cl.CreateDeck()
gm.cl.Shuffle(deck)
gm.deck[roomID] = deck
gm.discard[roomID] = make([]models.Card, 0)
gm.saveDeck(ctx, roomID, deck)
gm.saveDiscard(ctx, roomID, []models.Card{})
room.State = models.RoomStatePlaying
room.RoundCount = 1
room.CurrentTurn = 0
room.LastPlay = nil
room.LastWinner = ""
gm.dealCards(roomID, 5)
if lastWinner != "" {
for i, p := range room.Players {
if p.ID == lastWinner {
room.CurrentTurn = i
break
}
}
} else {
room.CurrentTurn = 0
}
gm.dealCards(room, 5)
gm.saveRoom(ctx, room)
return nil
}
func (gm *GameManager) dealCards(roomID string, count int) {
room := gm.rooms[roomID]
deck := gm.deck[roomID]
func (gm *GameManager) dealCards(room *models.Room, count int) {
ctx := context.Background()
deck, _ := gm.loadDeck(ctx, room.ID)
for i := 0; i < count; i++ {
for _, p := range room.Players {
if len(deck) == 0 {
deck = gm.discard[roomID]
deck, _ = gm.loadDiscard(ctx, room.ID)
gm.cl.Shuffle(deck)
gm.discard[roomID] = make([]models.Card, 0)
gm.saveDiscard(ctx, room.ID, []models.Card{})
}
if len(deck) > 0 {
p.Cards = append(p.Cards, deck[0])
@@ -395,7 +472,8 @@ func (gm *GameManager) dealCards(roomID string, count int) {
}
}
}
gm.deck[roomID] = deck
gm.saveDeck(ctx, room.ID, deck)
for _, p := range room.Players {
gm.cl.SortCards(p.Cards)
@@ -407,8 +485,9 @@ func (gm *GameManager) PlayCards(roomID, playerID string, cards []models.Card) e
gm.mu.Lock()
defer gm.mu.Unlock()
room, ok := gm.rooms[roomID]
if !ok {
ctx := context.Background()
room, err := gm.loadRoom(ctx, roomID)
if err != nil {
return ErrRoomNotFound
}
if room.State != models.RoomStatePlaying {
@@ -439,15 +518,20 @@ func (gm *GameManager) PlayCards(roomID, playerID string, cards []models.Card) e
Cards: cards,
CardType: cardType,
}
gm.discard[roomID] = append(gm.discard[roomID], cards...)
discard, _ := gm.loadDiscard(ctx, roomID)
discard = append(discard, cards...)
gm.saveDiscard(ctx, roomID, discard)
if len(player.Cards) == 0 {
room.LastWinner = playerID
room.State = models.RoomStateFinished
return nil
gm.rdb.Set(ctx, redis.RoomTTLKey(roomID), time.Now().Add(redis.RoomTTL).Unix(), redis.RoomTTL)
} else {
room.CurrentTurn = (room.CurrentTurn + 1) % len(room.Players)
}
gm.nextTurn(room)
gm.saveRoom(ctx, room)
return nil
}
@@ -455,8 +539,9 @@ func (gm *GameManager) Pass(roomID, playerID string) error {
gm.mu.Lock()
defer gm.mu.Unlock()
room, ok := gm.rooms[roomID]
if !ok {
ctx := context.Background()
room, err := gm.loadRoom(ctx, roomID)
if err != nil {
return ErrRoomNotFound
}
if room.State != models.RoomStatePlaying {
@@ -469,18 +554,67 @@ func (gm *GameManager) Pass(roomID, playerID string) error {
return ErrCannotPass
}
gm.nextTurn(room)
room.CurrentTurn = (room.CurrentTurn + 1) % len(room.Players)
// 一轮结束:回到最后出牌者,发牌并让他先出
if room.Players[room.CurrentTurn].ID == room.LastPlay.PlayerID {
winnerID := room.LastPlay.PlayerID
room.LastPlay = nil
gm.dealCards(roomID, 1)
room.RoundCount++
gm.dealCards(room, 1)
for i, p := range room.Players {
if p.ID == winnerID {
room.CurrentTurn = i
break
}
}
}
gm.saveRoom(ctx, room)
return nil
}
func (gm *GameManager) nextTurn(room *models.Room) {
room.CurrentTurn = (room.CurrentTurn + 1) % len(room.Players)
func (gm *GameManager) GetRoom(roomID string) (*models.Room, error) {
gm.mu.RLock()
defer gm.mu.RUnlock()
return gm.loadRoom(context.Background(), roomID)
}
func (gm *GameManager) GetRoomState(roomID, playerID string) *models.Room {
gm.mu.RLock()
defer gm.mu.RUnlock()
room, err := gm.loadRoom(context.Background(), roomID)
if err != nil {
return nil
}
players := make([]*models.Player, len(room.Players))
for i, p := range room.Players {
pp := &models.Player{
ID: p.ID,
Name: p.Name,
IsReady: p.IsReady,
IsOnline: p.IsOnline,
CardCount: p.CardCount,
}
if p.ID == playerID {
pp.Cards = p.Cards
}
players[i] = pp
}
return &models.Room{
ID: room.ID,
Players: players,
CurrentTurn: room.CurrentTurn,
State: room.State,
LastPlay: room.LastPlay,
LastWinner: room.LastWinner,
RoundCount: room.RoundCount,
MaxPlayers: room.MaxPlayers,
}
}
func (gm *GameManager) hasCards(player *models.Player, cards []models.Card) bool {
@@ -516,48 +650,118 @@ func (gm *GameManager) removeCards(player *models.Player, cards []models.Card) {
player.Cards = newCards
}
func (gm *GameManager) GetRoom(roomID string) (*models.Room, error) {
gm.mu.RLock()
defer gm.mu.RUnlock()
room, ok := gm.rooms[roomID]
if !ok {
return nil, ErrRoomNotFound
func (gm *GameManager) cleanupLoop() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
gm.cleanupExpired()
case <-gm.stopCleanup:
return
}
}
return room, nil
}
func (gm *GameManager) GetRoomState(roomID, playerID string) *models.Room {
gm.mu.RLock()
defer gm.mu.RUnlock()
func (gm *GameManager) cleanupExpired() {
gm.mu.Lock()
defer gm.mu.Unlock()
room, ok := gm.rooms[roomID]
if !ok {
ctx := context.Background()
gm.rdb.ScanKeys(ctx, "room:*:ttl", func(key string) error {
var expiryTime int64
if err := gm.rdb.Get(ctx, key, &expiryTime); err != nil {
return nil
}
if time.Now().Unix() > expiryTime {
roomID := key[len("room:") : len(key)-len(":ttl")]
gm.deleteRoomData(ctx, roomID)
}
return nil
}
})
players := make([]*models.Player, len(room.Players))
for i, p := range room.Players {
pp := &models.Player{
ID: p.ID,
Name: p.Name,
IsReady: p.IsReady,
IsOnline: p.IsOnline,
CardCount: p.CardCount,
gm.rdb.ScanKeys(ctx, "player:*:ttl", func(key string) error {
var expiryTime int64
if err := gm.rdb.Get(ctx, key, &expiryTime); err != nil {
return nil
}
if p.ID == playerID {
pp.Cards = p.Cards
if time.Now().Unix() > expiryTime {
playerID := key[len("player:") : len(key)-len(":ttl")]
gm.rdb.Delete(ctx, redis.PlayerKey(playerID), redis.PlayerTTLKey(playerID))
}
players[i] = pp
}
return &models.Room{
ID: room.ID,
Players: players,
CurrentTurn: room.CurrentTurn,
State: room.State,
LastPlay: room.LastPlay,
LastWinner: room.LastWinner,
RoundCount: room.RoundCount,
MaxPlayers: room.MaxPlayers,
}
return nil
})
}
func (gm *GameManager) Stop() {
gm.stopOnce.Do(func() {
close(gm.stopCleanup)
})
}
func (gm *GameManager) saveRoom(ctx context.Context, room *models.Room) {
gm.rdb.Set(ctx, redis.RoomKey(room.ID), room, redis.RoomTTL)
}
func (gm *GameManager) loadRoom(ctx context.Context, roomID string) (*models.Room, error) {
var room models.Room
if err := gm.rdb.Get(ctx, redis.RoomKey(roomID), &room); err != nil {
return nil, err
}
return &room, nil
}
func (gm *GameManager) saveDeck(ctx context.Context, roomID string, deck []models.Card) {
gm.rdb.Set(ctx, redis.RoomDeckKey(roomID), deck, redis.RoomTTL)
}
func (gm *GameManager) loadDeck(ctx context.Context, roomID string) ([]models.Card, error) {
var deck []models.Card
if err := gm.rdb.Get(ctx, redis.RoomDeckKey(roomID), &deck); err != nil {
return nil, err
}
return deck, nil
}
func (gm *GameManager) saveDiscard(ctx context.Context, roomID string, discard []models.Card) {
gm.rdb.Set(ctx, redis.RoomDiscardKey(roomID), discard, redis.RoomTTL)
}
func (gm *GameManager) loadDiscard(ctx context.Context, roomID string) ([]models.Card, error) {
var discard []models.Card
if err := gm.rdb.Get(ctx, redis.RoomDiscardKey(roomID), &discard); err != nil {
return nil, err
}
return discard, nil
}
func (gm *GameManager) deleteRoomData(ctx context.Context, roomID string) {
room, err := gm.loadRoom(ctx, roomID)
if err == nil {
for _, p := range room.Players {
gm.rdb.Delete(ctx, redis.PlayerKey(p.ID), redis.PlayerTTLKey(p.ID))
}
}
gm.rdb.Delete(ctx,
redis.RoomKey(roomID),
redis.RoomDeckKey(roomID),
redis.RoomDiscardKey(roomID),
redis.RoomTTLKey(roomID),
)
}
func generateID() string {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 8)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
func cardKey(c models.Card) string {
data, _ := json.Marshal(c)
return string(data)
}

252
app/internal/redis/redis.go Normal file
View File

@@ -0,0 +1,252 @@
package redis
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type Config struct {
Addr string
Password string
DB int
}
type Client struct {
rdb *redis.Client
}
func NewClient(cfg Config) (*Client, error) {
rdb := redis.NewClient(&redis.Options{
Addr: cfg.Addr,
Password: cfg.Password,
DB: cfg.DB,
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := rdb.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("failed to connect to redis: %w", err)
}
return &Client{rdb: rdb}, nil
}
func (c *Client) Close() error {
return c.rdb.Close()
}
func (c *Client) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("failed to marshal value: %w", err)
}
return c.rdb.Set(ctx, key, data, ttl).Err()
}
func (c *Client) Get(ctx context.Context, key string, dest interface{}) error {
data, err := c.rdb.Get(ctx, key).Bytes()
if err != nil {
return err
}
if err := json.Unmarshal(data, dest); err != nil {
return fmt.Errorf("failed to unmarshal value: %w", err)
}
return nil
}
func (c *Client) Exists(ctx context.Context, key string) (bool, error) {
result, err := c.rdb.Exists(ctx, key).Result()
if err != nil {
return false, err
}
return result > 0, nil
}
func (c *Client) Delete(ctx context.Context, keys ...string) error {
return c.rdb.Del(ctx, keys...).Err()
}
func (c *Client) SetTTL(ctx context.Context, key string, ttl time.Duration) error {
return c.rdb.Expire(ctx, key, ttl).Err()
}
func (c *Client) GetTTL(ctx context.Context, key string) (time.Duration, error) {
return c.rdb.TTL(ctx, key).Result()
}
func (c *Client) LPush(ctx context.Context, key string, values ...interface{}) error {
data := make([]interface{}, len(values))
for i, v := range values {
b, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("failed to marshal list value: %w", err)
}
data[i] = b
}
return c.rdb.LPush(ctx, key, data...).Err()
}
func (c *Client) RPush(ctx context.Context, key string, values ...interface{}) error {
data := make([]interface{}, len(values))
for i, v := range values {
b, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("failed to marshal list value: %w", err)
}
data[i] = b
}
return c.rdb.RPush(ctx, key, data...).Err()
}
func (c *Client) LRange(ctx context.Context, key string, start, stop int64, dest interface{}) error {
results, err := c.rdb.LRange(ctx, key, start, stop).Result()
if err != nil {
return err
}
data, err := json.Marshal(results)
if err != nil {
return fmt.Errorf("failed to marshal results: %w", err)
}
return json.Unmarshal(data, dest)
}
func (c *Client) LLen(ctx context.Context, key string) (int64, error) {
return c.rdb.LLen(ctx, key).Result()
}
func (c *Client) LPop(ctx context.Context, key string) (string, error) {
return c.rdb.LPop(ctx, key).Result()
}
func (c *Client) LTrim(ctx context.Context, key string, start, stop int64) error {
return c.rdb.LTrim(ctx, key, start, stop).Err()
}
func (c *Client) LSet(ctx context.Context, key string, index int64, value interface{}) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("failed to marshal value: %w", err)
}
return c.rdb.LSet(ctx, key, index, data).Err()
}
func (c *Client) SAdd(ctx context.Context, key string, members ...interface{}) error {
data := make([]interface{}, len(members))
for i, m := range members {
b, err := json.Marshal(m)
if err != nil {
return fmt.Errorf("failed to marshal set member: %w", err)
}
data[i] = b
}
return c.rdb.SAdd(ctx, key, data...).Err()
}
func (c *Client) SMembers(ctx context.Context, key string) ([]string, error) {
return c.rdb.SMembers(ctx, key).Result()
}
func (c *Client) SRem(ctx context.Context, key string, members ...interface{}) error {
data := make([]interface{}, len(members))
for i, m := range members {
b, err := json.Marshal(m)
if err != nil {
return fmt.Errorf("failed to marshal set member: %w", err)
}
data[i] = b
}
return c.rdb.SRem(ctx, key, data...).Err()
}
func (c *Client) HSet(ctx context.Context, key string, field string, value interface{}) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("failed to marshal hash value: %w", err)
}
return c.rdb.HSet(ctx, key, field, data).Err()
}
func (c *Client) HGet(ctx context.Context, key, field string, dest interface{}) error {
data, err := c.rdb.HGet(ctx, key, field).Bytes()
if err != nil {
return err
}
return json.Unmarshal(data, dest)
}
func (c *Client) HDel(ctx context.Context, key string, fields ...string) error {
return c.rdb.HDel(ctx, key, fields...).Err()
}
func (c *Client) HGetAll(ctx context.Context, key string) (map[string]string, error) {
return c.rdb.HGetAll(ctx, key).Result()
}
func (c *Client) HExists(ctx context.Context, key, field string) (bool, error) {
return c.rdb.HExists(ctx, key, field).Result()
}
func (c *Client) Keys(ctx context.Context, pattern string) ([]string, error) {
return c.rdb.Keys(ctx, pattern).Result()
}
func (c *Client) ScanKeys(ctx context.Context, pattern string, callback func(key string) error) error {
var cursor uint64
for {
var keys []string
var err error
keys, cursor, err = c.rdb.Scan(ctx, cursor, pattern, 100).Result()
if err != nil {
return err
}
for _, key := range keys {
if err := callback(key); err != nil {
return err
}
}
if cursor == 0 {
break
}
}
return nil
}
const (
RoomTTL = 30 * time.Minute
PlayerTTL = 10 * time.Minute
)
func RoomKey(roomID string) string {
return "room:" + roomID
}
func PlayerKey(playerID string) string {
return "player:" + playerID
}
func RoomPlayersKey(roomID string) string {
return "room:" + roomID + ":players"
}
func RoomDeckKey(roomID string) string {
return "room:" + roomID + ":deck"
}
func RoomDiscardKey(roomID string) string {
return "room:" + roomID + ":discard"
}
func RoomTTLKey(roomID string) string {
return "room:" + roomID + ":ttl"
}
func PlayerTTLKey(playerID string) string {
return "player:" + playerID + ":ttl"
}

View File

@@ -20,11 +20,13 @@ var upgrader = websocket.Upgrader{
}
type Client struct {
ID string
RoomID string
Conn *websocket.Conn
Send chan []byte
Hub *Hub
ID string
RoomID string
Conn *websocket.Conn
Send chan []byte
Hub *Hub
done chan struct{}
doneOnce sync.Once
}
type Hub struct {
@@ -33,6 +35,8 @@ type Hub struct {
Unregister chan *Client
GameMgr *game.GameManager
mu sync.RWMutex
stopChan chan struct{}
stopOnce sync.Once
}
func NewHub(gameMgr *game.GameManager) *Hub {
@@ -41,6 +45,7 @@ func NewHub(gameMgr *game.GameManager) *Hub {
Register: make(chan *Client, 64),
Unregister: make(chan *Client, 64),
GameMgr: gameMgr,
stopChan: make(chan struct{}),
}
}
@@ -49,39 +54,71 @@ func (h *Hub) Run() {
select {
case c := <-h.Register:
h.mu.Lock()
if old, exists := h.Clients[c.ID]; exists {
old.close()
delete(h.Clients, old.ID)
}
h.Clients[c.ID] = c
h.mu.Unlock()
case c := <-h.Unregister:
h.mu.Lock()
if _, ok := h.Clients[c.ID]; ok {
delete(h.Clients, c.ID)
close(c.Send)
c.close()
}
h.mu.Unlock()
if c.RoomID != "" {
h.GameMgr.LeaveRoom(c.RoomID, c.ID)
h.GameMgr.LeaveRoomWithTTL(c.RoomID, c.ID)
h.broadcastRoomState(c.RoomID)
}
case <-h.stopChan:
h.mu.Lock()
for _, c := range h.Clients {
c.close()
}
h.Clients = make(map[string]*Client)
h.mu.Unlock()
return
}
}
}
func (h *Hub) Stop() {
h.stopOnce.Do(func() {
close(h.stopChan)
})
}
func (c *Client) close() {
c.doneOnce.Do(func() {
close(c.done)
})
}
func (c *Client) ReadPump() {
defer func() {
c.Hub.Unregister <- c
c.Conn.Close()
}()
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
c.Conn.SetPongHandler(func(string) error {
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
_, data, err := c.Conn.ReadMessage()
if err != nil {
break
select {
case <-c.done:
return
default:
_, data, err := c.Conn.ReadMessage()
if err != nil {
return
}
c.handleMessage(data)
}
c.handleMessage(data)
}
}
@@ -91,8 +128,13 @@ func (c *Client) WritePump() {
ticker.Stop()
c.Conn.Close()
}()
for {
select {
case <-c.done:
c.Conn.WriteMessage(websocket.CloseMessage, nil)
return
case msg, ok := <-c.Send:
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
@@ -102,6 +144,7 @@ func (c *Client) WritePump() {
if err := c.Conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
case <-ticker.C:
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
@@ -210,6 +253,7 @@ func (c *Client) sendError(msg string) {
})
select {
case c.Send <- data:
case <-c.done:
default:
}
}
@@ -222,6 +266,7 @@ func (h *Hub) broadcastToRoom(roomID string, msg models.Message) {
if c.RoomID == roomID {
select {
case c.Send <- data:
case <-c.done:
default:
}
}
@@ -248,6 +293,7 @@ func (h *Hub) broadcastRoomState(roomID string) {
})
select {
case c.Send <- data:
case <-c.done:
default:
}
}
@@ -274,15 +320,15 @@ func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
Conn: conn,
Send: make(chan []byte, 256),
Hub: hub,
done: make(chan struct{}),
}
hub.mu.Lock()
hub.Clients[client.ID] = client
hub.mu.Unlock()
hub.GameMgr.MarkPlayerOnline(roomID, playerID)
go client.WritePump()
go client.ReadPump()
hub.Register <- client
time.Sleep(50 * time.Millisecond)
go client.WritePump()
go client.ReadPump()
hub.broadcastRoomState(roomID)
}

View File

@@ -4,6 +4,10 @@ services:
restart: unless-stopped
environment:
- TZ=Asia/Shanghai
- REDIS_ADDR=redis:6379
depends_on:
redis:
condition: service_healthy
nginx:
image: nginx:alpine
@@ -17,3 +21,20 @@ services:
restart: unless-stopped
environment:
- TZ=Asia/Shanghai
redis:
image: redis:7-alpine
restart: unless-stopped
command: >
redis-server
--maxmemory 128mb
--maxmemory-policy volatile-ttl
--save ""
--appendonly no
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
environment:
- TZ=Asia/Shanghai