From 97c68bafa2a48a0d7288ccc795286f1532ccd548 Mon Sep 17 00:00:00 2001 From: cialloo Date: Mon, 6 Oct 2025 18:35:04 +0800 Subject: [PATCH] Refactor server query logic and add new endpoint for listing monitored game servers --- .gitignore | 3 +- api/ServerStatistics.api | 42 ++---- src/go.mod | 1 - src/go.sum | 2 - src/internal/handler/routes.go | 8 +- ...2squeryhandler.go => serverlisthandler.go} | 10 +- src/internal/logic/a2squerylogic.go | 126 ------------------ src/internal/logic/serverlistlogic.go | 64 +++++++++ src/internal/svc/servicecontext.go | 29 ++++ src/internal/types/types.go | 43 ++---- 10 files changed, 130 insertions(+), 198 deletions(-) rename src/internal/handler/{a2squeryhandler.go => serverlisthandler.go} (63%) delete mode 100644 src/internal/logic/a2squerylogic.go create mode 100644 src/internal/logic/serverlistlogic.go diff --git a/.gitignore b/.gitignore index 03aa658..13b66cc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /api/ServerStatistics.yaml *.exe /src/src -/.vscode \ No newline at end of file +/.vscode +/.ignore \ No newline at end of file diff --git a/api/ServerStatistics.api b/api/ServerStatistics.api index e13ec24..a76dd5d 100644 --- a/api/ServerStatistics.api +++ b/api/ServerStatistics.api @@ -180,31 +180,15 @@ type ( ) type ( - A2SQueryReqServer { - ServerIP string `json:"serverIP"` // Server IP address - ServerPort int `json:"serverPort"` // Server port - Timeout int `json:"timeout,example=3000,default=3000"` // Timeout in milliseconds (default: 3000ms) - } - A2SQueryReq { - Servers []A2SQueryReqServer `json:"servers"` // List of servers to query - } - A2SQueryRespServer { - ServerName string `json:"serverName"` // Server name - MapName string `json:"mapName"` // Current map name - GameDirectory string `json:"gameDirectory"` // Game directory - GameDescription string `json:"gameDescription"` // Game description - AppID int `json:"appID"` // Steam App ID - PlayerCount int `json:"playerCount"` // Current player count - MaxPlayers int `json:"maxPlayers"` // Maximum player count - BotCount int `json:"botCount"` // Bot count - ServerType string `json:"serverType"` // Server type (e.g., "d" for dedicated) - Environment string `json:"environment"` // Environment (e.g., "w" for Windows, "l" for Linux) - Visibility int `json:"visibility"` // Visibility (0 for public, 1 for private) - VAC int `json:"vac"` // VAC status (0 for unsecured, 1 for secured) - Version string `json:"version"` // Server version - } - A2SQueryResp { - Results []A2SQueryRespServer `json:"results"` // List of query results for each server + ServerListReq {} + ServerListResp { + ServerName string `json:"serverName"` // Server name + ServerIP string `json:"serverIP"` // Server IP address + ServerPort int `json:"serverPort"` // Server port + Category string `json:"category"` // Server category + MapName string `json:"mapName"` // Current map name + PlayerCount int `json:"playerCount"` // Current player count + BotCount int `json:"botCount"` // Bot count } ) @@ -304,10 +288,10 @@ service ServerStatistics { post /top-killer (TopKillerReq) returns (TopKillerResp) @doc ( - summary: "Perform an A2S query to get real-time server information" - description: "Perform an A2S query to get real-time server information" + summary: "Get the list of monitored game servers" + description: "Get the list of monitored game servers" ) - @handler a2sQueryHandler - post /a2s-query (A2SQueryReq) returns (A2SQueryResp) + @handler ServerListHandler + get /list (ServerListReq) returns ([]ServerListResp) } diff --git a/src/go.mod b/src/go.mod index 395e2de..a60164d 100644 --- a/src/go.mod +++ b/src/go.mod @@ -29,7 +29,6 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rumblefrog/go-a2s v1.0.2 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect diff --git a/src/go.sum b/src/go.sum index b78da10..00d6652 100644 --- a/src/go.sum +++ b/src/go.sum @@ -63,8 +63,6 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/rumblefrog/go-a2s v1.0.2 h1:rT/QP/B+h2R9/3PEfmOkWPdHnEKExskOMPTTkeX+vuA= -github.com/rumblefrog/go-a2s v1.0.2/go.mod h1:6nq//LMUMa3ElowQ7eH8atnDbQG+nVMFsaMFzSo8p/M= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/src/internal/handler/routes.go b/src/internal/handler/routes.go index 9085cb4..84fe658 100644 --- a/src/internal/handler/routes.go +++ b/src/internal/handler/routes.go @@ -15,10 +15,10 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { server.AddRoutes( []rest.Route{ { - // Perform an A2S query to get real-time server information - Method: http.MethodPost, - Path: "/a2s-query", - Handler: a2sQueryHandler(serverCtx), + // Get the list of monitored game servers + Method: http.MethodGet, + Path: "/list", + Handler: ServerListHandler(serverCtx), }, { // Ping the server to check if it's alive diff --git a/src/internal/handler/a2squeryhandler.go b/src/internal/handler/serverlisthandler.go similarity index 63% rename from src/internal/handler/a2squeryhandler.go rename to src/internal/handler/serverlisthandler.go index dd1bd5b..78a3ded 100644 --- a/src/internal/handler/a2squeryhandler.go +++ b/src/internal/handler/serverlisthandler.go @@ -9,17 +9,17 @@ import ( "src/internal/types" ) -// Perform an A2S query to get real-time server information -func a2sQueryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { +// Get the list of monitored game servers +func ServerListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var req types.A2SQueryReq + var req types.ServerListReq if err := httpx.Parse(r, &req); err != nil { httpx.ErrorCtx(r.Context(), w, err) return } - l := logic.NewA2sQueryLogic(r.Context(), svcCtx) - resp, err := l.A2sQuery(&req) + l := logic.NewServerListLogic(r.Context(), svcCtx) + resp, err := l.ServerList(&req) if err != nil { httpx.ErrorCtx(r.Context(), w, err) } else { diff --git a/src/internal/logic/a2squerylogic.go b/src/internal/logic/a2squerylogic.go deleted file mode 100644 index 5064ab6..0000000 --- a/src/internal/logic/a2squerylogic.go +++ /dev/null @@ -1,126 +0,0 @@ -package logic - -import ( - "context" - "fmt" - "sync" - "time" - - "src/internal/svc" - "src/internal/types" - - "github.com/rumblefrog/go-a2s" - "github.com/zeromicro/go-zero/core/logx" -) - -type A2sQueryLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Perform an A2S query to get real-time server information -func NewA2sQueryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *A2sQueryLogic { - return &A2sQueryLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *A2sQueryLogic) A2sQuery(req *types.A2SQueryReq) (resp *types.A2SQueryResp, err error) { - if len(req.Servers) == 0 { - return &types.A2SQueryResp{Results: []types.A2SQueryRespServer{}}, nil - } - - // Channel to collect results with their indices - type indexedResult struct { - index int - result types.A2SQueryRespServer - } - results := make(chan indexedResult, len(req.Servers)) - var wg sync.WaitGroup - - // Query each server concurrently - for i, server := range req.Servers { - wg.Add(1) - go func(index int, srv types.A2SQueryReqServer) { - defer wg.Done() - - result := l.querySingleServer(srv) - results <- indexedResult{index: index, result: result} - }(i, server) - } - - // Close channel when all goroutines are done - go func() { - wg.Wait() - close(results) - }() - - // Collect all results and sort by index to maintain order - resp = &types.A2SQueryResp{ - Results: make([]types.A2SQueryRespServer, len(req.Servers)), - } - - for indexed := range results { - resp.Results[indexed.index] = indexed.result - } - - return resp, nil -} - -// querySingleServer performs A2S query for a single server -func (l *A2sQueryLogic) querySingleServer(req types.A2SQueryReqServer) types.A2SQueryRespServer { - // Set default timeout if not provided - timeout := time.Duration(req.Timeout) * time.Millisecond - if req.Timeout <= 0 { - timeout = 3 * time.Second - } - - // Create A2S client with timeout option - client, err := a2s.NewClient(fmt.Sprintf("%s:%d", req.ServerIP, req.ServerPort), a2s.TimeoutOption(timeout)) - if err != nil { - l.Logger.Errorf("Failed to create A2S client for %s:%d: %v", req.ServerIP, req.ServerPort, err) - return types.A2SQueryRespServer{ - ServerName: fmt.Sprintf("Error: %v", err), - } - } - defer client.Close() - - // Query server info - info, err := client.QueryInfo() - if err != nil { - l.Logger.Errorf("Failed to query server info for %s:%d: %v", req.ServerIP, req.ServerPort, err) - return types.A2SQueryRespServer{ - ServerName: fmt.Sprintf("Error: %v", err), - } - } - - // Convert boolean values to int - visibility := 0 - if info.Visibility { - visibility = 1 - } - vac := 0 - if info.VAC { - vac = 1 - } - - // Map response to our types - return types.A2SQueryRespServer{ - ServerName: info.Name, - MapName: info.Map, - GameDirectory: info.Folder, - GameDescription: info.Game, - AppID: int(info.ID), - PlayerCount: int(info.Players), - MaxPlayers: int(info.MaxPlayers), - BotCount: int(info.Bots), - ServerType: info.ServerType.String(), - Environment: info.ServerOS.String(), - Visibility: visibility, - VAC: vac, - Version: info.Version, - } -} diff --git a/src/internal/logic/serverlistlogic.go b/src/internal/logic/serverlistlogic.go new file mode 100644 index 0000000..63df4df --- /dev/null +++ b/src/internal/logic/serverlistlogic.go @@ -0,0 +1,64 @@ +package logic + +import ( + "context" + + "src/internal/svc" + "src/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ServerListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get the list of monitored game servers +func NewServerListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServerListLogic { + return &ServerListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ServerListLogic) ServerList(req *types.ServerListReq) (resp []types.ServerListResp, err error) { + // Query to select all servers from the database + query := `SELECT name, ip, port, category, bot_count, human_count, mapname FROM server ORDER BY id` + + rows, err := l.svcCtx.DB.Query(query) + if err != nil { + l.Logger.Errorf("Failed to query servers: %v", err) + return nil, err + } + defer rows.Close() + + // Iterate through the results and build the response + for rows.Next() { + var server types.ServerListResp + err = rows.Scan( + &server.ServerName, + &server.ServerIP, + &server.ServerPort, + &server.Category, + &server.BotCount, + &server.PlayerCount, + &server.MapName, + ) + if err != nil { + l.Logger.Errorf("Failed to scan server row: %v", err) + return nil, err + } + resp = append(resp, server) + } + + // Check for any error that occurred during iteration + if err = rows.Err(); err != nil { + l.Logger.Errorf("Error iterating through server rows: %v", err) + return nil, err + } + + return resp, nil +} diff --git a/src/internal/svc/servicecontext.go b/src/internal/svc/servicecontext.go index dee7b9a..a36778f 100644 --- a/src/internal/svc/servicecontext.go +++ b/src/internal/svc/servicecontext.go @@ -28,8 +28,37 @@ func NewServiceContext(c config.Config) *ServiceContext { log.Println("Database connection established successfully") + // Create tables if they don't exist + if err := createTables(db); err != nil { + log.Fatalf("Failed to create tables: %v", err) + } + return &ServiceContext{ Config: c, DB: db, } } + +func createTables(db *sql.DB) error { + // Create server table + serverTableQuery := ` + CREATE TABLE IF NOT EXISTS server ( + id SERIAL PRIMARY KEY, + name VARCHAR(64) NOT NULL, + ip VARCHAR(64) NOT NULL, + port INTEGER NOT NULL, + category VARCHAR(64) NOT NULL, + bot_count INTEGER NOT NULL, + human_count INTEGER NOT NULL, + mapname VARCHAR(64) NOT NULL, + last_update TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (ip, port) + );` + + if _, err := db.Exec(serverTableQuery); err != nil { + return err + } + + log.Println("Database tables created successfully") + return nil +} diff --git a/src/internal/types/types.go b/src/internal/types/types.go index d77a808..68b7517 100644 --- a/src/internal/types/types.go +++ b/src/internal/types/types.go @@ -3,36 +3,6 @@ package types -type A2SQueryReq struct { - Servers []A2SQueryReqServer `json:"servers"` // List of servers to query -} - -type A2SQueryReqServer struct { - ServerIP string `json:"serverIP"` // Server IP address - ServerPort int `json:"serverPort"` // Server port - Timeout int `json:"timeout,example=3000,default=3000"` // Timeout in milliseconds (default: 3000ms) -} - -type A2SQueryResp struct { - Results []A2SQueryRespServer `json:"results"` // List of query results for each server -} - -type A2SQueryRespServer struct { - ServerName string `json:"serverName"` // Server name - MapName string `json:"mapName"` // Current map name - GameDirectory string `json:"gameDirectory"` // Game directory - GameDescription string `json:"gameDescription"` // Game description - AppID int `json:"appID"` // Steam App ID - PlayerCount int `json:"playerCount"` // Current player count - MaxPlayers int `json:"maxPlayers"` // Maximum player count - BotCount int `json:"botCount"` // Bot count - ServerType string `json:"serverType"` // Server type (e.g., "d" for dedicated) - Environment string `json:"environment"` // Environment (e.g., "w" for Windows, "l" for Linux) - Visibility int `json:"visibility"` // Visibility (0 for public, 1 for private) - VAC int `json:"vac"` // VAC status (0 for unsecured, 1 for secured) - Version string `json:"version"` // Server version -} - type PingReq struct { } @@ -96,6 +66,19 @@ type RecentPlayRespPlayer struct { PlayTime int64 `json:"playTime"` // Playtime in seconds during the time range } +type ServerListReq struct { +} + +type ServerListResp struct { + ServerName string `json:"serverName"` // Server name + ServerIP string `json:"serverIP"` // Server IP address + ServerPort int `json:"serverPort"` // Server port + Category string `json:"category"` // Server category + MapName string `json:"mapName"` // Current map name + PlayerCount int `json:"playerCount"` // Current player count + BotCount int `json:"botCount"` // Bot count +} + type TopKillerReq struct { TimeRangeStart int64 `json:"timeRangeStart"` // Unix timestamp in milliseconds TimeRangeEnd int64 `json:"timeRangeEnd"` // Unix timestamp in milliseconds