commit 3248cc9682922617d1b0578ef874e5495ebe2447 Author: kercylan98 Date: Fri Apr 7 11:21:50 2023 +0800 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..404c8b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,166 @@ +### Minotaur +app/monopoly/ + +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/ +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Linux template +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + diff --git a/app/template/app/app.go b/app/template/app/app.go new file mode 100644 index 0000000..4fd17f5 --- /dev/null +++ b/app/template/app/app.go @@ -0,0 +1,7 @@ +package app + +import ( + "minotaur/game" +) + +var State = new(game.StateMachine).Init() diff --git a/app/template/main.go b/app/template/main.go new file mode 100644 index 0000000..253791b --- /dev/null +++ b/app/template/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "minotaur/app/template/app" + "minotaur/game" + "minotaur/game/gateway/conn" + "minotaur/game/protobuf/protobuf" +) + +func init() { + app.State.RegMessagePlayer(int32(protobuf.MessageCode_SystemHeartbeat), func(player *game.Player) { + fmt.Println("hhha") + }) +} + +func main() { + app.State.Run("/test", 8888, conn.NewOrdinary()) +} diff --git a/app/template/main_test.go b/app/template/main_test.go new file mode 100644 index 0000000..93e2123 --- /dev/null +++ b/app/template/main_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "github.com/gorilla/websocket" + "google.golang.org/protobuf/proto" + "minotaur/game/protobuf/protobuf" + "testing" + "time" +) + +func TestA(t *testing.T) { +run: + { + } + for { + ws := `ws://127.0.0.1:9000/test` + c, _, err := websocket.DefaultDialer.Dial(ws, nil) + if err != nil { + continue + } + + req := &protobuf.SystemHeartbeatClient{} + d, _ := proto.Marshal(req) + + data, err := proto.Marshal(&protobuf.Message{ + Code: int32(protobuf.MessageCode_SystemHeartbeat), + Data: d, + }) + if err != nil { + panic(err) + } + + for { + //fmt.Println(c, data) + if err := c.WriteMessage(2, data); err != nil { + fmt.Println(err) + goto run + } + + time.Sleep(3 * time.Second) + } + + } +} diff --git a/game/channel.go b/game/channel.go new file mode 100644 index 0000000..4d53065 --- /dev/null +++ b/game/channel.go @@ -0,0 +1,183 @@ +package game + +import ( + "fmt" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + "minotaur/utils/log" + "minotaur/utils/super" + "minotaur/utils/timer" + "reflect" + "strconv" + "sync" + "time" +) + +// channel 频道 +type channel struct { + id any // 频道ID + size int // 最大人数 + stateMachine *StateMachine // 状态机 + timer *timer.Manager // 定时器 + lock sync.RWMutex // 玩家锁 + players map[int64]*Player // 所有玩家 + alone bool // 频道运行逻辑是否单线程 + + // alone mode + messages chan *Message + + // multi mode +} + +// run 开始运行频道 +func (slf *channel) run() { + slf.messages = make(chan *Message, 4096*1000) + go func() { + for { + msg, open := <-slf.messages + if !open { + return + } + slf.dispatch(msg) + } + }() +} + +// tryRelease 尝试释放频道 +func (slf *channel) tryRelease() { + slf.timer.After("channelRelease", time.Second*10, func() { + slf.stateMachine.channelRWMutex.Lock() + defer slf.stateMachine.channelRWMutex.Unlock() + if len(slf.players) > 0 { + return + } + slf.timer.Release() + close(slf.messages) + delete(slf.stateMachine.channels, slf.id) + log.Info("ChannelRelease", zap.Any("channelID", slf.id), zap.Int("channelCount", len(slf.stateMachine.channels))) + }) +} + +// tryRelease 强制释放频道 +func (slf *channel) release() { + slf.stateMachine.channelRWMutex.Lock() + defer slf.stateMachine.channelRWMutex.Unlock() + slf.timer.Release() + close(slf.messages) + delete(slf.stateMachine.channels, slf.id) + log.Info("ChannelRelease", + zap.Any("channelID", slf.id), + zap.Int("channelCount", len(slf.stateMachine.channels)), + zap.String("type", "force"), + ) +} + +// join 加入频道 +func (slf *channel) join(player *Player) (*Player, error) { + slf.lock.Lock() + if slf.size > 0 && len(slf.players) > slf.size { + return player, fmt.Errorf("join channel[%v] failed, maximum[%d] number of player", slf.id, slf.size) + } + if player.channel != nil { + player.channel.lock.Lock() + delete(player.channel.players, player.guid) + player.channel.lock.Unlock() + player.channel.tryRelease() + } + slf.players[player.guid] = player + slf.lock.Unlock() + player.channel = slf + player.ChannelTimer = slf.timer + log.Info("ChannelJoin", + zap.Any("channelID", slf.id), + zap.Int64("playerGuid", player.guid), + zap.String("size", super.If(slf.size <= 0, "NaN", strconv.Itoa(slf.size))), + zap.Int("headcount", len(slf.players)), + zap.String("address", player.ip), + ) + return player, nil +} + +// push 向频道内推送消息 +func (slf *channel) push(message *Message) { + switch message.Type() { + case MessageTypePlayer: + if slf.alone { + slf.messages <- message + } else { + slf.dispatch(message) + } + case MessageTypeEvent: + slf.messages <- message + } + +} + +// dispatch 消息分发 +func (slf *channel) dispatch(message *Message) { + args := message.Args() + switch message.Type() { + case MessageTypePlayer: + player, code, data := args[0], args[1], args[2] + h := slf.stateMachine.handles[code.(int32)] + var in = []reflect.Value{reflect.ValueOf(player)} + if h[1] != nil { + messageType := reflect.New(h[1].(reflect.Type).Elem()) + if err := proto.Unmarshal(data.([]byte), messageType.Interface().(proto.Message)); err != nil { + panic(err) + } + in = append(in, messageType) + } + h[0].(reflect.Value).Call(in) + case MessageTypeEvent: + event, player := args[0].(byte), args[1].(*Player) + switch event { + case EventTypeGuestJoin: + slf.stateMachine.router.OnGuestPlayerJoinEvent(player) + case EventTypeGuestLeave: + callback := args[2].(func(player *Player)) + slf.stateMachine.router.OnGuestPlayerLeaveEvent(player) + callback(player) + } + } +} + +// GetPlayers 获取频道内所有玩家 +func (slf *channel) GetPlayers() map[int64]*Player { + var players = make(map[int64]*Player) + if !slf.alone { + slf.lock.RLock() + defer slf.lock.RUnlock() + } + for id, player := range slf.players { + players[id] = player + } + return players +} + +// GetAllChannelPlayers 获取所有频道玩家 +func (slf *channel) GetAllChannelPlayers() map[int64]*Player { + if !slf.alone { + slf.stateMachine.channelRWMutex.RLock() + } + var channels = make([]*channel, 0, len(slf.stateMachine.channels)) + for _, channel := range slf.stateMachine.channels { + channels = append(channels, channel) + } + if !slf.alone { + slf.stateMachine.channelRWMutex.RUnlock() + } + var players = make(map[int64]*Player) + for _, channel := range channels { + if !slf.alone { + channel.lock.RLock() + } + for id, player := range channel.players { + players[id] = player + } + if !slf.alone { + channel.lock.RUnlock() + } + } + return players +} diff --git a/game/conn.go b/game/conn.go new file mode 100644 index 0000000..bc9ba49 --- /dev/null +++ b/game/conn.go @@ -0,0 +1,8 @@ +package game + +// Conn 用户连接抽象 +// +// 连接支持使用 tag 进行参数读取 +type Conn interface { + GetConn() any +} diff --git a/game/feature/actor.go b/game/feature/actor.go new file mode 100644 index 0000000..12ea346 --- /dev/null +++ b/game/feature/actor.go @@ -0,0 +1,9 @@ +package feature + +// Actor 玩家演员接口定义 +type Actor interface { + // GetGuid 获取演员 guid + GetGuid() int64 + // GetPlayer 获取所属玩家 + GetPlayer() Player +} diff --git a/game/feature/chatroom.go b/game/feature/chatroom.go new file mode 100644 index 0000000..2b6bd08 --- /dev/null +++ b/game/feature/chatroom.go @@ -0,0 +1,16 @@ +package feature + +// ChatRoom 聊天室接口定义 +type ChatRoom interface { + // JoinChatRoom 加入聊天室 + JoinChatRoom(chat PlayerChat) + // LeaveChatRoom 离开聊天室 + LeaveChatRoom(chat PlayerChat) +} + +// PlayerChat 玩家聊天接口定义 +type PlayerChat interface { + Player + // SendChatMessage 发送聊天消息给该玩家 + SendChatMessage(message string) +} diff --git a/game/feature/components/room_manager.go b/game/feature/components/room_manager.go new file mode 100644 index 0000000..9572304 --- /dev/null +++ b/game/feature/components/room_manager.go @@ -0,0 +1,48 @@ +package components + +import ( + "minotaur/game/feature" + "reflect" +) + +// NewRoomManager 创建房间管理组件 +func NewRoomManager[P feature.Player, R feature.Room[P]]() *RoomManager[P, R] { + return &RoomManager[P, R]{} +} + +// RoomManager 房间管理组件 +type RoomManager[P feature.Player, R feature.Room[P]] struct { + rooms map[int64]R + playerRoomRef map[int64]int64 +} + +func (slf *RoomManager[P, R]) JoinRoom(player feature.Player, room R) { + if slf.rooms == nil { + slf.rooms = map[int64]R{} + slf.playerRoomRef = map[int64]int64{} + } + slf.rooms[room.GetGuid()] = room + slf.playerRoomRef[player.GetGuid()] = room.GetGuid() +} + +func (slf *RoomManager[P, R]) LeaveRoom(playerGuid int64) { + roomId := slf.playerRoomRef[playerGuid] + room := slf.rooms[roomId] + if !reflect.ValueOf(room).IsNil() { + room.LeaveRoom(playerGuid) + } + delete(slf.rooms, roomId) + delete(slf.playerRoomRef, playerGuid) +} + +func (slf *RoomManager[P, R]) GetRoom(guid int64) R { + return slf.rooms[guid] +} + +func (slf *RoomManager[P, R]) GetPlayer(guid int64) P { + return slf.rooms[slf.playerRoomRef[guid]].GetPlayer(guid) +} + +func (slf *RoomManager[P, R]) GetPlayerRoom(guid int64) R { + return slf.rooms[slf.playerRoomRef[guid]] +} diff --git a/game/feature/player.go b/game/feature/player.go new file mode 100644 index 0000000..9b87215 --- /dev/null +++ b/game/feature/player.go @@ -0,0 +1,7 @@ +package feature + +// Player 玩家接口定义 +type Player interface { + // GetGuid 获取玩家 guid + GetGuid() int64 +} diff --git a/game/feature/room.go b/game/feature/room.go new file mode 100644 index 0000000..bbeb59e --- /dev/null +++ b/game/feature/room.go @@ -0,0 +1,21 @@ +package feature + +// Room 游戏房间接口定义 +type Room[P Player] interface { + // GetGuid 获取房间 guid + GetGuid() int64 + // JoinRoom 加入房间 + JoinRoom(player P) error + // LeaveRoom 离开房间 + LeaveRoom(guid int64) + // GetPlayerMaximum 获取游戏参与人上限 + GetPlayerMaximum() int + // GetPlayer 获取特定玩家 + GetPlayer(guid int64) P + // GetPlayers 获取所有玩家 + GetPlayers() map[int64]P + // GetPlayerCount 获取玩家数量 + GetPlayerCount() int + // IsExist 玩家是否存在 + IsExist(playerGuid int64) bool +} diff --git a/game/feature/room_game.go b/game/feature/room_game.go new file mode 100644 index 0000000..88c1fba --- /dev/null +++ b/game/feature/room_game.go @@ -0,0 +1,10 @@ +package feature + +// RoomGame 房间游戏接口定义 +type RoomGame[P Player] interface { + Room[P] + // GameStart 游戏开始 + GameStart(startTimeSecond int64, loop func(game RoomGame[P])) + // GameOver 游戏结束 + GameOver() +} diff --git a/game/feature/room_ready.go b/game/feature/room_ready.go new file mode 100644 index 0000000..326138d --- /dev/null +++ b/game/feature/room_ready.go @@ -0,0 +1,14 @@ +package feature + +// RoomReady 房间准备接口定义 +type RoomReady[P Player] interface { + Room[P] + // Ready 设置玩家准备状态 + Ready(playerGuid int64, ready bool) + // IsAllReady 是否全部玩家已准备 + IsAllReady() bool + // GetReadyCount 获取已准备玩家数量 + GetReadyCount() int + // GetUnready 获取未准备的玩家 + GetUnready() map[int64]P +} diff --git a/game/feature/room_spectator.go b/game/feature/room_spectator.go new file mode 100644 index 0000000..6bf3cb1 --- /dev/null +++ b/game/feature/room_spectator.go @@ -0,0 +1,16 @@ +package feature + +// RoomSpectator 房间观众席接口定义 +type RoomSpectator[P Player] interface { + Room[P] + // GetSpectatorMaximum 获取观众人数上限 + GetSpectatorMaximum() int + // JoinSpectator 加入观众席 + JoinSpectator(player P) error + // LeaveSpectator 离开观众席 + LeaveSpectator(guid int64) error + // GetSpectatorPlayer 获取特定观众席玩家 + GetSpectatorPlayer(guid int64) P + // GetSpectatorPlayers 获取观众席玩家 + GetSpectatorPlayers() map[int64]P +} diff --git a/game/feature/room_team.go b/game/feature/room_team.go new file mode 100644 index 0000000..5155970 --- /dev/null +++ b/game/feature/room_team.go @@ -0,0 +1,20 @@ +package feature + +// RoomTeam 房间小队接口定义 +type RoomTeam interface { + // GetTeamMaximum 获取最大小队数量 + GetTeamMaximum() int + // GetTeam 获取特定队伍 + GetTeam(guid int64) Team + // GetTeams 获取所有队伍 + GetTeams() map[int64]Team +} + +type Team interface { + // GetGuid 获取小队 guid + GetGuid() int64 + // GetPlayer 获取小队特定玩家 + GetPlayer(guid int64) Player + // GetPlayers 获取小队玩家 + GetPlayers() map[int64]Player +} diff --git a/game/feature/standard/player.go b/game/feature/standard/player.go new file mode 100644 index 0000000..4045ec5 --- /dev/null +++ b/game/feature/standard/player.go @@ -0,0 +1,9 @@ +package standard + +import ( + "minotaur/game" +) + +type Player struct { + *game.Player +} diff --git a/game/feature/standard/room.go b/game/feature/standard/room.go new file mode 100644 index 0000000..3b5c15f --- /dev/null +++ b/game/feature/standard/room.go @@ -0,0 +1,61 @@ +package standard + +import ( + "errors" + "minotaur/game/feature" + "reflect" +) + +// NewRoom 普通房间实例 +func NewRoom[P feature.Player](guid int64, playerMaximum int) *Room[P] { + room := &Room[P]{ + guid: guid, + playerMaximum: playerMaximum, + } + return room +} + +type Room[P feature.Player] struct { + guid int64 // 房间 guid + playerMaximum int // 房间最大人数,小于等于0不限 + players map[int64]P // 房间玩家 +} + +func (slf *Room[P]) GetGuid() int64 { + return slf.guid +} + +func (slf *Room[P]) JoinRoom(player P) error { + if slf.playerMaximum > 0 && len(slf.players) >= slf.playerMaximum { + return errors.New("maximum number of player") + } + if slf.players == nil { + slf.players = map[int64]P{} + } + slf.players[player.GetGuid()] = player + return nil +} + +func (slf *Room[P]) LeaveRoom(guid int64) { + delete(slf.players, guid) +} + +func (slf *Room[P]) GetPlayerMaximum() int { + return slf.playerMaximum +} + +func (slf *Room[P]) GetPlayer(guid int64) P { + return slf.players[guid] +} + +func (slf *Room[P]) GetPlayers() map[int64]P { + return slf.players +} + +func (slf *Room[P]) GetPlayerCount() int { + return len(slf.players) +} + +func (slf *Room[P]) IsExist(playerGuid int64) bool { + return !reflect.ValueOf(slf.players[playerGuid]).IsNil() +} diff --git a/game/feature/standard/room_game.go b/game/feature/standard/room_game.go new file mode 100644 index 0000000..db5e5c0 --- /dev/null +++ b/game/feature/standard/room_game.go @@ -0,0 +1,39 @@ +package standard + +import ( + "minotaur/game/feature" + "time" +) + +// NewRoomGame 对普通房间附加游戏功能的实例 +// +// startCountDown 游戏开始倒计时,大于0生效 +func NewRoomGame[P feature.Player](room *Room[P], startCountDown time.Duration) *RoomGame[P] { + return &RoomGame[P]{ + Room: room, + startCountDown: startCountDown, + } +} + +type RoomGame[P feature.Player] struct { + *Room[P] + *Timer + startTimeSecond int64 // 游戏开始时间戳(秒) + startCountDown time.Duration // 游戏开始倒计时 +} + +func (slf *RoomGame[P]) GameStart(startTimeSecond int64, loop func(game feature.RoomGame[P])) { + slf.startTimeSecond = startTimeSecond + + if slf.startCountDown > 0 { + slf.After("RoomGame.GameStart.StartCountDown", slf.startCountDown, func() { + loop(slf) + }) + } else { + loop(slf) + } +} + +func (slf *RoomGame[P]) GameOver() { + slf.Release() +} diff --git a/game/feature/standard/room_ready.go b/game/feature/standard/room_ready.go new file mode 100644 index 0000000..1d2612e --- /dev/null +++ b/game/feature/standard/room_ready.go @@ -0,0 +1,48 @@ +package standard + +import "minotaur/game/feature" + +// NewRoomReady 对普通房间附加准备功能的实例 +func NewRoomReady[P feature.Player](room *Room[P]) *RoomReady[P] { + return &RoomReady[P]{} +} + +type RoomReady[P feature.Player] struct { + *Room[P] + readies map[int64]bool +} + +func (slf *RoomReady[P]) Ready(playerGuid int64, ready bool) { + if ready { + if !slf.IsExist(playerGuid) { + if _, exist := slf.readies[playerGuid]; exist { + delete(slf.readies, playerGuid) + } + return + } + if slf.readies == nil { + slf.readies = map[int64]bool{} + } + slf.readies[playerGuid] = true + } else { + delete(slf.readies, playerGuid) + } +} + +func (slf *RoomReady[P]) IsAllReady() bool { + return len(slf.readies) >= slf.GetPlayerCount() +} + +func (slf *RoomReady[P]) GetReadyCount() int { + return len(slf.readies) +} + +func (slf *RoomReady[P]) GetUnready() map[int64]P { + var unreadiest = make(map[int64]P) + for guid, player := range slf.GetPlayers() { + if !slf.readies[guid] { + unreadiest[guid] = player + } + } + return unreadiest +} diff --git a/game/feature/standard/room_spectator.go b/game/feature/standard/room_spectator.go new file mode 100644 index 0000000..5bbff28 --- /dev/null +++ b/game/feature/standard/room_spectator.go @@ -0,0 +1,72 @@ +package standard + +import ( + "errors" + "minotaur/game/feature" + "reflect" +) + +// NewRoomSpectator 对普通房间附加观众席功能的实例 +func NewRoomSpectator[P feature.Player](room *Room[P], spectatorMaximum int) *RoomSpectator[P] { + return &RoomSpectator[P]{ + Room: room, + spectatorMaximum: spectatorMaximum, + } +} + +type RoomSpectator[P feature.Player] struct { + *Room[P] + spectatorMaximum int // 观众席最大人数,小于等于0不限 + spectatorPlayers map[int64]P // 观众席玩家 + recordInRoom map[int64]bool // 记录玩家是否从房间中移动到观众席 +} + +func (slf *RoomSpectator[P]) GetSpectatorMaximum() int { + return slf.spectatorMaximum +} + +func (slf *RoomSpectator[P]) JoinSpectator(player P) error { + if slf.spectatorMaximum > 0 && len(slf.spectatorPlayers) >= slf.spectatorMaximum { + return errors.New("maximum number of player") + } + + var guid = player.GetGuid() + + if !reflect.ValueOf(slf.GetPlayer(guid)).IsNil() { + if slf.recordInRoom == nil { + slf.recordInRoom = map[int64]bool{} + } + slf.recordInRoom[guid] = true + slf.LeaveRoom(player.GetGuid()) + } + if slf.spectatorPlayers == nil { + slf.spectatorPlayers = map[int64]P{} + } + slf.spectatorPlayers[guid] = player + return nil +} + +func (slf *RoomSpectator[P]) LeaveSpectator(guid int64) error { + var ( + inRoom = slf.recordInRoom[guid] + inSpectator = !reflect.ValueOf(slf.spectatorPlayers[guid]).IsNil() + ) + + if inSpectator { + delete(slf.spectatorPlayers, guid) + delete(slf.recordInRoom, guid) + if inRoom { + return slf.JoinRoom(slf.GetPlayer(guid)) + } + } + + return nil +} + +func (slf *RoomSpectator[P]) GetSpectatorPlayer(guid int64) P { + return slf.spectatorPlayers[guid] +} + +func (slf *RoomSpectator[P]) GetSpectatorPlayers() map[int64]P { + return slf.spectatorPlayers +} diff --git a/game/feature/standard/timer.go b/game/feature/standard/timer.go new file mode 100644 index 0000000..5c37651 --- /dev/null +++ b/game/feature/standard/timer.go @@ -0,0 +1,15 @@ +package standard + +import "minotaur/utils/timer" + +// Timer 附加定时器功能 +type Timer struct { + *timer.Manager +} + +func (slf *Timer) Timer() *timer.Manager { + if slf.Manager == nil { + slf.Manager = timer.GetManager(64) + } + return slf.Manager +} diff --git a/game/feature/timer.go b/game/feature/timer.go new file mode 100644 index 0000000..be7d79c --- /dev/null +++ b/game/feature/timer.go @@ -0,0 +1,9 @@ +package feature + +import "minotaur/utils/timer" + +// Timer 定时器接口定义 +type Timer interface { + // Timer 获取定时器 + Timer() *timer.Manager +} diff --git a/game/gametime.go b/game/gametime.go new file mode 100644 index 0000000..305c967 --- /dev/null +++ b/game/gametime.go @@ -0,0 +1,22 @@ +package game + +import "time" + +// Time 带有偏移的游戏时间 +type Time struct { + Offset time.Duration +} + +// Now 获取包含偏移的当前时间 +func (slf *Time) Now() time.Time { + t := time.Now() + if slf.Offset == 0 { + return t + } + return t.Add(slf.Offset) +} + +// Since 获取两个时间之间的差 +func (slf *Time) Since(t time.Time) time.Duration { + return slf.Now().Sub(t) +} diff --git a/game/gateway.go b/game/gateway.go new file mode 100644 index 0000000..1e03099 --- /dev/null +++ b/game/gateway.go @@ -0,0 +1,147 @@ +package game + +import ( + "context" + "fmt" + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + "minotaur/game/protobuf/protobuf" + "minotaur/utils/gin/middlewares" + "minotaur/utils/log" + "minotaur/utils/timer" + "net/http" + "strings" + "time" +) + +const ( + loginTimeoutTimerName = "player_login_timeout_timer" +) + +type OnCreateConnHandleFunc func() Conn +type gateway struct { + server *http.Server +} + +func (slf *gateway) run(stateMachine *StateMachine, appName string, port int, onCreateConnHandleFunc OnCreateConnHandleFunc) *gateway { + + // Gin WebSocket + gin.SetMode(gin.ReleaseMode) + router := gin.New() + router.Use(middlewares.Logger(log.Logger), middlewares.Cors()) + pprof.Register(router) // pprof 可视化分析 + + var upgrade = websocket.Upgrader{ + ReadBufferSize: 4096, + WriteBufferSize: 4096, + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + router.GET(fmt.Sprintf("/%s", appName), func(context *gin.Context) { + ip := context.GetHeader("X-Real-IP") + ws, err := upgrade.Upgrade(context.Writer, context.Request, nil) + if err != nil { + log.Error("Websocket Upgrade error", zap.Error(err)) + return + } + if len(ip) == 0 { + addr := ws.RemoteAddr().String() + if index := strings.LastIndex(addr, ":"); index != -1 { + ip = addr[0:index] + } + } + conn := onCreateConnHandleFunc() + if err := context.ShouldBind(conn); err != nil { + log.Error("ServeWebSocket", zap.Error(err)) + context.AbortWithStatus(http.StatusBadRequest) + return + } + + playerGuid, err := stateMachine.sonyflake.NextID() + if err != nil { + log.Error("GuidGenerateFailed", zap.Error(err)) + context.AbortWithStatus(http.StatusBadRequest) + return + } + + player := &Player{ + guid: int64(playerGuid), + conn: conn, + ws: ws, + ip: ip, + Manager: timer.GetManager(64), + GameTimer: stateMachine.Manager, + } + + channelId, size := stateMachine.channelStrategy() + channel := stateMachine.channel(channelId, size) + player, err = channel.join(player) + if err != nil { + player.exit(err.Error()) + return + } + + player.channel.push(new(Message).Init(MessageTypeEvent, EventTypeGuestJoin, player)) + if stateMachine.loginTimeout > 0 { + player.After(loginTimeoutTimerName, stateMachine.loginTimeout, func() { + player.exit("login timeout") + }) + } + + defer func() { + if err := recover(); err != nil { + player.exit() + } + }() + + for { + if err := player.ws.SetReadDeadline(time.Now().Add(time.Second * 30)); err != nil { + panic(err) + } + + _, data, err := player.ws.ReadMessage() + if err != nil { + panic(err) + } + + var msg = new(protobuf.Message) + if err = proto.Unmarshal(data, msg); err != nil { + continue + } + + player.channel.push(new(Message).Init(MessageTypePlayer, player, msg.Code, msg.Data)) + + } + + }) + + // HttpServer + slf.server = &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: router, + } + log.Info("GatewayStart", zap.String("listen", slf.server.Addr)) + + go func() { + if err := slf.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + stateMachine.errChannel <- err + return + } + }() + + return slf +} + +func (slf *gateway) shutdown(context context.Context) error { + log.Info("GatewayShutdown", zap.String("stateMachine", "start")) + if err := slf.server.Shutdown(context); err != nil { + return err + } + log.Info("GatewayShutdown", zap.String("stateMachine", "normal")) + return nil +} diff --git a/game/gateway/conn/ordinary.go b/game/gateway/conn/ordinary.go new file mode 100644 index 0000000..b7915a8 --- /dev/null +++ b/game/gateway/conn/ordinary.go @@ -0,0 +1,18 @@ +package conn + +import ( + "minotaur/game" +) + +func NewOrdinary() game.OnCreateConnHandleFunc { + return func() game.Conn { + return new(Ordinary) + } +} + +type Ordinary struct { +} + +func (slf *Ordinary) GetConn() any { + return slf +} diff --git a/game/message.go b/game/message.go new file mode 100644 index 0000000..c04fde5 --- /dev/null +++ b/game/message.go @@ -0,0 +1,34 @@ +package game + +const ( + // MessageTypePlayer 玩家通过客户端发送到服务端的消息 + MessageTypePlayer byte = iota + // MessageTypeEvent 服务端内部事件消息 + MessageTypeEvent +) + +// EventTypeGuestJoin 服务器内部事件消息类型 +const ( + EventTypeGuestJoin byte = iota // 访客加入事件 + EventTypeGuestLeave // 访客离开事件 +) + +// Message 游戏消息数据结构 +type Message struct { + t byte + args []any +} + +func (slf *Message) Init(t byte, args ...any) *Message { + slf.t = t + slf.args = args + return slf +} + +func (slf *Message) Type() byte { + return slf.t +} + +func (slf *Message) Args() []any { + return slf.args +} diff --git a/game/player.go b/game/player.go new file mode 100644 index 0000000..40394ed --- /dev/null +++ b/game/player.go @@ -0,0 +1,192 @@ +package game + +import ( + "fmt" + "github.com/gorilla/websocket" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + "minotaur/game/protobuf/messagepack" + "minotaur/game/protobuf/protobuf" + "minotaur/utils/log" + "minotaur/utils/timer" +) + +// Player 玩家数据结构 +type Player struct { + *channel // 玩家所在频道信息 + + guid int64 // 玩家guid + ip string // 玩家IP + conn Conn // 玩家连接信息 + ws *websocket.Conn // 玩家 WebSocket 连接 + login byte // 登录状态 0: 无需登录 1: 登录成功 + + // 游戏定时器 + // + // GameTimer: 状态机级别定时器; + // ChannelTimer: 频道级别定时器; + // Manager: 玩家级别定时器。 + *timer.Manager + GameTimer, ChannelTimer *timer.Manager +} + +// Login 设置玩家是否登录成功 +func (slf *Player) Login(success bool) { + slf.stateMachine.router.OnPlayerLoginEvent(slf, success) + if success { + slf.StopTimer(loginTimeoutTimerName) + slf.login = 1 + } else { + slf.exit("login failed") + } +} + +// IsLogin 获取玩家是否已登录 +func (slf *Player) IsLogin() bool { + return slf.login == 1 +} + +// GetGuid 获取玩家GUID +func (slf *Player) GetGuid() int64 { + return slf.guid +} + +// GetChannelId 返回玩家所在频道id +func (slf *Player) GetChannelId() any { + return slf.channel.id +} + +// GetConn 获取连接信息 +func (slf *Player) GetConn() any { + return slf.conn.GetConn() +} + +// Close 关闭玩家连接 +func (slf *Player) Close() { + _ = slf.ws.Close() +} + +// PushToClient 推送消息到客户端 +func (slf *Player) PushToClient(messageCode int32, message proto.Message) { + data, err := messagepack.Pack(messageCode, message) + if err != nil { + panic(err) + } + + if err = slf.ws.WriteMessage(2, data); err != nil { + panic(err) + } +} + +// PushToClients 推送消息到多个玩家客户端 +func (slf *Player) PushToClients(messageCode int32, message proto.Message, playerGuids ...int64) []PushFail { + data, err := messagepack.Pack(messageCode, message) + if err != nil { + panic(err) + } + + if !slf.channel.alone { + slf.channel.lock.RLock() + defer slf.channel.lock.RUnlock() + } + + var pushFails []PushFail + for _, id := range playerGuids { + player := slf.channel.players[id] + if player == nil { + pushFails = append(pushFails, PushFail{ + Data: data, + Err: fmt.Errorf("player [%v] not found", id), + }) + continue + } + + if err = player.ws.WriteMessage(2, data); err != nil { + pushFails = append(pushFails, PushFail{ + Player: player, + Data: data, + Err: err, + }) + } + } + return pushFails +} + +// PushToChannelPlayerClients 推送消息到频道内所有玩家客户端,可通过指定玩家或玩家id排除特定玩家 +func (slf *Player) PushToChannelPlayerClients(messageCode int32, message proto.Message, excludeGuids ...int64) []PushFail { + data, err := messagepack.Pack(messageCode, message) + if err != nil { + panic(err) + } + + var pushFails []PushFail + if !slf.channel.alone { + slf.channel.lock.RLock() + defer slf.channel.lock.RUnlock() + } + for id, player := range slf.channel.players { + var exclude bool + for _, target := range excludeGuids { + if id == target { + exclude = true + break + } + } + if exclude { + break + } + + if err = player.ws.WriteMessage(2, data); err != nil { + pushFails = append(pushFails, PushFail{ + Player: player, + Data: data, + Err: err, + }) + } + } + return pushFails +} + +// PushToChannel 推送消息到频道 +func (slf *Player) PushToChannel(messageCode protobuf.MessageCode, message proto.Message) { + slf.channel.push(new(Message).Init(MessageTypePlayer, slf, messageCode, message)) +} + +// ChangeChannel 改变玩家所在频道 +func (slf *Player) ChangeChannel(channelId any) error { + channel := slf.channel.stateMachine.channel(channelId, slf.channel.size, slf.channel.alone) + _, err := channel.join(slf) + return err +} + +// exit 退出游戏 +func (slf *Player) exit(reason ...string) { + var r = "normal exit" + if len(reason) > 0 { + r = reason[0] + } + + slf.channel.push(new(Message).Init(MessageTypeEvent, EventTypeGuestLeave, slf, func(player *Player) { + slf.Release() + _ = slf.ws.Close() + if slf.channel != nil { + slf.channel.lock.Lock() + defer slf.channel.lock.Unlock() + delete(slf.channel.players, slf.guid) + log.Info("ChannelLeave", + zap.Any("channelID", slf.channel.id), + zap.Int64("playerGuid", slf.guid), + zap.Int("headcount", len(slf.channel.players)), + zap.String("address", slf.ip), + zap.String("reason", r), + ) + slf.channel.tryRelease() + } else { + log.Info("ChannelLeave", + zap.Int64("playerGuid", slf.guid), + zap.String("address", slf.ip), + zap.String("reason", r), + ) + } + })) +} diff --git a/game/protobuf/message.proto b/game/protobuf/message.proto new file mode 100644 index 0000000..5e1cfb4 --- /dev/null +++ b/game/protobuf/message.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; +// protoc --proto_path . --go_out=./game/protobuf/protobuf/ --go-grpc_out=./game/protobuf/protobuf/ ./game/protobuf/*.proto + +package protobuf; +option go_package = "./;protobuf"; + +enum MessageCode { + SystemHeartbeat = 0; // 心跳 +} + +//通用消息 +message Message { + int32 code = 1; + bytes data = 2; +} \ No newline at end of file diff --git a/game/protobuf/messagepack/pack.go b/game/protobuf/messagepack/pack.go new file mode 100644 index 0000000..2323fdb --- /dev/null +++ b/game/protobuf/messagepack/pack.go @@ -0,0 +1,21 @@ +package messagepack + +import ( + "google.golang.org/protobuf/proto" + "minotaur/game/protobuf/protobuf" +) + +// Pack 消息打包 +func Pack(messageCode int32, message proto.Message) ([]byte, error) { + data, err := proto.Marshal(message) + if err != nil { + return nil, err + } + + msg := &protobuf.Message{ + Code: messageCode, + Data: data, + } + + return proto.Marshal(msg) +} diff --git a/game/protobuf/protobuf/message.pb.go b/game/protobuf/protobuf/message.pb.go new file mode 100644 index 0000000..2a56162 --- /dev/null +++ b/game/protobuf/protobuf/message.pb.go @@ -0,0 +1,201 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v4.22.1 +// source: game/protobuf/message.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type MessageCode int32 + +const ( + MessageCode_SystemHeartbeat MessageCode = 0 // 心跳 +) + +// Enum value maps for MessageCode. +var ( + MessageCode_name = map[int32]string{ + 0: "SystemHeartbeat", + } + MessageCode_value = map[string]int32{ + "SystemHeartbeat": 0, + } +) + +func (x MessageCode) Enum() *MessageCode { + p := new(MessageCode) + *p = x + return p +} + +func (x MessageCode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MessageCode) Descriptor() protoreflect.EnumDescriptor { + return file_game_protobuf_message_proto_enumTypes[0].Descriptor() +} + +func (MessageCode) Type() protoreflect.EnumType { + return &file_game_protobuf_message_proto_enumTypes[0] +} + +func (x MessageCode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MessageCode.Descriptor instead. +func (MessageCode) EnumDescriptor() ([]byte, []int) { + return file_game_protobuf_message_proto_rawDescGZIP(), []int{0} +} + +// 通用消息 +type Message struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *Message) Reset() { + *x = Message{} + if protoimpl.UnsafeEnabled { + mi := &file_game_protobuf_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_game_protobuf_message_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_game_protobuf_message_proto_rawDescGZIP(), []int{0} +} + +func (x *Message) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *Message) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +var File_game_protobuf_message_proto protoreflect.FileDescriptor + +var file_game_protobuf_message_proto_rawDesc = []byte{ + 0x0a, 0x1b, 0x67, 0x61, 0x6d, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x22, 0x31, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x2a, 0x22, 0x0a, 0x0b, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x10, 0x00, 0x42, 0x0d, + 0x5a, 0x0b, 0x2e, 0x2f, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_game_protobuf_message_proto_rawDescOnce sync.Once + file_game_protobuf_message_proto_rawDescData = file_game_protobuf_message_proto_rawDesc +) + +func file_game_protobuf_message_proto_rawDescGZIP() []byte { + file_game_protobuf_message_proto_rawDescOnce.Do(func() { + file_game_protobuf_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_game_protobuf_message_proto_rawDescData) + }) + return file_game_protobuf_message_proto_rawDescData +} + +var file_game_protobuf_message_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_game_protobuf_message_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_game_protobuf_message_proto_goTypes = []interface{}{ + (MessageCode)(0), // 0: protobuf.MessageCode + (*Message)(nil), // 1: protobuf.Message +} +var file_game_protobuf_message_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_game_protobuf_message_proto_init() } +func file_game_protobuf_message_proto_init() { + if File_game_protobuf_message_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_game_protobuf_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_game_protobuf_message_proto_rawDesc, + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_game_protobuf_message_proto_goTypes, + DependencyIndexes: file_game_protobuf_message_proto_depIdxs, + EnumInfos: file_game_protobuf_message_proto_enumTypes, + MessageInfos: file_game_protobuf_message_proto_msgTypes, + }.Build() + File_game_protobuf_message_proto = out.File + file_game_protobuf_message_proto_rawDesc = nil + file_game_protobuf_message_proto_goTypes = nil + file_game_protobuf_message_proto_depIdxs = nil +} diff --git a/game/protobuf/protobuf/system.pb.go b/game/protobuf/protobuf/system.pb.go new file mode 100644 index 0000000..798bf70 --- /dev/null +++ b/game/protobuf/protobuf/system.pb.go @@ -0,0 +1,134 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v4.22.1 +// source: game/protobuf/system.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// 心跳 +type SystemHeartbeatClient struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SystemHeartbeatClient) Reset() { + *x = SystemHeartbeatClient{} + if protoimpl.UnsafeEnabled { + mi := &file_game_protobuf_system_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SystemHeartbeatClient) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SystemHeartbeatClient) ProtoMessage() {} + +func (x *SystemHeartbeatClient) ProtoReflect() protoreflect.Message { + mi := &file_game_protobuf_system_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SystemHeartbeatClient.ProtoReflect.Descriptor instead. +func (*SystemHeartbeatClient) Descriptor() ([]byte, []int) { + return file_game_protobuf_system_proto_rawDescGZIP(), []int{0} +} + +var File_game_protobuf_system_proto protoreflect.FileDescriptor + +var file_game_protobuf_system_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x67, 0x61, 0x6d, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, + 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x22, 0x17, 0x0a, 0x15, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x42, + 0x0d, 0x5a, 0x0b, 0x2e, 0x2f, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_game_protobuf_system_proto_rawDescOnce sync.Once + file_game_protobuf_system_proto_rawDescData = file_game_protobuf_system_proto_rawDesc +) + +func file_game_protobuf_system_proto_rawDescGZIP() []byte { + file_game_protobuf_system_proto_rawDescOnce.Do(func() { + file_game_protobuf_system_proto_rawDescData = protoimpl.X.CompressGZIP(file_game_protobuf_system_proto_rawDescData) + }) + return file_game_protobuf_system_proto_rawDescData +} + +var file_game_protobuf_system_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_game_protobuf_system_proto_goTypes = []interface{}{ + (*SystemHeartbeatClient)(nil), // 0: protobuf.SystemHeartbeatClient +} +var file_game_protobuf_system_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_game_protobuf_system_proto_init() } +func file_game_protobuf_system_proto_init() { + if File_game_protobuf_system_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_game_protobuf_system_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SystemHeartbeatClient); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_game_protobuf_system_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_game_protobuf_system_proto_goTypes, + DependencyIndexes: file_game_protobuf_system_proto_depIdxs, + MessageInfos: file_game_protobuf_system_proto_msgTypes, + }.Build() + File_game_protobuf_system_proto = out.File + file_game_protobuf_system_proto_rawDesc = nil + file_game_protobuf_system_proto_goTypes = nil + file_game_protobuf_system_proto_depIdxs = nil +} diff --git a/game/protobuf/system.proto b/game/protobuf/system.proto new file mode 100644 index 0000000..bf00a13 --- /dev/null +++ b/game/protobuf/system.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +// protoc --proto_path . --go_out=./game/protobuf/protobuf/ --go-grpc_out=./game/protobuf/protobuf/ ./game/protobuf/*.proto + +package protobuf; +option go_package = "./;protobuf"; + +// 心跳 +message SystemHeartbeatClient {} \ No newline at end of file diff --git a/game/pushfail.go b/game/pushfail.go new file mode 100644 index 0000000..ac8c006 --- /dev/null +++ b/game/pushfail.go @@ -0,0 +1,8 @@ +package game + +// PushFail 消息推送失败信息 +type PushFail struct { + Player *Player + Data []byte + Err error +} diff --git a/game/router.go b/game/router.go new file mode 100644 index 0000000..9e4b456 --- /dev/null +++ b/game/router.go @@ -0,0 +1,123 @@ +package game + +import ( + "fmt" + "go.uber.org/zap" + "minotaur/game/protobuf/protobuf" + "minotaur/utils/log" + "reflect" +) + +var allowReplaceMessages = map[int32]bool{ + int32(protobuf.MessageCode_SystemHeartbeat): true, +} + +type router struct { + handles map[int32][2]any // 玩家消息表 + + guestPlayerJoinEvents []func(guest *Player) + guestPlayerLeaveEvents []func(guest *Player) + heartbeatEvents []func(player *Player) + playerLoginEvents []func(player *Player, success bool) + gameLaunchEvents []func() +} + +func (slf *router) init(stateMachine *StateMachine) *router { + slf.RegMessagePlayer(int32(protobuf.MessageCode_SystemHeartbeat), func(player *Player) { + slf.OnHeartbeatEvent(player) + }) + + return slf +} + +// RegGameLaunchEvent 注册游戏启动完成事件 +func (slf *router) RegGameLaunchEvent(handleFunc func()) { + slf.gameLaunchEvents = append(slf.gameLaunchEvents, handleFunc) +} + +// OnGameLaunchEvent 游戏启动完成事件发生时 +func (slf *router) OnGameLaunchEvent() { + for _, event := range slf.gameLaunchEvents { + event() + } +} + +// RegPlayerLoginEvent 注册玩家登录事件 +func (slf *router) RegPlayerLoginEvent(handleFunc func(player *Player, success bool)) { + slf.playerLoginEvents = append(slf.playerLoginEvents, handleFunc) +} + +// OnPlayerLoginEvent 玩家登录事件发生时 +func (slf *router) OnPlayerLoginEvent(player *Player, success bool) { + for _, event := range slf.playerLoginEvents { + event(player, success) + } +} + +// RegHeartbeatEvent 注册客户端心跳事件 +func (slf *router) RegHeartbeatEvent(handleFunc func(guest *Player)) { + slf.heartbeatEvents = append(slf.heartbeatEvents, handleFunc) +} + +// OnHeartbeatEvent 客户端心跳事件发生时 +func (slf *router) OnHeartbeatEvent(guest *Player) { + for _, event := range slf.heartbeatEvents { + event(guest) + } +} + +// RegGuestPlayerJoinEvent 注册访客玩家加入事件 +func (slf *router) RegGuestPlayerJoinEvent(handleFunc func(guest *Player)) { + slf.guestPlayerJoinEvents = append(slf.guestPlayerJoinEvents, handleFunc) +} + +// OnGuestPlayerJoinEvent 访问玩家加入时 +func (slf *router) OnGuestPlayerJoinEvent(guest *Player) { + for _, event := range slf.guestPlayerJoinEvents { + event(guest) + } +} + +// RegGuestPlayerLeaveEvent 注册访客玩家离开事件 +func (slf *router) RegGuestPlayerLeaveEvent(handleFunc func(guest *Player)) { + slf.guestPlayerLeaveEvents = append(slf.guestPlayerLeaveEvents, handleFunc) +} + +// OnGuestPlayerLeaveEvent 访问玩家离开时 +func (slf *router) OnGuestPlayerLeaveEvent(guest *Player) { + for _, event := range slf.guestPlayerLeaveEvents { + event(guest) + } +} + +// RegMessagePlayer 注册玩家消息 +func (slf *router) RegMessagePlayer(messageCode int32, handleFunc any) { + if slf.handles == nil { + slf.handles = map[int32][2]any{} + } + + typeOf := reflect.TypeOf(handleFunc) + if typeOf.Kind() != reflect.Func { + panic(fmt.Errorf("register player message failed, handleFunc must is func(player *Player, message *protobufType)")) + } + if typeOf.NumIn() == 0 { + panic(fmt.Errorf("register player message failed, handleFunc must is func(player *Player) or func(player *Player, message *protobufType)")) + } + handle, exist := slf.handles[messageCode] + if exist && !allowReplaceMessages[messageCode] { + panic(fmt.Errorf("register player message failed, message %s existed and cannot be replaced", messageCode)) + } + + handle = [2]any{reflect.ValueOf(handleFunc), nil} + if typeOf.NumIn() == 2 { + messageType := typeOf.In(1) + handle[1] = messageType + } + + slf.handles[messageCode] = handle + if exist && allowReplaceMessages[messageCode] { + log.Info("RouterRegMessagePlayer", zap.Any("code", messageCode), zap.Any("messageType", handle[1]), zap.String("type", "replace")) + } else { + log.Info("RouterRegMessagePlayer", zap.Any("code", messageCode), zap.Any("messageType", handle[1])) + } +} diff --git a/game/statemachine.go b/game/statemachine.go new file mode 100644 index 0000000..66cf66f --- /dev/null +++ b/game/statemachine.go @@ -0,0 +1,150 @@ +package game + +import ( + "context" + "github.com/sony/sonyflake" + "go.uber.org/zap" + "minotaur/utils/log" + "minotaur/utils/super" + "minotaur/utils/timer" + "os" + "os/signal" + "strconv" + "sync" + "syscall" + "time" +) + +// StateMachine 游戏状态机,维护全局游戏信息 +type StateMachine struct { + *gateway // 网关 + *router // 路由器 + *timer.Manager // 定时器 + *Time // 游戏时间 + channelRWMutex sync.RWMutex // 频道锁 + channels map[any]*channel // 所有频道 + channelStrategy func() (channelId any, size int) // 频道策略 + errChannel chan error // 异步错误管道 + loginTimeout time.Duration // 玩家登录超时时间 + + closeErrors []error // 关闭错误集 + closingMutex sync.Mutex // 关闭锁 + closed bool // 是否已关闭 + + sonyflake *sonyflake.Sonyflake // 雪花id生成器 +} + +// SetLoginTimeout 设置玩家登录超时时间,登录超时时,玩家将被踢出游戏(默认无超时) +func (slf *StateMachine) SetLoginTimeout(duration time.Duration) { + slf.loginTimeout = duration +} + +// SetChannelStrategy 设置频道策略,返回频道id和最大容纳人数 +func (slf *StateMachine) SetChannelStrategy(onGetChannelId func() (channelId any, size int)) { + slf.channelStrategy = onGetChannelId +} + +func (slf *StateMachine) Init() *StateMachine { + slf.router = new(router).init(slf) + slf.Time = new(Time) + return slf +} + +// Run 运行状态机 +func (slf *StateMachine) Run(appName string, port int, onCreateConnHandleFunc OnCreateConnHandleFunc) { + slf.errChannel = make(chan error, 1) + slf.channels = map[any]*channel{} + if slf.channelStrategy == nil { + slf.channelStrategy = func() (channelId any, size int) { + return 0, 0 + } + } + + slf.gateway = new(gateway).run(slf, appName, port, onCreateConnHandleFunc) + slf.Manager = timer.GetManager(64) + slf.sonyflake = sonyflake.NewSonyflake(sonyflake.Settings{}) + + log.Info("StateMachineRun", zap.String("stateMachine", "finish")) + + systemSignal := make(chan os.Signal, 1) + signal.Notify(systemSignal, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) + select { + case <-systemSignal: + slf.Shutdown(nil) + case err := <-slf.errChannel: + slf.Shutdown(err) + } +} + +// Shutdown 停止状态机 +func (slf *StateMachine) Shutdown(err error) { + if err != nil { + slf.closeErrors = append(slf.closeErrors, err) + } + + slf.closingMutex.Lock() + defer slf.closingMutex.Unlock() + if slf.closed { + return + } + slf.closed = true + + var shutDownTimeoutContext, cancel = context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + if err = slf.gateway.shutdown(shutDownTimeoutContext); err != nil { + slf.closeErrors = append(slf.closeErrors, err) + } + for _, c := range slf.channels { + c.release() + } + + slf.Manager.Release() + if len(slf.closeErrors) > 0 { + log.Error("StateMachineShutdown", zap.Errors("errors", slf.closeErrors)) + } else { + log.Info("StateMachineShutdown", zap.String("stateMachine", "normal")) + } +} + +// channel 获取频道,如果频道不存在将会进行创建 +// +// size: 指定频道最大容纳人数( <= 0 无限制) +// alone: 指定频道创建时候的逻辑是否以单线程运行 (default: true) +func (slf *StateMachine) channel(channelId any, size int, alone ...bool) *channel { + slf.channelRWMutex.Lock() + defer slf.channelRWMutex.Unlock() + + isAlone := true + if len(alone) > 0 { + isAlone = alone[0] + } + c := slf.channels[channelId] + if c == nil { + c = &channel{ + id: channelId, + size: size, + stateMachine: slf, + alone: isAlone, + players: map[int64]*Player{}, + timer: timer.GetManager(64), + } + slf.channels[channelId] = c + c.run() + log.Info("ChannelCreate", + zap.Any("channelID", channelId), + zap.Bool("alone", isAlone), + zap.Int("count", len(slf.channels)), + zap.String("size", super.If(c.size <= 0, "NaN", strconv.Itoa(c.size)))) + } + return c +} + +// GenerateGuid 生成一个 Guid +func (slf *StateMachine) GenerateGuid() int64 { + guid, err := slf.sonyflake.NextID() + if err != nil { + panic(err) + } + return int64(guid) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7d8abe9 --- /dev/null +++ b/go.mod @@ -0,0 +1,44 @@ +module minotaur + +go 1.20 + +require ( + github.com/gin-contrib/pprof v1.4.0 + github.com/gin-gonic/gin v1.9.0 + github.com/gorilla/websocket v1.5.0 + github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible + go.uber.org/zap v1.24.0 +) + +require ( + github.com/RussellLuo/timingwheel v0.0.0-20220218152713-54845bda3108 // indirect + github.com/bytedance/sonic v1.8.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.11.2 // indirect + github.com/goccy/go-json v0.10.0 // indirect + github.com/jonboulle/clockwork v0.3.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/lestrrat-go/strftime v1.0.6 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sony/sonyflake v1.1.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.9 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect + golang.org/x/crypto v0.5.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3c95fb4 --- /dev/null +++ b/go.sum @@ -0,0 +1,143 @@ +github.com/RussellLuo/timingwheel v0.0.0-20220218152713-54845bda3108 h1:iPugyBI7oFtbDZXC4dnY093M1kZx6k/95sen92gafbY= +github.com/RussellLuo/timingwheel v0.0.0-20220218152713-54845bda3108/go.mod h1:WAMLHwunr1hi3u7OjGV6/VWG9QbdMhGpEKjROiSFd10= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= +github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg= +github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= +github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= +github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= +github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= +github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ= +github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/sony/sonyflake v1.1.0 h1:wnrEcL3aOkWmPlhScLEGAXKkLAIslnBteNUq4Bw6MM4= +github.com/sony/sonyflake v1.1.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= +github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/utils/bypassflow/bypassflow.go b/utils/bypassflow/bypassflow.go new file mode 100644 index 0000000..a4fbf5c --- /dev/null +++ b/utils/bypassflow/bypassflow.go @@ -0,0 +1,57 @@ +package bypassflow + +import ( + "context" + "go.uber.org/zap" + "minotaur/utils/hash" + "minotaur/utils/log" + "runtime/debug" +) + +// BypassFlow 分流器 +type BypassFlow struct { + consistency *hash.Consistency + processor []chan func() +} + +func New(ctx context.Context, nodeCount, nodeBuffer int) *BypassFlow { + bypassFlow := &BypassFlow{ + consistency: &hash.Consistency{}, + processor: make([]chan func(), nodeCount), + } + + for i := 0; i < nodeCount; i++ { + bypassFlow.consistency.AddNode(i) + + nodeChan := make(chan func(), nodeBuffer) + bypassFlow.processor[i] = nodeChan + go func() { + for { + select { + case f := <-nodeChan: + go func() { + f() + defer func() { + if err := recover(); err != nil { + log.Error("BypassFlow", zap.Any("error", err), zap.Any("stack\n", debug.Stack())) + } + }() + }() + case <-ctx.Done(): + close(nodeChan) + return + } + } + }() + } + return bypassFlow +} + +func (slf *BypassFlow) getNode(item Item) int { + return slf.consistency.PickNode(item) +} + +func (slf *BypassFlow) Handle(item Item, handleFunc func()) { + node := slf.getNode(item) + slf.processor[node] <- handleFunc +} diff --git a/utils/bypassflow/item.go b/utils/bypassflow/item.go new file mode 100644 index 0000000..a60fe45 --- /dev/null +++ b/utils/bypassflow/item.go @@ -0,0 +1,5 @@ +package bypassflow + +type Item interface { + UniqueIdentification() any +} diff --git a/utils/g2d/coordinate.go b/utils/g2d/coordinate.go new file mode 100644 index 0000000..2445abc --- /dev/null +++ b/utils/g2d/coordinate.go @@ -0,0 +1 @@ +package g2d diff --git a/utils/g2d/g2d.go b/utils/g2d/g2d.go new file mode 100644 index 0000000..3e17831 --- /dev/null +++ b/utils/g2d/g2d.go @@ -0,0 +1,8 @@ +package g2d + +import "minotaur/utils/g2d/matrix" + +// NewMatrix 生成特定宽高的二维矩阵 +func NewMatrix[T any](width, height int) *matrix.Matrix[T] { + return matrix.NewMatrix[T](width, height) +} diff --git a/utils/g2d/matrix/matrix.go b/utils/g2d/matrix/matrix.go new file mode 100644 index 0000000..0b49893 --- /dev/null +++ b/utils/g2d/matrix/matrix.go @@ -0,0 +1,59 @@ +package matrix + +// NewMatrix 生成特定宽高的二维矩阵 +func NewMatrix[T any](width, height int) *Matrix[T] { + matrix := &Matrix[T]{ + w: width, h: height, + } + matrix.m = make([][]T, width) + for x := 0; x < len(matrix.m); x++ { + matrix.m[x] = make([]T, height) + } + return matrix +} + +// Matrix 二维矩阵 +type Matrix[T any] struct { + w, h int + m [][]T +} + +// GetWidth 获取二维矩阵宽度 +func (slf *Matrix[T]) GetWidth() int { + return slf.w +} + +// GetHeight 获取二维矩阵高度 +func (slf *Matrix[T]) GetHeight() int { + return slf.h +} + +// GetMatrix 获取二维矩阵 +func (slf *Matrix[T]) GetMatrix() [][]T { + return slf.m +} + +// Get 获取特定坐标的内容 +func (slf *Matrix[T]) Get(x, y int) T { + return slf.m[x][y] +} + +// Set 设置特定坐标的内容 +func (slf *Matrix[T]) Set(x, y int, data T) { + slf.m[x][y] = data +} + +// Swap 交换两个位置的内容 +func (slf *Matrix[T]) Swap(x1, y1, x2, y2 int) { + a, b := slf.Get(x1, y1), slf.Get(x2, y2) + slf.m[x1][y1], slf.m[x2][y2] = b, a +} + +// TrySwap 尝试交换两个位置的内容,交换后不满足表达式时进行撤销 +func (slf *Matrix[T]) TrySwap(x1, y1, x2, y2 int, expression bool) { + a, b := slf.Get(x1, y1), slf.Get(x2, y2) + slf.m[x1][y1], slf.m[x2][y2] = b, a + if expression { + slf.m[x1][y1], slf.m[x2][y2] = a, b + } +} diff --git a/utils/gin/middlewares/cros.go b/utils/gin/middlewares/cros.go new file mode 100644 index 0000000..c965b14 --- /dev/null +++ b/utils/gin/middlewares/cros.go @@ -0,0 +1,43 @@ +package middlewares + +import ( + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "minotaur/utils/log" + "net/http" + "runtime/debug" +) + +// Cors 设置跨域 +func Cors() gin.HandlerFunc { + return func(c *gin.Context) { + method := c.Request.Method + origin := c.Request.Header.Get("Origin") // 请求头部 + if origin != "" { + // 接收客户端发送的origin (重要!) + c.Writer.Header().Set("Access-Control-Allow-Origin", origin) + // 服务器支持的所有跨域请求的方法 + c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") + // 允许跨域设置可以返回其他子段,可以自定义字段 + c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session") + // 允许浏览器(客户端)可以解析的头部 (重要) + c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers") + // 设置缓存时间 + // c.Header("Access-Control-Max-Age", "172800") + // 允许客户端传递校验信息比如 cookie (重要) + c.Header("Access-Control-Allow-Credentials", "true") + } + + // 允许类型校验 + if method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + } + + defer func() { + if err := recover(); err != nil { + log.Error("Panic info is", zap.Any("err", err), zap.Any("stack\n", string(debug.Stack()))) + } + }() + c.Next() + } +} diff --git a/utils/gin/middlewares/logger.go b/utils/gin/middlewares/logger.go new file mode 100644 index 0000000..52a99be --- /dev/null +++ b/utils/gin/middlewares/logger.go @@ -0,0 +1,28 @@ +package middlewares + +import ( + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "time" +) + +func Logger(logger *zap.Logger) gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := c.Request.URL.RawQuery + c.Next() + + cost := time.Since(start) + logger.Info(path, + zap.Int("status", c.Writer.Status()), + zap.String("method", c.Request.Method), + zap.String("path", path), + zap.String("query", query), + zap.String("ip", c.ClientIP()), + zap.String("user-agent", c.Request.UserAgent()), + zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), + zap.Duration("cost", cost), + ) + } +} diff --git a/utils/hash/consistency.go b/utils/hash/consistency.go new file mode 100644 index 0000000..b1e2cfb --- /dev/null +++ b/utils/hash/consistency.go @@ -0,0 +1,71 @@ +package hash + +import ( + "fmt" + "hash/crc32" + "sort" + "strconv" + "strings" +) + +// Consistency 一致性哈希生成 +// +// https://blog.csdn.net/zhpCSDN921011/article/details/126845397 +type Consistency struct { + Replicas int // 虚拟节点的数量 + keys []int // 所有虚拟节点的哈希值 + hashMap map[int]int // 虚拟节点的哈希值: 节点(虚拟节点映射到真实节点) +} + +// AddNode 添加节点 +func (slf *Consistency) AddNode(keys ...int) { + if slf.hashMap == nil { + slf.hashMap = map[int]int{} + } + if slf.Replicas == 0 { + slf.Replicas = 3 + } + for _, key := range keys { + for i := 0; i < slf.Replicas; i++ { + // 计算虚拟节点哈希值 + hash := int(crc32.ChecksumIEEE([]byte(strconv.Itoa(i) + strconv.Itoa(key)))) + + // 存储虚拟节点哈希值 + slf.keys = append(slf.keys, hash) + + // 存入map做映射 + slf.hashMap[hash] = key + } + } + + //排序哈希值,下面匹配的时候要二分搜索 + sort.Ints(slf.keys) +} + +// PickNode 获取与 key 最接近的节点 +func (slf *Consistency) PickNode(key any) int { + if len(slf.keys) == 0 { + return 0 + } + + partitionKey := fmt.Sprintf("%#v", key) + beg := strings.Index(partitionKey, "{") + end := strings.Index(partitionKey, "}") + if beg != -1 && !(end == -1 || end == beg+1) { + partitionKey = partitionKey[beg+1 : end] + } + + // 计算传入key的哈希值 + hash := int(crc32.ChecksumIEEE([]byte(partitionKey))) + + // sort.Search使用二分查找满足m.Keys[i]>=hash的最小哈希值 + idx := sort.Search(len(slf.keys), func(i int) bool { return slf.keys[i] >= hash }) + + // 若key的hash值大于最后一个虚拟节点的hash值,则选择第一个虚拟节点 + if idx == len(slf.keys) { + idx = 0 + } + + return slf.hashMap[slf.keys[idx]] + +} diff --git a/utils/log/log.go b/utils/log/log.go new file mode 100644 index 0000000..4d4e44e --- /dev/null +++ b/utils/log/log.go @@ -0,0 +1,96 @@ +package log + +import ( + "fmt" + rotatelogs "github.com/lestrrat-go/file-rotatelogs" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "io" + "os" + "time" +) + +var ( + Logger *zap.Logger + + Info func(msg string, fields ...zap.Field) + Warn func(msg string, fields ...zap.Field) + Debug func(msg string, fields ...zap.Field) + Error func(msg string, fields ...zap.Field) +) + +const ( + debug = true + logPath = "./logs" + logTime = 7 +) + +func init() { + Logger = newLogger() + + Info = Logger.Info + Warn = Logger.Warn + Debug = Logger.Debug + Error = Logger.Error +} + +func newLogger() *zap.Logger { + encoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ + MessageKey: "msg", + LevelKey: "level", + EncodeLevel: zapcore.CapitalLevelEncoder, + TimeKey: "ts", + EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format("2006-01-02 15:04:05")) + }, + CallerKey: "file", + EncodeCaller: zapcore.ShortCallerEncoder, + EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendInt64(int64(d) / 1000000) + }, + }) + + infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl == zapcore.InfoLevel + }) + debugLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl <= zapcore.FatalLevel + }) + warnLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= zapcore.ErrorLevel + }) + + var cores zapcore.Core + + if debug { + cores = zapcore.NewTee( + zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), debugLevel), + ) + } else { + infoWriter := getWriter(fmt.Sprintf("%s/info.log", logPath), logTime) + warnWriter := getWriter(fmt.Sprintf("%s/error.log", logPath), logTime) + cores = zapcore.NewTee( + zapcore.NewCore(encoder, zapcore.AddSync(infoWriter), infoLevel), + zapcore.NewCore(encoder, zapcore.AddSync(warnWriter), warnLevel), + ) + } + + return zap.New(cores, zap.AddCaller()) +} + +func getWriter(filename string, times int32) io.Writer { + // 生成rotatelogs的Logger 实际生成的文件名 demo.log.YYmmddHH + // demo.log是指向最新日志的链接 + // //保存7天内的日志,每天分割一次日志 + hook, err := rotatelogs.New( + filename+".%Y%m%d", // 没有使用go风格反人类的format格式 + rotatelogs.WithLinkName(filename), + rotatelogs.WithMaxAge(time.Hour*24*7), + rotatelogs.WithRotationTime(time.Hour*time.Duration(times)), + ) + + if err != nil { + panic(err) + } + return hook +} diff --git a/utils/super/if.go b/utils/super/if.go new file mode 100644 index 0000000..09509f2 --- /dev/null +++ b/utils/super/if.go @@ -0,0 +1,8 @@ +package super + +func If[T any](expression bool, t T, f T) T { + if expression { + return t + } + return f +} diff --git a/utils/timer/constants.go b/utils/timer/constants.go new file mode 100644 index 0000000..3cf682a --- /dev/null +++ b/utils/timer/constants.go @@ -0,0 +1,8 @@ +package timer + +import "time" + +var ( + // 时间轮一刻度长度 + timingWheelTick = time.Millisecond * 10 +) diff --git a/utils/timer/manager.go b/utils/timer/manager.go new file mode 100644 index 0000000..6fe01e4 --- /dev/null +++ b/utils/timer/manager.go @@ -0,0 +1,104 @@ +package timer + +import ( + "reflect" + "sync" + "time" + + "github.com/RussellLuo/timingwheel" +) + +// Manager 管理器 +type Manager struct { + timer *Timer + wheel *timingwheel.TimingWheel + timers map[string]*Scheduler + lock sync.RWMutex +} + +// Release 释放管理器,并将管理器重新放回 Timer 池中 +func (slf *Manager) Release() { + slf.timer.lock.Lock() + defer slf.timer.lock.Unlock() + + slf.lock.Lock() + + for name, scheduler := range slf.timers { + scheduler.close() + delete(slf.timers, name) + } + + slf.lock.Unlock() + + slf.timer.Managers = append(slf.timer.Managers, slf) +} + +// StopTimer 停止特定名称的调度器 +func (slf *Manager) StopTimer(name string) { + slf.lock.Lock() + defer slf.lock.Unlock() + + if s, ok := slf.timers[name]; ok { + s.close() + delete(slf.timers, name) + } +} + +// IsStopped 特定名称的调度器是否已停止 +func (slf *Manager) IsStopped(name string) bool { + slf.lock.RLock() + defer slf.lock.RUnlock() + if s, ok := slf.timers[name]; ok { + return s.isClosed() + } + return true +} + +// GetSchedulers 获取所有调度器名称 +func (slf *Manager) GetSchedulers() []string { + slf.lock.RLock() + defer slf.lock.RUnlock() + names := make([]string, 0, len(slf.timers)) + for name := range slf.timers { + names = append(names, name) + } + return names +} + +// After 设置一个在特定时间后运行一次的调度器 +func (slf *Manager) After(name string, after time.Duration, handleFunc interface{}, args ...interface{}) { + slf.Loop(name, after, timingWheelTick, 1, handleFunc, args...) +} + +// Loop 设置一个在特定时间后仿佛运行的调度器 +func (slf *Manager) Loop(name string, after, interval time.Duration, times int, handleFunc interface{}, args ...interface{}) { + slf.StopTimer(name) + + if after < timingWheelTick { + after = timingWheelTick + } + if interval < timingWheelTick { + interval = timingWheelTick + } + + var values = make([]reflect.Value, len(args)) + for i, v := range args { + values[i] = reflect.ValueOf(v) + } + + scheduler := &Scheduler{ + name: name, + after: after, + interval: interval, + total: times, + cbFunc: reflect.ValueOf(handleFunc), + cbArgs: values, + manager: slf, + } + + slf.lock.Lock() + slf.timers[name] = scheduler + scheduler.timer = slf.wheel.ScheduleFunc(scheduler, scheduler.caller) + slf.lock.Unlock() +} + diff --git a/utils/timer/scheduler.go b/utils/timer/scheduler.go new file mode 100644 index 0000000..3659384 --- /dev/null +++ b/utils/timer/scheduler.go @@ -0,0 +1,95 @@ +package timer + +import ( + "reflect" + "sync" + "time" + + "github.com/RussellLuo/timingwheel" +) + +// Scheduler 调度器 +type Scheduler struct { + name string + after time.Duration + interval time.Duration + + total int + trigger int + kill bool + + cbFunc reflect.Value + cbArgs []reflect.Value + + timer *timingwheel.Timer + + manager *Manager + + lock sync.RWMutex +} + +// Name 获取调度器名称 +func (slf *Scheduler) Name() string { + return slf.name +} + +// Next 获取下一次执行的时间 +func (slf *Scheduler) Next(prev time.Time) time.Time { + slf.lock.RLock() + defer slf.lock.RUnlock() + + if slf.kill || (slf.total > 0 && slf.trigger >= slf.total) { + return time.Time{} + } + if slf.trigger == 0 { + return prev.Add(slf.after) + } + return prev.Add(slf.interval) +} + +// 实际执行的任务 +func (slf *Scheduler) caller() { + // TODO: 直接调用可能会导致更高的并发复杂度 + slf.Caller() +} + +// Caller 可由外部发起调用的执行函数 +func (slf *Scheduler) Caller() { + slf.lock.Lock() + + if slf.kill { + slf.lock.Unlock() + return + } + + slf.trigger++ + if slf.total > 0 && slf.trigger >= slf.total { + slf.lock.Unlock() + slf.manager.StopTimer(slf.name) + } else { + slf.lock.Unlock() + } + slf.cbFunc.Call(slf.cbArgs) +} + +// isClosed 检查调度器是否已关闭 +func (slf *Scheduler) isClosed() bool { + slf.lock.RLock() + defer slf.lock.RUnlock() + + return slf.kill +} + +// close 关闭调度器 +func (slf *Scheduler) close() { + slf.lock.Lock() + defer slf.lock.Unlock() + + if slf.kill { + return + } + slf.kill = true + if slf.total <= 0 || slf.trigger < slf.total { + slf.timer.Stop() + } +} diff --git a/utils/timer/timer.go b/utils/timer/timer.go new file mode 100644 index 0000000..6811f2e --- /dev/null +++ b/utils/timer/timer.go @@ -0,0 +1,38 @@ +package timer + +import ( + "sync" + + "github.com/RussellLuo/timingwheel" +) + +var timer = new(Timer) + +func GetManager(size int) *Manager { + return timer.NewManager(size) +} + +type Timer struct { + Managers []*Manager + lock sync.Mutex +} + +func (slf *Timer) NewManager(size int) *Manager { + slf.lock.Lock() + defer slf.lock.Unlock() + + var manager *Manager + if len(slf.Managers) > 0 { + manager = slf.Managers[0] + slf.Managers = slf.Managers[1:] + return manager + } + + manager = &Manager{ + timer: slf, + wheel: timingwheel.NewTimingWheel(timingWheelTick, int64(size)), + timers: make(map[string]*Scheduler), + } + manager.wheel.Start() + return manager +}