引子
总所周知,Minecraft 的服务端和客户端是分离的两部分,客户端与服务端通过 TCP / IP(特指 Java 版,基岩版使用的是 UDP) 进行数据通讯(所以我们需要在服务端配置 server.properties 的 port 属性以及客户端连接时所需输入 IP:PORT)。如果我们知道客户端与服务端所采用的具体通讯协议,那么就可以伪装客户端对服务器发起访问请求从而进行一系列操作(比如压测
知识点
本节所涉及知识点如下:
- 对于特定协议的解析与封装
- Socket API
- BIO
说明
总所周知,截止到发帖日期,Minecraft Server 的最新版本已经达到了 1.18+,对于这样一个累积多年进行了无数版本迭代的成熟项目,其协议必然也经过了一系列发展变化,所以出于上手难度的考虑,本文将从低到高介绍 MC C / S 通信协议版本,基于 MC Server 的向下兼容,高版本服务器也支持对低版本客户端的解析,故此处我们使用 Sugarcane 1.17.1 这与的高板本服务器完成本章的测试。
开始
BETA 1.8 - 1.3
在 Minecraft 1.4 以前,如果需要请求服务器返回当前基本信息,则仅需向服务器发送 0xFE 这一个字节即可,服务器会按照以下以下协议返回其当前状态信息:
字段名称 | 字段类型 | 注意事项 |
---|---|---|
包ID | Byte | 返回的包ID应为: 0xFF |
字段长度 | Short | 数据包剩余部分的长度 |
MOTD | 一段以 UTF-16BE 编码的字符串 | 从这里开始,所有字段都应该在同一个字符串中用 § 分隔。此字符串的最大长度为 64 字节。 |
在线玩家数 | 服务器当前游玩的玩家数量. | |
最大玩家数 | 服务器能支持的最大玩家数量 |
基于上述我们可以编写以下程序对数据包进行解析,此处先给出通用工具方法
/**
* 获取经校验的合法字符串内容
* @apiNote 数据包ID需为 0xFF 且长度合法
* */
protected static String getSecureString(InputStream inputStream, InputStreamReader inputStreamReader) throws IOException {
int packetId = inputStream.read();
if (packetId == -1)
throw new IOException("Premature end of stream.");
if (packetId != 0xFF)
throw new IOException("Invalid packet ID (" + packetId + ").");
int length = inputStreamReader.read();
if (length == -1)
throw new IOException("Premature end of stream.");
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.");
return new String(chars);
}
解析代码
/**
* @version BETA - 1.3
* */
private void connect() throws IOException {
try (
Socket socket = new Socket()
) {
socket.setSoTimeout(TIMEOUT);
socket.connect(new InetSocketAddress(host, port), TIMEOUT);
try (
OutputStream dataOutputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_16BE);
) {
dataOutputStream.write(0xFE);
String string = getSecureString(inputStream, inputStreamReader);
String[] args = string.split("§");
motd = args[0];
onlinePlayers = Integer.parseInt(args[1]);
maxPlayers = Integer.parseInt(args[2]);
}
}
}
返回的数据内容(敏感部分已做处理
原始数据(指除包ID与字段长度之外的可视化数据)