generated from mingliqiye/lib-tem
All checks were successful
Gitea Actions Build / Build (push) Successful in 59s
220 lines
7.4 KiB
Java
220 lines
7.4 KiB
Java
/*
|
||
* Copyright 2025 mingliqiye
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*
|
||
* ProjectName mingli-utils
|
||
* ModuleName mingli-utils.main
|
||
* CurrentFile SLP.java
|
||
* LastUpdate 2025-09-09 08:37:33
|
||
* UpdateUser MingLiPro
|
||
*/
|
||
|
||
package com.mingliqiye.utils.minecraft.slp;
|
||
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import com.mingliqiye.utils.network.NetworkEndpoint;
|
||
import java.io.ByteArrayOutputStream;
|
||
import java.io.DataInputStream;
|
||
import java.io.IOException;
|
||
import java.io.OutputStream;
|
||
import java.net.Socket;
|
||
import java.nio.ByteBuffer;
|
||
import java.nio.ByteOrder;
|
||
|
||
/**
|
||
* Minecraft 服务器列表协议(Server List Ping, SLP)工具类。
|
||
* 提供了与 Minecraft 服务器通信以获取其状态信息的功能。
|
||
*/
|
||
public class SLP {
|
||
|
||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||
|
||
/**
|
||
* 将 int32 值截断为无符号 short(2 字节)并按大端序写入字节数组。
|
||
*
|
||
* @param value 需要转换的整数(int32)
|
||
* @return 包含两个字节的数组,表示无符号 short
|
||
*/
|
||
public static byte[] toUnsignedShort(int value) {
|
||
byte[] array = new byte[2];
|
||
ByteBuffer.wrap(array, 0, 2)
|
||
.order(ByteOrder.BIG_ENDIAN)
|
||
.putShort((short) (value & 0xFFFF));
|
||
return array;
|
||
}
|
||
|
||
/**
|
||
* 构造 Minecraft 握手包数据。
|
||
* 握手包用于初始化客户端与服务器之间的连接。
|
||
*
|
||
* @param serverIP 服务器 IP 地址或域名
|
||
* @param serverPort 服务器端口号
|
||
* @param type 连接类型(通常为 1 表示获取状态)
|
||
* @return 握手包的完整字节数组
|
||
* @throws IOException 如果构造过程中发生 IO 错误
|
||
*/
|
||
public static byte[] getHandshakePack(
|
||
String serverIP,
|
||
int serverPort,
|
||
int type
|
||
) throws IOException {
|
||
ByteArrayOutputStream pack = new ByteArrayOutputStream();
|
||
ByteArrayOutputStream byteArrayOutputStream =
|
||
new ByteArrayOutputStream();
|
||
pack.write(0x00); // 握手包标识符
|
||
pack.write(toVarInt(1156)); // 协议版本号(示例值)
|
||
byte[] sip = serverIP.getBytes();
|
||
pack.write(toVarInt(sip.length)); // 服务器地址长度
|
||
pack.write(sip); // 服务器地址
|
||
pack.write(toUnsignedShort(serverPort)); // 服务器端口
|
||
pack.write(toVarInt(type)); // 下一阶段类型(1 表示状态请求)
|
||
byteArrayOutputStream.write(toVarInt(pack.size())); // 包长度前缀
|
||
byteArrayOutputStream.write(pack.toByteArray());
|
||
|
||
return byteArrayOutputStream.toByteArray();
|
||
}
|
||
|
||
/**
|
||
* 获取状态请求包的固定字节表示。
|
||
* 此包用于向服务器请求当前状态信息。
|
||
*
|
||
* @return 状态请求包的字节数组
|
||
*/
|
||
public static byte[] getStatusPack() {
|
||
return new byte[] { 0x01, 0x00 };
|
||
}
|
||
|
||
/**
|
||
* 从输入流中读取服务器返回的状态 JSON 数据,并解析为 MinecraftServerStatus 实体对象。
|
||
*
|
||
* @param inputStream 输入流,包含服务器响应的数据
|
||
* @return 解析后的 MinecraftServerStatus 对象
|
||
* @throws IOException 如果读取过程中发生 IO 错误
|
||
*/
|
||
public static MinecraftServerStatus getStatusJsonEntity(
|
||
DataInputStream inputStream
|
||
) throws IOException {
|
||
readVarInt(inputStream); // 忽略第一个 VarInt(包长度)
|
||
inputStream.readByte(); // 忽略包标识符
|
||
int lengthjson = readVarInt(inputStream); // 读取 JSON 数据长度
|
||
byte[] data = new byte[lengthjson];
|
||
inputStream.readFully(data); // 读取完整的 JSON 数据
|
||
MinecraftServerStatus serverStatus = objectMapper.readValue(
|
||
data,
|
||
MinecraftServerStatus.class
|
||
);
|
||
serverStatus.setJsonData(new String(data)); // 设置原始 JSON 字符串
|
||
return serverStatus;
|
||
}
|
||
|
||
/**
|
||
* 从输入流中读取一个 VarInt 类型的整数(最多 5 个字节)。
|
||
*
|
||
* @param in 输入流
|
||
* @return 解码后的整数值
|
||
* @throws IOException 如果读取过程中发生 IO 错误
|
||
*/
|
||
public static int readVarInt(DataInputStream in) throws IOException {
|
||
int value = 0;
|
||
int length = 0;
|
||
byte currentByte;
|
||
do {
|
||
currentByte = in.readByte();
|
||
value |= (currentByte & 0x7F) << (length * 7);
|
||
length += 1;
|
||
if (length > 5) {
|
||
throw new RuntimeException("VarInt too long");
|
||
}
|
||
} while ((currentByte & 0x80) != 0);
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
* 将一个 int32 整数编码为 VarInt 格式的字节数组(1 到 5 个字节)。
|
||
*
|
||
* @param value 需要编码的整数
|
||
* @return 编码后的 VarInt 字节数组
|
||
*/
|
||
public static byte[] toVarInt(int value) {
|
||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||
while (true) {
|
||
if ((value & 0xFFFFFF80) == 0) {
|
||
buffer.write(value); // 最后一个字节
|
||
break;
|
||
}
|
||
buffer.write((value & 0x7F) | 0x80); // 写入带继续位的字节
|
||
value >>>= 7; // 右移 7 位继续处理
|
||
}
|
||
return buffer.toByteArray();
|
||
}
|
||
|
||
/**
|
||
* 创建一个新的 Socket 连接到指定的网络端点,并设置超时时间。
|
||
*
|
||
* @param networkEndpoint 目标网络端点(包括主机和端口)
|
||
* @return 已连接的 Socket 实例
|
||
* @throws IOException 如果连接失败或发生 IO 错误
|
||
*/
|
||
public static Socket getNewConnect(NetworkEndpoint networkEndpoint)
|
||
throws IOException {
|
||
Socket socket = new Socket();
|
||
socket.setSoTimeout(5000); // 设置读取超时时间为 5 秒
|
||
socket.connect(networkEndpoint.toInetSocketAddress()); // 执行连接操作
|
||
return socket;
|
||
}
|
||
|
||
/**
|
||
* 使用 "host:port" 格式的字符串连接到 Minecraft 服务器并获取其状态信息。
|
||
*
|
||
* @param s 域名或 IP 地址加端口号组成的字符串,例如 "127.0.0.1:25565"
|
||
* @return 服务器状态实体对象
|
||
* @throws IOException 如果连接失败或发生 IO 错误
|
||
*/
|
||
public static MinecraftServerStatus getServerStatus(String s)
|
||
throws IOException {
|
||
return getServerStatus(NetworkEndpoint.of(s));
|
||
}
|
||
|
||
/**
|
||
* 使用指定的主机名和端口号连接到 Minecraft 服务器并获取其状态信息。
|
||
*
|
||
* @param s 主机名或 IP 地址
|
||
* @param i 端口号
|
||
* @return 服务器状态实体对象
|
||
* @throws IOException 如果连接失败或发生 IO 错误
|
||
*/
|
||
public static MinecraftServerStatus getServerStatus(String s, Integer i)
|
||
throws IOException {
|
||
return getServerStatus(NetworkEndpoint.of(s, i));
|
||
}
|
||
|
||
/**
|
||
* 使用 NetworkEndpoint 实例连接到 Minecraft 服务器并获取其状态信息。
|
||
*
|
||
* @param e 网络端点实例,包含主机和端口信息
|
||
* @return 服务器状态实体对象
|
||
* @throws IOException 如果连接失败或发生 IO 错误
|
||
* @see NetworkEndpoint
|
||
*/
|
||
public static MinecraftServerStatus getServerStatus(NetworkEndpoint e)
|
||
throws IOException {
|
||
Socket socket = getNewConnect(e); // 建立 TCP 连接
|
||
OutputStream out = socket.getOutputStream(); // 获取输出流发送数据
|
||
DataInputStream in = new DataInputStream(socket.getInputStream()); // 获取输入流接收数据
|
||
out.write(getHandshakePack(e.getHost(), e.getPort(), 1)); // 发送握手包
|
||
out.write(getStatusPack()); // 发送状态请求包
|
||
return getStatusJsonEntity(in); // 读取并解析服务器响应
|
||
}
|
||
}
|