first commit
This commit is contained in:
commit
b556ca78f9
61
example/index.html
Normal file
61
example/index.html
Normal file
@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>MC服务器监控</title>
|
||||
<link href="https://cdn.jsdmirror.com/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background: linear-gradient(45deg, #1a1a1a, #2a2a2a); color: #fff; }
|
||||
.card { background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); }
|
||||
.chart-container { background: rgba(0,0,0,0.3); border-radius: 15px; padding: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-5">
|
||||
<h1 class="text-center mb-4"><i class="fas fa-server"></i> MC服务器监控</h1>
|
||||
<div id="status" class="row"></div>
|
||||
<div class="chart-container mt-5">
|
||||
<canvas id="chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdmirror.com/npm/chart.js"></script>
|
||||
<script>
|
||||
let chart;
|
||||
async function update() {
|
||||
const res = await fetch('/data');
|
||||
const data = await res.json();
|
||||
|
||||
// 更新状态卡片
|
||||
document.getElementById('status').innerHTML = data.map(s => `
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card p-3">
|
||||
<h3>${s.addr}:${s.port}</h3>
|
||||
<p class="mb-1">当前在线: <span class="badge bg-success">${s.current}</span></p>
|
||||
<small>最后更新: ${new Date(s.updated).toLocaleString()}</small>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// 更新图表
|
||||
const ctx = document.getElementById('chart').getContext('2d');
|
||||
if(chart) chart.destroy();
|
||||
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data[0].history.map(d => new Date(d.timestamp).toLocaleString()),
|
||||
datasets: data.map((s, i) => ({
|
||||
label: `${s.addr}:${s.port}`,
|
||||
data: s.history.map(d => d.online),
|
||||
borderColor: `hsl(${i * 90}, 75%, 50%)`,
|
||||
tension: 0.2
|
||||
}))
|
||||
}
|
||||
});
|
||||
}
|
||||
setInterval(update, 60000);
|
||||
update();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
301
example/main.go
Normal file
301
example/main.go
Normal file
@ -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))
|
||||
}
|
47
example/server_192_168_1_214_25565.csv
Normal file
47
example/server_192_168_1_214_25565.csv
Normal file
@ -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
|
|
39
example/server_HY_HYPIXEL_COM_CN_25565.csv
Normal file
39
example/server_HY_HYPIXEL_COM_CN_25565.csv
Normal file
@ -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
|
|
6
go.mod
Normal file
6
go.mod
Normal file
@ -0,0 +1,6 @@
|
||||
module github.com/ssdomei232/go-mc-listener
|
||||
|
||||
go 1.23.2
|
||||
|
||||
|
||||
require github.com/google/uuid v1.3.0 // indirect
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -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=
|
18
main.go
Normal file
18
main.go
Normal file
@ -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)
|
||||
}
|
307
minecraftquery/minecraftquery.go
Normal file
307
minecraftquery/minecraftquery.go
Normal file
@ -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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user