diff --git a/build.gradle.kts b/build.gradle.kts index f5de2c4..0db2629 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,6 +38,8 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") implementation("org.jetbrains:annotations:24.0.0") annotationProcessor("org.jetbrains:annotations:24.0.0") + implementation("com.alibaba.fastjson2:fastjson2:2.0.57") + implementation("com.mingliqiye:network-endpoint:1.0.3") } tasks.test { diff --git a/gradle.properties b/gradle.properties index b5598b5..16a913b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUPSID=com.mingliqiye -ARTIFACTID=socket-utilts +ARTIFACTID=minecraft-SLP VERSIONS=1.0.1 -MAINCLASS=com.mingliqiye.Main +MAINCLASS=com.mingliqiye.minecraftSLP.Main JDKVERSIONS=1.8 diff --git a/src/main/java/com/mingliqiye/Main.java b/src/main/java/com/mingliqiye/Main.java deleted file mode 100644 index 1f285d4..0000000 --- a/src/main/java/com/mingliqiye/Main.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.mingliqiye; - -public class Main { - /** - * @param args [] - */ - public static void main(String[] args) { - System.out.print("Hello and welcome!"); - for (int i = 1; i <= 5; i++) { - System.out.println("i = " + i); - } - } -} diff --git a/src/main/java/com/mingliqiye/minecraftSLP/Main.java b/src/main/java/com/mingliqiye/minecraftSLP/Main.java new file mode 100644 index 0000000..b37d71f --- /dev/null +++ b/src/main/java/com/mingliqiye/minecraftSLP/Main.java @@ -0,0 +1,69 @@ +package com.mingliqiye.minecraftSLP; + +import com.mingliqiye.network.endpoint.NetworkEndpoint; +import com.mingliqiye.network.endpoint.NetworkException; +import java.io.IOException; + +public class Main { + + public static void main(String[] args) { + if (args.length != 1) { + System.err.println("请使用 {地址}:{端口号}"); + return; + } + try { + NetworkEndpoint networkEndpoints = NetworkEndpoint.of(args[0]); + MinecraftServerStatus minecraftServerStatus = SLP.getServerStatus( + networkEndpoints + ); + System.out.printf( + "服务器 %s %n", + networkEndpoints.toHostPortString() + ); + System.out.printf( + "版本号 %s 协议版本 %s %n", + minecraftServerStatus.getVersion().getName(), + minecraftServerStatus.getVersion().getProtocol() + ); + System.out.printf( + "描述信息 %s %n", + minecraftServerStatus.getDescription().getText() + ); + System.out.printf( + "玩家数 %s/%s %n", + minecraftServerStatus.getPlayers().getOnline(), + minecraftServerStatus.getPlayers().getMax() + ); + if (minecraftServerStatus.getPlayers().getOnline() != 0) { + System.out.println("在线玩家:"); + minecraftServerStatus + .getPlayers() + .getSample() + .forEach(v -> { + System.out.printf( + "玩家 %s UUID %s %n", + v.getName(), + v.getId() + ); + }); + } + System.out.println("源数据:"); + System.out.println(minecraftServerStatus.getJsonData()); + } catch (NetworkException e) { + System.err.printf( + "服务器地址 %s DNS 查询失败 %s %s %n", + args[0], + e.getClass().getName(), + e.getMessage() + ); + } catch (IOException e) { + System.err.printf( + "%s 查询失败 %s %s %n", + args[0], + e.getClass().getName(), + e.getMessage() + ); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/mingliqiye/minecraftSLP/MinecraftServerStatus.java b/src/main/java/com/mingliqiye/minecraftSLP/MinecraftServerStatus.java new file mode 100644 index 0000000..779b912 --- /dev/null +++ b/src/main/java/com/mingliqiye/minecraftSLP/MinecraftServerStatus.java @@ -0,0 +1,234 @@ +package com.mingliqiye.minecraftSLP; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class MinecraftServerStatus { + + private Description description; + private Players players; + private Version version; + private String favicon; + private boolean enforcesSecureChat; + private boolean previewsChat; + private String jsonData; + + // Getters and Setters + public Description getDescription() { + return description; + } + + public void setDescription(Description description) { + this.description = description; + } + + public Players getPlayers() { + return players; + } + + public void setPlayers(Players players) { + this.players = players; + } + + public Version getVersion() { + return version; + } + + public void setVersion(Version version) { + this.version = version; + } + + public String getFavicon() { + return favicon; + } + + public void setFavicon(String favicon) { + this.favicon = favicon; + } + + public boolean isEnforcesSecureChat() { + return enforcesSecureChat; + } + + public void setEnforcesSecureChat(boolean enforcesSecureChat) { + this.enforcesSecureChat = enforcesSecureChat; + } + + public boolean isPreviewsChat() { + return previewsChat; + } + + public void setPreviewsChat(boolean previewsChat) { + this.previewsChat = previewsChat; + } + + public String getJsonData() { + return jsonData; + } + + public void setJsonData(String jsonData) { + this.jsonData = jsonData; + } + + // Nested Classes + public static class Description { + + private String text; + private Extra[] extra; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public List getExtra() { + return Arrays.asList(extra); + } + + public void setExtra(Extra[] extra) { + this.extra = extra; + } + + public static class Extra { + + private String text; + private String color; + private Boolean bold; + private Boolean italic; + + // 其他样式字段... + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } + + public Boolean getBold() { + return bold; + } + + public void setBold(Boolean bold) { + this.bold = bold; + } + + public Boolean getItalic() { + return italic; + } + + public void setItalic(Boolean italic) { + this.italic = italic; + } + } + } + + public static class Players { + + private int max; + private int online; + private PlayerSample[] sample; + + public int getMax() { + return max; + } + + public void setMax(int max) { + this.max = max; + } + + public int getOnline() { + return online; + } + + public void setOnline(int online) { + this.online = online; + } + + public List getSample() { + return Arrays.asList(sample); + } + + public void setSample(PlayerSample[] sample) { + this.sample = sample; + } + + public static class PlayerSample { + + private String name; + private String id; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String toString() { + return String.format( + "Player(name=%s,id=%s)", + getName(), + getId() + ); + } + } + } + + public static class Version { + + private String name; + private int protocol; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getProtocol() { + return protocol; + } + + public void setProtocol(int protocol) { + this.protocol = protocol; + } + } + + // 实用方法 + @Override + public String toString() { + return String.format( + "Version: %s (Protocol %d), Players: %d/%d, Description: %s", + version != null ? version.name : "?", + version != null ? version.protocol : 0, + players != null ? players.online : 0, + players != null ? players.max : 0, + description != null ? description.text : "" + ); + } +} diff --git a/src/main/java/com/mingliqiye/minecraftSLP/SLP.java b/src/main/java/com/mingliqiye/minecraftSLP/SLP.java new file mode 100644 index 0000000..f9a8610 --- /dev/null +++ b/src/main/java/com/mingliqiye/minecraftSLP/SLP.java @@ -0,0 +1,166 @@ +package com.mingliqiye.minecraftSLP; + +import com.alibaba.fastjson2.JSON; +import com.mingliqiye.network.endpoint.NetworkEndpoint; +import java.io.*; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class SLP { + + /** + * 将 int32 4个字节 转换为 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; + } + + /** + * 获取 和服务器握手使用的字节 + * @param serverIP 服务器地址 + * @param serverPort 服务器端口号 + * @param type 类型 1为 Status + * @return 字节数组 + */ + 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)); + byteArrayOutputStream.write(toVarInt(pack.size())); + byteArrayOutputStream.write(pack.toByteArray()); + + return byteArrayOutputStream.toByteArray(); + } + + /** + * 获取信息的字节 是固定值 + * @return 信息的字节 + */ + public static byte[] getStatusPack() { + return new byte[] { 0x01, 0x00 }; + } + + /** + * 从输入流中读取 服务器信息 + * @param inputStream 输入流 + * @return 服务器信息实体 + * @throws IOException 读取错误 + */ + public static MinecraftServerStatus getStatusJsonEntity( + DataInputStream inputStream + ) throws IOException { + readVarInt(inputStream); + inputStream.readByte(); + int lengthjson = readVarInt(inputStream); + byte[] data = new byte[lengthjson]; + inputStream.readFully(data); + MinecraftServerStatus serverStatus = JSON.parseObject( + data, + MinecraftServerStatus.class + ); + serverStatus.setJsonData(new String(data)); + return serverStatus; + } + + /** + * 从输入流中读取 varint 1-5字节 + * @param in 输入流 + * @return int + * @throws IOException 读取错误 + */ + 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转换为 1-5个字节的 varint + * @param value int32 + * @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; + } + return buffer.toByteArray(); + } + + public static Socket getNewConnect(NetworkEndpoint networkEndpoint) + throws IOException { + Socket socket = new Socket(); + socket.setSoTimeout(5000); + socket.connect(networkEndpoint.toInetSocketAddress()); + return socket; + } + + /** + * @param s 域名IP+端口号字符串 示例 "127.0.0.1:25565" + * @return MinecraftServerStatus 服务器回执状态实体 + * @throws IOException 网络错误 + */ + public static MinecraftServerStatus getServerStatus(String s) + throws IOException { + return getServerStatus(NetworkEndpoint.of(s)); + } + + /** + * @param s 域名IP + * @param i 端口号 + * @return MinecraftServerStatus 服务器回执状态实体 + * @throws IOException 网络错误 + */ + public static MinecraftServerStatus getServerStatus(String s, Integer i) + throws IOException { + return getServerStatus(NetworkEndpoint.of(s, i)); + } + + /** + * @param e 域名IP+端口号实例 + * @return MinecraftServerStatus 服务器回执状态实体 + * @throws IOException 网络错误 + * @see NetworkEndpoint + */ + public static MinecraftServerStatus getServerStatus(NetworkEndpoint e) + throws IOException { + Socket socket = getNewConnect(e); + OutputStream out = socket.getOutputStream(); + DataInputStream in = new DataInputStream(socket.getInputStream()); + out.write(getHandshakePack(e.getHost(), e.getPort(), 1)); + out.write(getStatusPack()); + return getStatusJsonEntity(in); + } +}