Refactor server query logic and add new endpoint for listing monitored game servers
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 47s
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 47s
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
*.exe
|
*.exe
|
||||||
/src/src
|
/src/src
|
||||||
/.vscode
|
/.vscode
|
||||||
|
/.ignore
|
||||||
@@ -180,31 +180,15 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
A2SQueryReqServer {
|
ServerListReq {}
|
||||||
ServerIP string `json:"serverIP"` // Server IP address
|
ServerListResp {
|
||||||
ServerPort int `json:"serverPort"` // Server port
|
ServerName string `json:"serverName"` // Server name
|
||||||
Timeout int `json:"timeout,example=3000,default=3000"` // Timeout in milliseconds (default: 3000ms)
|
ServerIP string `json:"serverIP"` // Server IP address
|
||||||
}
|
ServerPort int `json:"serverPort"` // Server port
|
||||||
A2SQueryReq {
|
Category string `json:"category"` // Server category
|
||||||
Servers []A2SQueryReqServer `json:"servers"` // List of servers to query
|
MapName string `json:"mapName"` // Current map name
|
||||||
}
|
PlayerCount int `json:"playerCount"` // Current player count
|
||||||
A2SQueryRespServer {
|
BotCount int `json:"botCount"` // Bot count
|
||||||
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
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -304,10 +288,10 @@ service ServerStatistics {
|
|||||||
post /top-killer (TopKillerReq) returns (TopKillerResp)
|
post /top-killer (TopKillerReq) returns (TopKillerResp)
|
||||||
|
|
||||||
@doc (
|
@doc (
|
||||||
summary: "Perform an A2S query to get real-time server information"
|
summary: "Get the list of monitored game servers"
|
||||||
description: "Perform an A2S query to get real-time server information"
|
description: "Get the list of monitored game servers"
|
||||||
)
|
)
|
||||||
@handler a2sQueryHandler
|
@handler ServerListHandler
|
||||||
post /a2s-query (A2SQueryReq) returns (A2SQueryResp)
|
get /list (ServerListReq) returns ([]ServerListResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ require (
|
|||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.62.0 // indirect
|
github.com/prometheus/common v0.62.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // 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
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
|
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
|
||||||
|
|||||||
@@ -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/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 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
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 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
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=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
server.AddRoutes(
|
server.AddRoutes(
|
||||||
[]rest.Route{
|
[]rest.Route{
|
||||||
{
|
{
|
||||||
// Perform an A2S query to get real-time server information
|
// Get the list of monitored game servers
|
||||||
Method: http.MethodPost,
|
Method: http.MethodGet,
|
||||||
Path: "/a2s-query",
|
Path: "/list",
|
||||||
Handler: a2sQueryHandler(serverCtx),
|
Handler: ServerListHandler(serverCtx),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Ping the server to check if it's alive
|
// Ping the server to check if it's alive
|
||||||
|
|||||||
@@ -9,17 +9,17 @@ import (
|
|||||||
"src/internal/types"
|
"src/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Perform an A2S query to get real-time server information
|
// Get the list of monitored game servers
|
||||||
func a2sQueryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func ServerListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.A2SQueryReq
|
var req types.ServerListReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
httpx.ErrorCtx(r.Context(), w, err)
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l := logic.NewA2sQueryLogic(r.Context(), svcCtx)
|
l := logic.NewServerListLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.A2sQuery(&req)
|
resp, err := l.ServerList(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpx.ErrorCtx(r.Context(), w, err)
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
64
src/internal/logic/serverlistlogic.go
Normal file
64
src/internal/logic/serverlistlogic.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -28,8 +28,37 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
|
|
||||||
log.Println("Database connection established successfully")
|
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{
|
return &ServiceContext{
|
||||||
Config: c,
|
Config: c,
|
||||||
DB: db,
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,36 +3,6 @@
|
|||||||
|
|
||||||
package types
|
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 {
|
type PingReq struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +66,19 @@ type RecentPlayRespPlayer struct {
|
|||||||
PlayTime int64 `json:"playTime"` // Playtime in seconds during the time range
|
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 {
|
type TopKillerReq struct {
|
||||||
TimeRangeStart int64 `json:"timeRangeStart"` // Unix timestamp in milliseconds
|
TimeRangeStart int64 `json:"timeRangeStart"` // Unix timestamp in milliseconds
|
||||||
TimeRangeEnd int64 `json:"timeRangeEnd"` // Unix timestamp in milliseconds
|
TimeRangeEnd int64 `json:"timeRangeEnd"` // Unix timestamp in milliseconds
|
||||||
|
|||||||
Reference in New Issue
Block a user