308 lines
6.7 KiB
Go
308 lines
6.7 KiB
Go
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)
|
|
}
|