302 lines
6.3 KiB
Go
302 lines
6.3 KiB
Go
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))
|
|
}
|