From b556ca78f98bf6272d6cec4ab3f1f05e215b9ad9 Mon Sep 17 00:00:00 2001 From: mei Date: Sat, 5 Apr 2025 10:23:28 +0800 Subject: [PATCH] first commit --- example/index.html | 61 ++++ example/main.go | 301 ++++++++++++++++++++ example/server_192_168_1_214_25565.csv | 47 ++++ example/server_HY_HYPIXEL_COM_CN_25565.csv | 39 +++ go.mod | 6 + go.sum | 4 + main.go | 18 ++ minecraftquery/minecraftquery.go | 307 +++++++++++++++++++++ 8 files changed, 783 insertions(+) create mode 100644 example/index.html create mode 100644 example/main.go create mode 100644 example/server_192_168_1_214_25565.csv create mode 100644 example/server_HY_HYPIXEL_COM_CN_25565.csv create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 minecraftquery/minecraftquery.go diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..56fd3e7 --- /dev/null +++ b/example/index.html @@ -0,0 +1,61 @@ + + + + MC服务器监控 + + + + + +
+

MC服务器监控

+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..27192fd --- /dev/null +++ b/example/main.go @@ -0,0 +1,301 @@ +package main + +import ( + "bytes" + "encoding/binary" + "encoding/csv" + "encoding/json" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" +) + +// 配置部分 +var ( + servers = []struct { + Addr string + Port int + }{ + {"192.168.1.214", 25565}, + {"HY.HYPIXEL.COM.CN", 25565}, + } + + dataExpiration = 180 * 24 * time.Hour + checkInterval = 30 * time.Second + webPort = "8080" +) + +// 历史记录结构 +type HistoryEntry struct { + Timestamp time.Time `json:"timestamp"` + Online int `json:"online"` +} + +// Minecraft协议处理函数 +func writeVarInt(w io.Writer, value int) error { + for { + if (value & ^0x7F) == 0 { + return binary.Write(w, binary.BigEndian, byte(value)) + } + binary.Write(w, binary.BigEndian, byte((value&0x7F)|0x80)) + value = int(uint32(value) >> 7) + } +} + +func readVarInt(r io.Reader) (int, error) { + var num int + var shift uint + for { + b := make([]byte, 1) + _, err := r.Read(b) + if err != nil { + return 0, err + } + num |= int(b[0]&0x7F) << shift + shift += 7 + if b[0]&0x80 == 0 { + break + } + } + return num, nil +} + +func writeString(w io.Writer, s string) error { + if err := writeVarInt(w, len(s)); err != nil { + return err + } + _, err := w.Write([]byte(s)) + return err +} + +func readString(r io.Reader) (string, error) { + length, err := readVarInt(r) + if err != nil { + return "", err + } + data := make([]byte, length) + _, err = io.ReadFull(r, data) + return string(data), err +} + +func queryMinecraftServer(addr string, port int) (int, error) { + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", addr, port), 5*time.Second) + if err != nil { + return 0, err + } + defer conn.Close() + + // 握手包 + handshake := new(bytes.Buffer) + writeVarInt(handshake, 0) // 包ID + writeVarInt(handshake, -1) // 协议版本 + writeString(handshake, addr) // 服务器地址 + binary.Write(handshake, binary.BigEndian, uint16(port)) + writeVarInt(handshake, 1) // 下一个状态 (Status) + + // 发送握手包 + packet := new(bytes.Buffer) + writeVarInt(packet, handshake.Len()) + packet.Write(handshake.Bytes()) + if _, err := conn.Write(packet.Bytes()); err != nil { + return 0, err + } + + // 发送请求包 + request := new(bytes.Buffer) + writeVarInt(request, 0) // 请求包ID + packet.Reset() + writeVarInt(packet, request.Len()) + packet.Write(request.Bytes()) + if _, err := conn.Write(packet.Bytes()); err != nil { + return 0, err + } + + // 读取响应 + length, err := readVarInt(conn) + if err != nil { + return 0, err + } + data := make([]byte, length) + _, err = io.ReadFull(conn, data) + if err != nil { + return 0, err + } + + // 解析JSON + jsonStr, err := readString(bytes.NewReader(data[1:])) // 跳过包ID + if err != nil { + return 0, err + } + + var response struct { + Players struct { + Online int `json:"online"` + } `json:"players"` + } + if err := json.Unmarshal([]byte(jsonStr), &response); err != nil { + return 0, err + } + + return response.Players.Online, nil +} + +// 数据存储 +func getFilename(addr string, port int) string { + safeAddr := strings.ReplaceAll(addr, ".", "_") + return fmt.Sprintf("server_%s_%d.csv", safeAddr, port) +} + +func saveData(addr string, port, online int) error { + filename := getFilename(addr, port) + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + if stat, _ := file.Stat(); stat.Size() == 0 { + writer.Write([]string{"timestamp", "online"}) + } + + timestamp := time.Now().UTC().Format(time.RFC3339) + return writer.Write([]string{timestamp, strconv.Itoa(online)}) +} + +func cleanupData() { + for range time.Tick(24 * time.Hour) { + cutoff := time.Now().Add(-dataExpiration) + files, _ := filepath.Glob("server_*.csv") + for _, f := range files { + processFile(f, cutoff) + } + } +} + +func processFile(filename string, cutoff time.Time) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + reader := csv.NewReader(file) + records, _ := reader.ReadAll() + + var filtered [][]string + for i, r := range records { + if i == 0 { + filtered = append(filtered, r) + continue + } + if ts, err := time.Parse(time.RFC3339, r[0]); err == nil && ts.After(cutoff) { + filtered = append(filtered, r) + } + } + + file.Close() + os.WriteFile(filename, []byte{}, 0644) + file, _ = os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0644) + writer := csv.NewWriter(file) + writer.WriteAll(filtered) + writer.Flush() + file.Close() +} + +// Web界面 +func webHandler(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./index.html") +} + +func dataHandler(w http.ResponseWriter, r *http.Request) { + type serverData struct { + Addr string `json:"addr"` + Port int `json:"port"` + Current int `json:"current"` + Updated time.Time `json:"updated"` + History []HistoryEntry `json:"history"` + } + + var response []serverData + for _, s := range servers { + history := readHistory(s.Addr, s.Port) + current := 0 + updated := time.Time{} + if len(history) > 0 { + current = history[len(history)-1].Online + updated = history[len(history)-1].Timestamp + } + + response = append(response, serverData{ + Addr: s.Addr, + Port: s.Port, + Current: current, + Updated: updated, + History: history, + }) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func readHistory(addr string, port int) []HistoryEntry { + filename := getFilename(addr, port) + file, err := os.Open(filename) + if err != nil { + return nil + } + defer file.Close() + + reader := csv.NewReader(file) + records, err := reader.ReadAll() + if err != nil { + return nil + } + + var history []HistoryEntry + for i, r := range records { + if i == 0 { + continue + } + ts, _ := time.Parse(time.RFC3339, r[0]) + online, _ := strconv.Atoi(r[1]) + history = append(history, HistoryEntry{ts, online}) + } + return history +} + +func main() { + // 启动数据清理 + go cleanupData() + + // 启动监控任务 + go func() { + ticker := time.NewTicker(checkInterval) + for range ticker.C { + for _, s := range servers { + go func(addr string, port int) { + if online, err := queryMinecraftServer(addr, port); err == nil { + saveData(addr, port, online) + } + }(s.Addr, s.Port) + } + } + }() + + // 启动Web服务器 + http.HandleFunc("/", webHandler) + http.HandleFunc("/data", dataHandler) + log.Printf("Server running on :%s", webPort) + log.Fatal(http.ListenAndServe(":"+webPort, nil)) +} diff --git a/example/server_192_168_1_214_25565.csv b/example/server_192_168_1_214_25565.csv new file mode 100644 index 0000000..0f2f39c --- /dev/null +++ b/example/server_192_168_1_214_25565.csv @@ -0,0 +1,47 @@ +timestamp,online +2025-04-03T14:05:06Z,0 +2025-04-03T14:08:06Z,1 +2025-04-03T14:10:06Z,0 +2025-04-03T14:22:19Z,0 +2025-04-03T14:23:19Z,0 +2025-04-03T14:24:19Z,0 +2025-04-03T14:25:19Z,0 +2025-04-03T14:26:19Z,0 +2025-04-03T14:27:16Z,0 +2025-04-03T14:27:46Z,0 +2025-04-03T14:28:16Z,0 +2025-04-03T14:28:46Z,0 +2025-04-03T14:29:16Z,0 +2025-04-03T14:29:46Z,0 +2025-04-03T14:30:16Z,0 +2025-04-03T14:30:46Z,0 +2025-04-03T14:31:16Z,0 +2025-04-03T14:31:46Z,0 +2025-04-03T14:32:16Z,0 +2025-04-03T14:32:46Z,0 +2025-04-03T14:33:16Z,0 +2025-04-03T14:33:46Z,0 +2025-04-03T14:34:16Z,0 +2025-04-03T14:34:46Z,0 +2025-04-03T14:35:16Z,0 +2025-04-03T14:35:46Z,0 +2025-04-03T14:36:16Z,0 +2025-04-03T14:36:46Z,0 +2025-04-03T14:37:16Z,0 +2025-04-03T14:37:46Z,0 +2025-04-03T14:38:16Z,0 +2025-04-03T14:38:46Z,0 +2025-04-03T14:39:16Z,0 +2025-04-03T14:39:46Z,0 +2025-04-03T14:40:16Z,0 +2025-04-03T14:40:46Z,0 +2025-04-03T14:41:16Z,0 +2025-04-03T14:41:46Z,0 +2025-04-03T14:42:16Z,0 +2025-04-03T14:42:46Z,0 +2025-04-03T14:43:16Z,0 +2025-04-03T14:43:46Z,0 +2025-04-03T14:44:16Z,0 +2025-04-03T14:44:46Z,0 +2025-04-03T14:45:16Z,0 +2025-04-03T14:45:46Z,0 diff --git a/example/server_HY_HYPIXEL_COM_CN_25565.csv b/example/server_HY_HYPIXEL_COM_CN_25565.csv new file mode 100644 index 0000000..8fefb14 --- /dev/null +++ b/example/server_HY_HYPIXEL_COM_CN_25565.csv @@ -0,0 +1,39 @@ +timestamp,online +2025-04-03T14:27:18Z,36 +2025-04-03T14:27:46Z,37 +2025-04-03T14:28:16Z,37 +2025-04-03T14:28:46Z,37 +2025-04-03T14:29:16Z,38 +2025-04-03T14:29:46Z,38 +2025-04-03T14:30:16Z,38 +2025-04-03T14:30:46Z,38 +2025-04-03T14:31:16Z,37 +2025-04-03T14:31:46Z,36 +2025-04-03T14:32:16Z,36 +2025-04-03T14:32:46Z,37 +2025-04-03T14:33:16Z,39 +2025-04-03T14:33:46Z,37 +2025-04-03T14:34:16Z,38 +2025-04-03T14:34:46Z,38 +2025-04-03T14:35:16Z,38 +2025-04-03T14:35:46Z,38 +2025-04-03T14:36:16Z,39 +2025-04-03T14:36:46Z,39 +2025-04-03T14:37:16Z,39 +2025-04-03T14:37:46Z,39 +2025-04-03T14:38:16Z,40 +2025-04-03T14:38:46Z,40 +2025-04-03T14:39:16Z,39 +2025-04-03T14:39:46Z,40 +2025-04-03T14:40:16Z,40 +2025-04-03T14:40:46Z,40 +2025-04-03T14:41:16Z,40 +2025-04-03T14:41:46Z,40 +2025-04-03T14:42:16Z,40 +2025-04-03T14:42:46Z,39 +2025-04-03T14:43:16Z,40 +2025-04-03T14:43:46Z,41 +2025-04-03T14:44:16Z,41 +2025-04-03T14:44:46Z,39 +2025-04-03T14:45:16Z,39 +2025-04-03T14:45:46Z,40 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1dbd3dd --- /dev/null +++ b/go.mod @@ -0,0 +1,6 @@ +module github.com/ssdomei232/go-mc-listener + +go 1.23.2 + + +require github.com/google/uuid v1.3.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a3a3534 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q= +github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/main.go b/main.go new file mode 100644 index 0000000..5e9960d --- /dev/null +++ b/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "time" + + "minecraftquery/minecraftquery" // 使用正确的模块路径或相对路径 +) + +func main() { + status, err := minecraftquery.Query("mc.example.com", 25565, 3*time.Second) + if err != nil { + fmt.Printf("查询失败: %v\n", err) // 输出具体错误信息以便调试 + return + } + fmt.Printf("实际连接地址: %s\n", status.ResolvedHost) + fmt.Printf("当前在线: %d/%d\n", status.Online, status.MaxPlayers) +} diff --git a/minecraftquery/minecraftquery.go b/minecraftquery/minecraftquery.go new file mode 100644 index 0000000..8c9acd5 --- /dev/null +++ b/minecraftquery/minecraftquery.go @@ -0,0 +1,307 @@ +package minecraftquery + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "strings" + "sync" + "time" +) + +// 连接池配置 +var ( + connPool sync.Map // 存储活跃连接 + poolMutex sync.Mutex // 连接池互斥锁 + connExpiry = 30 * time.Second // 连接最长保留时间 +) + +type ServerStatus struct { + Version string + Protocol int + Online int + MaxPlayers int + Description string + Latency time.Duration + ResolvedHost string // 实际连接的服务器地址 +} + +func Query(addr string, port int, timeout time.Duration) (*ServerStatus, error) { + start := time.Now() + + // 解析SRV记录 + resolvedAddr, resolvedPort, err := resolveSRV(addr) + if err == nil { + addr, port = resolvedAddr, resolvedPort + } + + key := fmt.Sprintf("%s:%d", addr, port) + + // 尝试获取连接 + conn, err := getConnection(key, timeout) + if err != nil { + return nil, err + } + defer returnConnection(key, conn) + + // 执行查询 + status, err := performQuery(conn, addr, port, start) + if err != nil { + conn.Close() + connPool.Delete(key) + return nil, err + } + + status.ResolvedHost = fmt.Sprintf("%s:%d", addr, port) + return status, nil +} + +// SRV记录解析 +func resolveSRV(domain string) (string, int, error) { + _, addrs, err := net.LookupSRV("minecraft", "tcp", domain) + if err != nil || len(addrs) == 0 { + return "", 0, errors.New("no SRV record found") + } + + // 选择优先级最高且权重最大的记录 + best := addrs[0] + for _, a := range addrs[1:] { + if a.Priority < best.Priority || + (a.Priority == best.Priority && a.Weight > best.Weight) { + best = a + } + } + + return strings.TrimSuffix(best.Target, "."), int(best.Port), nil +} + +// 连接管理 +func getConnection(key string, timeout time.Duration) (net.Conn, error) { + // 尝试获取现有连接 + if val, ok := connPool.Load(key); ok { + conn := val.(*pooledConn) + if time.Since(conn.lastUsed) < connExpiry && isConnAlive(conn.Conn) { + poolMutex.Lock() + conn.lastUsed = time.Now() + poolMutex.Unlock() + return conn.Conn, nil + } + conn.Close() + connPool.Delete(key) + } + + // 创建新连接 + conn, err := net.DialTimeout("tcp", key, timeout) + if err != nil { + return nil, fmt.Errorf("connection failed: %w", err) + } + + pooled := &pooledConn{ + Conn: conn, + lastUsed: time.Now(), + } + connPool.Store(key, pooled) + return conn, nil +} + +func returnConnection(key string, conn net.Conn) { + if val, ok := connPool.Load(key); ok { + pooled := val.(*pooledConn) + poolMutex.Lock() + pooled.lastUsed = time.Now() + poolMutex.Unlock() + } +} + +type pooledConn struct { + net.Conn + lastUsed time.Time +} + +func isConnAlive(conn net.Conn) bool { + // 发送1字节的无效数据测试连接 + conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + defer conn.SetReadDeadline(time.Time{}) + + _, err := conn.Write([]byte{0x00}) + if err != nil { + return false + } + + buf := make([]byte, 1) + _, err = conn.Read(buf) + return err == nil || errors.Is(err, io.EOF) +} + +// 查询实现 +func performQuery(conn net.Conn, addr string, port int, start time.Time) (*ServerStatus, error) { + conn.SetDeadline(time.Now().Add(5 * time.Second)) + + if err := sendHandshake(conn, addr, port); err != nil { + return nil, fmt.Errorf("handshake failed: %w", err) + } + + if err := sendStatusRequest(conn); err != nil { + return nil, fmt.Errorf("status request failed: %w", err) + } + + jsonData, err := readStatusResponse(conn) + if err != nil { + return nil, fmt.Errorf("response parse failed: %w", err) + } + + var response struct { + Version struct { + Name string `json:"name"` + Protocol int `json:"protocol"` + } `json:"version"` + Players struct { + Online int `json:"online"` + Max int `json:"max"` + } `json:"players"` + Description json.RawMessage `json:"description"` + } + if err := json.Unmarshal(jsonData, &response); err != nil { + return nil, fmt.Errorf("json parse error: %w", err) + } + + return &ServerStatus{ + Version: response.Version.Name, + Protocol: response.Version.Protocol, + Online: response.Players.Online, + MaxPlayers: response.Players.Max, + Description: parseDescription(response.Description), + Latency: time.Since(start), + }, nil +} + +// 以下为内部实现细节 +func writeVarInt(w io.Writer, value int) error { + for { + if (value & ^0x7F) == 0 { + return binary.Write(w, binary.BigEndian, byte(value)) + } + binary.Write(w, binary.BigEndian, byte((value&0x7F)|0x80)) + value = int(uint32(value) >> 7) + } +} + +func readVarInt(r io.Reader) (int, error) { + var num int + var shift uint + for { + b := make([]byte, 1) + _, err := r.Read(b) + if err != nil { + return 0, err + } + num |= int(b[0]&0x7F) << shift + shift += 7 + if b[0]&0x80 == 0 { + break + } + } + return num, nil +} + +func writeString(w io.Writer, s string) error { + if err := writeVarInt(w, len(s)); err != nil { + return err + } + _, err := w.Write([]byte(s)) + return err +} + +func sendHandshake(conn net.Conn, addr string, port int) error { + handshake := new(bytes.Buffer) + writeVarInt(handshake, 0) // 包ID + writeVarInt(handshake, -1) // 协议版本 + writeString(handshake, addr) // 服务器地址 + binary.Write(handshake, binary.BigEndian, uint16(port)) + writeVarInt(handshake, 1) // 下一个状态 (Status) + + packet := new(bytes.Buffer) + writeVarInt(packet, handshake.Len()) + packet.Write(handshake.Bytes()) + _, err := conn.Write(packet.Bytes()) + return err +} + +func sendStatusRequest(conn net.Conn) error { + request := new(bytes.Buffer) + writeVarInt(request, 0) // 请求包ID + + packet := new(bytes.Buffer) + writeVarInt(packet, request.Len()) + packet.Write(request.Bytes()) + _, err := conn.Write(packet.Bytes()) + return err +} + +func readStatusResponse(conn net.Conn) ([]byte, error) { + length, err := readVarInt(conn) + if err != nil { + return nil, err + } + + data := make([]byte, length) + _, err = io.ReadFull(conn, data) + if err != nil { + return nil, err + } + + r := bytes.NewReader(data) + packetID, err := readVarInt(r) + if err != nil || packetID != 0 { + return nil, fmt.Errorf("invalid packet ID: %d", packetID) + } + + jsonStr, err := readString(r) + if err != nil { + return nil, err + } + + return []byte(jsonStr), nil +} + +// 从输入流中读取一个字符串 +func readString(r io.Reader) (string, error) { + length, err := readVarInt(r) + if err != nil { + return "", err + } + + if length < 0 { + return "", fmt.Errorf("invalid string length: %d", length) + } + + data := make([]byte, length) + _, err = io.ReadFull(r, data) + if err != nil { + return "", err + } + + return string(data), nil +} + +func parseDescription(raw json.RawMessage) string { + // 尝试解析为text component + var obj struct { + Text string `json:"text"` + } + if json.Unmarshal(raw, &obj) == nil { + return obj.Text + } + + // 直接返回原始字符串 + var str string + if json.Unmarshal(raw, &str) == nil { + return str + } + + return string(raw) +}