first commit

This commit is contained in:
mei 2025-04-05 10:23:28 +08:00
commit b556ca78f9
8 changed files with 783 additions and 0 deletions

61
example/index.html Normal file
View 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
View 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))
}

View 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
1 timestamp online
2 2025-04-03T14:05:06Z 0
3 2025-04-03T14:08:06Z 1
4 2025-04-03T14:10:06Z 0
5 2025-04-03T14:22:19Z 0
6 2025-04-03T14:23:19Z 0
7 2025-04-03T14:24:19Z 0
8 2025-04-03T14:25:19Z 0
9 2025-04-03T14:26:19Z 0
10 2025-04-03T14:27:16Z 0
11 2025-04-03T14:27:46Z 0
12 2025-04-03T14:28:16Z 0
13 2025-04-03T14:28:46Z 0
14 2025-04-03T14:29:16Z 0
15 2025-04-03T14:29:46Z 0
16 2025-04-03T14:30:16Z 0
17 2025-04-03T14:30:46Z 0
18 2025-04-03T14:31:16Z 0
19 2025-04-03T14:31:46Z 0
20 2025-04-03T14:32:16Z 0
21 2025-04-03T14:32:46Z 0
22 2025-04-03T14:33:16Z 0
23 2025-04-03T14:33:46Z 0
24 2025-04-03T14:34:16Z 0
25 2025-04-03T14:34:46Z 0
26 2025-04-03T14:35:16Z 0
27 2025-04-03T14:35:46Z 0
28 2025-04-03T14:36:16Z 0
29 2025-04-03T14:36:46Z 0
30 2025-04-03T14:37:16Z 0
31 2025-04-03T14:37:46Z 0
32 2025-04-03T14:38:16Z 0
33 2025-04-03T14:38:46Z 0
34 2025-04-03T14:39:16Z 0
35 2025-04-03T14:39:46Z 0
36 2025-04-03T14:40:16Z 0
37 2025-04-03T14:40:46Z 0
38 2025-04-03T14:41:16Z 0
39 2025-04-03T14:41:46Z 0
40 2025-04-03T14:42:16Z 0
41 2025-04-03T14:42:46Z 0
42 2025-04-03T14:43:16Z 0
43 2025-04-03T14:43:46Z 0
44 2025-04-03T14:44:16Z 0
45 2025-04-03T14:44:46Z 0
46 2025-04-03T14:45:16Z 0
47 2025-04-03T14:45:46Z 0

View 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
1 timestamp online
2 2025-04-03T14:27:18Z 36
3 2025-04-03T14:27:46Z 37
4 2025-04-03T14:28:16Z 37
5 2025-04-03T14:28:46Z 37
6 2025-04-03T14:29:16Z 38
7 2025-04-03T14:29:46Z 38
8 2025-04-03T14:30:16Z 38
9 2025-04-03T14:30:46Z 38
10 2025-04-03T14:31:16Z 37
11 2025-04-03T14:31:46Z 36
12 2025-04-03T14:32:16Z 36
13 2025-04-03T14:32:46Z 37
14 2025-04-03T14:33:16Z 39
15 2025-04-03T14:33:46Z 37
16 2025-04-03T14:34:16Z 38
17 2025-04-03T14:34:46Z 38
18 2025-04-03T14:35:16Z 38
19 2025-04-03T14:35:46Z 38
20 2025-04-03T14:36:16Z 39
21 2025-04-03T14:36:46Z 39
22 2025-04-03T14:37:16Z 39
23 2025-04-03T14:37:46Z 39
24 2025-04-03T14:38:16Z 40
25 2025-04-03T14:38:46Z 40
26 2025-04-03T14:39:16Z 39
27 2025-04-03T14:39:46Z 40
28 2025-04-03T14:40:16Z 40
29 2025-04-03T14:40:46Z 40
30 2025-04-03T14:41:16Z 40
31 2025-04-03T14:41:46Z 40
32 2025-04-03T14:42:16Z 40
33 2025-04-03T14:42:46Z 39
34 2025-04-03T14:43:16Z 40
35 2025-04-03T14:43:46Z 41
36 2025-04-03T14:44:16Z 41
37 2025-04-03T14:44:46Z 39
38 2025-04-03T14:45:16Z 39
39 2025-04-03T14:45:46Z 40

6
go.mod Normal file
View 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
View 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
View 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)
}

View 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)
}