diff --git a/src/main/java/pw/yumc/YumCore/mc/MinecraftTools.java b/src/main/java/pw/yumc/YumCore/mc/MinecraftTools.java index a3c23cf..64b5820 100644 --- a/src/main/java/pw/yumc/YumCore/mc/MinecraftTools.java +++ b/src/main/java/pw/yumc/YumCore/mc/MinecraftTools.java @@ -4,7 +4,7 @@ import pw.yumc.YumCore.annotation.NotProguard; /** * Minecraft工具类 - * + * * @author 喵♂呜 * @since 2017/1/26 0026 */ @@ -12,9 +12,9 @@ import pw.yumc.YumCore.annotation.NotProguard; public class MinecraftTools { /** * 获得服务器信息 - * + * * @param address - * 服务器地址 + * 服务器地址 * @return {@link ServerInfo} 服务器信息 */ public static ServerInfo getServerInfo(String address) { @@ -25,4 +25,20 @@ public class MinecraftTools { return new ServerInfo(address); } } + + /** + * 获得服务器信息 + * + * @param address + * 服务器地址 + * @return {@link ServerInfo} 服务器信息 + */ + public static ServerPing getServerPing(String address) { + if (address.contains(":")) { + String[] args = address.split(":"); + return new ServerPing(args[0], Integer.parseInt(args[1])); + } else { + return new ServerPing(address); + } + } } diff --git a/src/main/java/pw/yumc/YumCore/mc/ServerInfo.java b/src/main/java/pw/yumc/YumCore/mc/ServerInfo.java index 1c3a3e5..f7fa04a 100644 --- a/src/main/java/pw/yumc/YumCore/mc/ServerInfo.java +++ b/src/main/java/pw/yumc/YumCore/mc/ServerInfo.java @@ -1,14 +1,14 @@ package pw.yumc.YumCore.mc; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; -import java.util.regex.Pattern; +import java.nio.charset.Charset; -import lombok.Data; import pw.yumc.YumCore.annotation.NotProguard; /** @@ -17,37 +17,8 @@ import pw.yumc.YumCore.annotation.NotProguard; * @author 喵♂呜 * @since 2017/1/26 0026 */ -@Data @NotProguard public class ServerInfo { - private static byte PACKET_HANDSHAKE = 0x00, PACKET_STATUSREQUEST = 0x00, PACKET_PING = 0x01; - private static int PROTOCOL_VERSION = 4; - private static int STATUS_HANDSHAKE = 1; - private static Pattern pattern = Pattern.compile(".*\"description\":\"(.*)\".*"); - /** - * { - * "version": {"name": "BungeeCord 1.8.x, 1.9.x, 1.10.x, 1.11.x", "protocol": 316}, - * "players": {"max": 922, "online": 921}, - * "description": { - * "extra": [ - * {"color": "white", "text": " "}, - * {"color": "aqua", "bold": true, "text": "梦世界 "}, - * {"color": "red","bold": true, "obfuscated": true, "text": "|"}, - * {"color": "white", "text": " "}, - * {"color": "white", "bold": true, "text": "i5mc.com\n"}, - * {"color": "white", "text": " " }, - * {"color": "yellow", "bold": true, "text": "★"}, - * {"color": "red", "bold": true, "text": "1.8-1.11"}, - * {"color": "yellow", "bold": true, "text": "★ ★"}, - * {"color": "yellow", "text": "黄金周末"}, - * {"color": "gray", "text": "-"}, - * {"color": "green", "text": "空岛战争"}, - * {"color": "red", "bold": true, "text": "双倍硬币"}, - * {"color": "green", "text": "奖励"}, - * {"color": "yellow", "bold": true, "text": "★"} - * ],"text": "" } - * } - */ private String address = "localhost"; private int port = 25565; private int timeout = 1500; @@ -88,56 +59,63 @@ public class ServerInfo { */ public boolean fetchData() { try (Socket socket = new Socket()) { - socket.connect(new InetSocketAddress(getAddress(), getPort()), getTimeout()); + OutputStream outputStream; + DataOutputStream dataOutputStream; + InputStream inputStream; + InputStreamReader inputStreamReader; - final DataInputStream in = new DataInputStream(socket.getInputStream()); - final DataOutputStream out = new DataOutputStream(socket.getOutputStream()); + socket.setSoTimeout(this.timeout); - //> Handshake - ByteArrayOutputStream handshake_bytes = new ByteArrayOutputStream(); - DataOutputStream handshake = new DataOutputStream(handshake_bytes); + socket.connect(new InetSocketAddress(this.getAddress(), this.getPort()), this.getTimeout()); - handshake.writeByte(PACKET_HANDSHAKE); - writeVarInt(handshake, PROTOCOL_VERSION); - writeVarInt(handshake, getAddress().length()); - handshake.writeBytes(getAddress()); - handshake.writeShort(getPort()); - writeVarInt(handshake, STATUS_HANDSHAKE); + outputStream = socket.getOutputStream(); + dataOutputStream = new DataOutputStream(outputStream); - writeVarInt(out, handshake_bytes.size()); - out.write(handshake_bytes.toByteArray()); + inputStream = socket.getInputStream(); + inputStreamReader = new InputStreamReader(inputStream, Charset.forName("UTF-16BE")); - //> Status request + dataOutputStream.write(new byte[]{(byte) 0xFE, (byte) 0x01}); - out.writeByte(0x01); // Size of packet - out.writeByte(PACKET_STATUSREQUEST); + int packetId = inputStream.read(); - //< Status response + if (packetId == -1) { throw new IOException("Premature end of stream."); } - readVarInt(in); // Size - int id = readVarInt(in); + if (packetId != 0xFF) { throw new IOException("Invalid packet ID (" + packetId + ")."); } - int length = readVarInt(in); + int length = inputStreamReader.read(); - byte[] data = new byte[length]; - in.readFully(data); - String json = new String(data, "UTF-8"); - System.out.println(json); - // //> Ping - // - // out.writeByte(0x09); // Size of packet - // out.writeByte(PACKET_PING); - // out.writeLong(System.currentTimeMillis()); - // - // //< Ping - // readVarInt(in); // Size - // id = readVarInt(in); + if (length == -1) { throw new IOException("Premature end of stream."); } - // Close - handshake.close(); - handshake_bytes.close(); - out.close(); - in.close(); + if (length == 0) { throw new IOException("Invalid string length."); } + + char[] chars = new char[length]; + + if (inputStreamReader.read(chars, 0, length) != length) { throw new IOException("Premature end of stream."); } + + String string = new String(chars); + + if (string.startsWith("§")) { + String[] data = string.split("\0"); + this.pingVersion = Integer.parseInt(data[0].substring(1)); + this.protocolVersion = Integer.parseInt(data[1]); + this.gameVersion = data[2]; + this.motd = data[3]; + this.playersOnline = Integer.parseInt(data[4]); + this.maxPlayers = Integer.parseInt(data[5]); + } else { + String[] data = string.split("§"); + this.motd = data[0]; + this.playersOnline = Integer.parseInt(data[1]); + this.maxPlayers = Integer.parseInt(data[2]); + } + + dataOutputStream.close(); + outputStream.close(); + + inputStreamReader.close(); + inputStream.close(); + + socket.close(); } catch (IOException exception) { gameVersion = "获取失败!"; motd = "获取失败!"; @@ -146,36 +124,44 @@ public class ServerInfo { return true; } - /** - * @author thinkofdeath - * See: https://gist.github.com/thinkofdeath/e975ddee04e9c87faf22 - */ - public int readVarInt(DataInputStream in) throws IOException { - int i = 0; - int j = 0; - while (true) { - int k = in.readByte(); - i |= (k & 0x7F) << j++ * 7; - if (j > 5) - throw new RuntimeException("VarInt too big"); - if ((k & 0x80) != 128) - break; - } - return i; + public String getAddress() { + return this.address; } - /** - * @author thinkofdeath - * See: https://gist.github.com/thinkofdeath/e975ddee04e9c87faf22 - */ - public void writeVarInt(DataOutputStream out, int paramInt) throws IOException { - while (true) { - if ((paramInt & 0xFFFFFF80) == 0) { - out.writeByte(paramInt); - return; - } - out.writeByte(paramInt & 0x7F | 0x80); - paramInt >>>= 7; - } + public int getPort() { + return this.port; } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public int getTimeout() { + return this.timeout; + } + + public int getPingVersion() { + return this.pingVersion; + } + + public int getProtocolVersion() { + return this.protocolVersion; + } + + public String getGameVersion() { + return this.gameVersion; + } + + public String getMotd() { + return this.motd; + } + + public int getPlayersOnline() { + return this.playersOnline; + } + + public int getMaxPlayers() { + return this.maxPlayers; + } + } diff --git a/src/main/java/pw/yumc/YumCore/mc/ServerPing.java b/src/main/java/pw/yumc/YumCore/mc/ServerPing.java new file mode 100644 index 0000000..a8744ed --- /dev/null +++ b/src/main/java/pw/yumc/YumCore/mc/ServerPing.java @@ -0,0 +1,223 @@ +package pw.yumc.YumCore.mc; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; + +import javax.script.ScriptException; + +import jdk.nashorn.api.scripting.ScriptObjectMirror; +import lombok.Data; +import lombok.SneakyThrows; +import pw.yumc.YumCore.annotation.NotProguard; +import pw.yumc.YumCore.bukkit.Log; +import pw.yumc.YumCore.engine.MiaoScriptEngine; + +/** + * Minecraft服务器数据获取类 + * + * @author 喵♂呜 + * @since 2017/1/26 0026 + */ +@Data +@NotProguard +public class ServerPing { + private static byte PACKET_HANDSHAKE = 0x00, PACKET_STATUSREQUEST = 0x00, PACKET_PING = 0x01; + private static int PROTOCOL_VERSION = 4; + private static int STATUS_HANDSHAKE = 1; + private static MiaoScriptEngine engine = new MiaoScriptEngine(); + + static { + try { + engine.eval("function parse(json) {\n" + + " var color = [];\n" + + " color['black'] = '0';\n" + + " color['dark_blue'] = '1';\n" + + " color['dark_green'] = '2';\n" + + " color['dark_aqua'] = '3';\n" + + " color['dark_red'] = '4';\n" + + " color['dark_purple'] = '5';\n" + + " color['gold'] = '6';\n" + + " color['gray'] = '7';\n" + + " color['dark_gray'] = '8';\n" + + " color['blue'] = '9';\n" + + " color['green'] = 'a';\n" + + " color['aqua'] = 'b';\n" + + " color['red'] = 'c';\n" + + " color['light_purple'] = 'd';\n" + + " color['yellow'] = 'e';\n" + + " color['white'] = 'f';\n" + + " var obj = JSON.parse(json);\n" + + " // noinspection JSUnresolvedVariable\n" + + " var motd = obj.description.text;\n" + + " // noinspection JSUnresolvedVariable\n" + + " if (obj.description.extra) {\n" + + " // noinspection JSUnresolvedVariable\n" + + " obj.description.extra.forEach(function (part) {\n" + + " // noinspection JSUnresolvedVariable\n" + + " if (part.obfuscated) {\n" + + " motd += \"§k\";\n" + + " }\n" + + " if (part.bold) {\n" + + " motd += \"§l\";\n" + + " }\n" + + " // noinspection JSUnresolvedVariable\n" + + " if (part.strikethrough) {\n" + + " motd += \"§m\";\n" + + " }\n" + + " // noinspection JSUnresolvedVariable\n" + + " if (part.underline) {\n" + + " motd += \"§n\";\n" + + " }\n" + + " // noinspection JSUnresolvedVariable\n" + + " if (part.italic) {\n" + + " motd += \"§o\";\n" + + " }\n" + + " if (part.reset) {\n" + + " motd += \"§r\";\n" + + " }\n" + + " if (part.color) {\n" + + " motd += '§' + color[part.color];\n" + + " }\n" + + " motd += part.text;\n" + + " })\n" + + " }\n" + + " obj.version_name = obj.version.name;\n" + + " obj.version_protocol = obj.version.protocol;\n" + + " obj.players_max = obj.players.max;\n" + + " obj.players_online = obj.players.online;\n" + + " obj.description = motd;\n" + + " return obj;\n" + + "}"); + } catch (ScriptException e) { + Log.w("警告! MOTD 解析脚本初始化失败!"); + } + } + + private String address = "localhost"; + private int port = 25565; + private int timeout = 1500; + + private String versionName = "初始化中..."; + private int versionProtocol = -1; + private int playersOnline = -1; + private int playersMax = -1; + private String motd = "初始化中..."; + + /** + * Minecraft服务器数据获取类 + * + * @param address + * 服务器地址 默认端口25565 + */ + public ServerPing(String address) { + this(address, 25565); + } + + /** + * Minecraft服务器数据获取类 + * + * @param address + * 服务器地址 + * @param port + * 服务器端口 + */ + public ServerPing(String address, int port) { + this.address = address; + this.port = port; + } + + /** + * 获取服务器数据 + * + * @return 是否获取成功 + */ + @SneakyThrows + public boolean fetchData() { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(getAddress(), getPort()), getTimeout()); + + final DataInputStream in = new DataInputStream(socket.getInputStream()); + final DataOutputStream out = new DataOutputStream(socket.getOutputStream()); + + //> Handshake + ByteArrayOutputStream handshake_bytes = new ByteArrayOutputStream(); + DataOutputStream handshake = new DataOutputStream(handshake_bytes); + + handshake.writeByte(PACKET_HANDSHAKE); + writeVarInt(handshake, PROTOCOL_VERSION); + writeVarInt(handshake, getAddress().length()); + handshake.writeBytes(getAddress()); + handshake.writeShort(getPort()); + writeVarInt(handshake, STATUS_HANDSHAKE); + + writeVarInt(out, handshake_bytes.size()); + out.write(handshake_bytes.toByteArray()); + + //> Status request + out.writeByte(0x01); // Size of packet + out.writeByte(PACKET_STATUSREQUEST); + + //< Status response + readVarInt(in); // Size + int id = readVarInt(in); + int length = readVarInt(in); + byte[] data = new byte[length]; + in.readFully(data); + String json = new String(data, "UTF-8"); + ScriptObjectMirror mirror = (ScriptObjectMirror) engine.invokeFunction("parse", json); + versionName = (String) mirror.get("version_name"); + versionProtocol = (int) mirror.get("version_protocol"); + playersMax = (int) mirror.get("players_max"); + playersOnline = (int) mirror.get("players_online"); + motd = (String) mirror.get("description"); + + // Close + handshake.close(); + handshake_bytes.close(); + out.close(); + in.close(); + } catch (IOException exception) { + versionName = "§c获取失败!"; + motd = "§c获取失败!"; + return false; + } + return true; + } + + /** + * @author thinkofdeath + * See: https://gist.github.com/thinkofdeath/e975ddee04e9c87faf22 + */ + public int readVarInt(DataInputStream in) throws IOException { + int i = 0; + int j = 0; + while (true) { + int k = in.readByte(); + i |= (k & 0x7F) << j++ * 7; + if (j > 5) + throw new RuntimeException("VarInt too big"); + if ((k & 0x80) != 128) + break; + } + return i; + } + + /** + * @author thinkofdeath + * See: https://gist.github.com/thinkofdeath/e975ddee04e9c87faf22 + */ + public void writeVarInt(DataOutputStream out, int paramInt) throws IOException { + while (true) { + if ((paramInt & 0xFFFFFF80) == 0) { + out.writeByte(paramInt); + return; + } + out.writeByte(paramInt & 0x7F | 0x80); + paramInt >>>= 7; + } + } +} diff --git a/src/test/java/pw/yumc/YumCore/mc/ServerPingTest.java b/src/test/java/pw/yumc/YumCore/mc/ServerPingTest.java new file mode 100644 index 0000000..be26d34 --- /dev/null +++ b/src/test/java/pw/yumc/YumCore/mc/ServerPingTest.java @@ -0,0 +1,22 @@ +package pw.yumc.YumCore.mc; + +import org.junit.Test; + +import lombok.val; +import pw.yumc.YumCore.bukkit.Log; + +/** + * Created with IntelliJ IDEA + * + * @author 喵♂呜 + * Created on 2017/9/29 13:05. + */ +public class ServerPingTest { + @Test + public void test() { + val ping = new ServerPing("server.yumc.pw"); + ping.fetchData(); + Log.i("version: %s max: %s online: %s motd: %s", + ping.getVersionName(), ping.getPlayersMax(), ping.getPlayersOnline(), ping.getMotd()); + } +} \ No newline at end of file