服务器列表查询 (Server List Ping) 是 Minecraft 客户端和服务器之间的一种简单的通信协议。它用于查询服务器的状态,例如在线玩家数量、最大玩家数量、服务器版本等。
Server List Ping(SLP)
身为 Minecraft 爱好者, 看了市面有很多 Minecraft 服务器状态查询功能, 我就想着能不能自己做个, 刚开始以为只有 Java 或者 Python 才能写出这样的后端业务, 在我查阅了一些关于这种功能的资料后, 我发现了mcsrvstat这个网站,它提供了一个简单的 API,可以查询 Minecraft 服务器的状态,包括在线玩家数量、最大玩家数量、服务器版本等。
我就参照了这个 API 的文档,自己手撸了一个简单的查询 Minecraft 服务器状态的工具.
SLP 是Server List Ping的缩写,是 Minecraft 客户端和服务器之间的一种简单的通信协议。它用于查询服务器的状态,例如在线玩家数量、最大玩家数量、服务器版本等。SLP 共有三个阶段,分别是握手阶段(Handshake)、状态查询阶段(Status Request)、状态查询响应阶段(Status Response)。
握手阶段(Handshake)
在握手阶段,客户端向服务器发送一个状态为 1 的握手包,
| Packet ID |
Field Name |
Field Type |
Notes |
| 0x00 |
Protocol Version |
VarInt |
协议版本号 |
| 0x00 |
Server Address |
String |
服务器地址 |
| 0x00 |
Server Port |
String |
服务器端口 |
| 0x00 |
Next state |
VarInt |
下一个状态 1: 状态 2: 登录 |
实现 VarInt 编码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function packVarint(num) { const buffers = [];
while (true) { if (num > 0x7f) { buffers.push(0x80 | (num & 0x7f)); num >>= 7; } else { buffers.push(num); break; } }
return Uint8Array.from(buffers); }
|
实现 VarInt 解码
有编码, 就有解码, 为了方便后续的使用, 我们也需要实现 VarInt 的解码方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function unPackVarint(arr) { let result = 0;
for (let idx = 0; idx < 5; idx++) { let bufferVal = arr[idx];
result |= (bufferVal & 0x7f) << (idx * 7);
if ((bufferVal & 0x80) === 0) return result; }
return result; }
|
构建 handshake 数据包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| const host = "mc.hypixel.net"; const port = 25565;
const hostUnit8Arr = Uint8Array.from(Buffer.from(host)); const dataBody = concatData([ Uint8Array.from([0x00, 0x00]), packVarint(hostUnit8Arr.length), hostUnit8Arr, concatData([ Uint8Array.from(Buffer.from(Uint16Array.from([port]).buffer)), Uint8Array.from([0x01]), ]), ]);
const data = concatData([packVarint(dataBody.length), dataBody]);
function concatData(arrays) { const totalLength = arrays.reduce((acc, value) => acc + value.length, 0); let result = new Uint8Array(totalLength);
arrays.reduce((offset, arr) => { result.set(arr, offset); return offset + arr.length; }, 0);
return result; }
|
状态查询阶段(Status Request)
同上面一样,数据长度进行了 VarInt 编码处理,但因为太小还是和未编码一样(一个字节)
| Packet ID |
Field Name |
Field Type |
Notes |
| 0x00 |
no fieds |
no fieds |
no fieds |
1
| const data = Uint8Array.from([0x01, 0x00]);
|
状态查询响应阶段(Status Response)
在状态查询响应阶段,服务器向客户端发送一个包含服务器状态信息的 JSON 字符串。
| Packet ID |
Field Name |
Field Type |
Notes |
| 0x00 |
JSON Response |
String |
服务器状态信息 |
获取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function getResponseData() { const socket = new Socket().connect({ host: "mc.hypixel.mc", port: 25565 }); let packageDataLen, responseBuffers = Buffer.from("");
return new Promise((resolve) => { socket.on("readable", () => { if (socket.readableLength >= 5) { if (packageDataLen === undefined) { const packageLen = unPackVarint(socket); const packageId = unPackVarint(socket); const packageDataLength = unPackVarint(socket);
packageDataLen = packageDataLength; }
responseBuffers = Buffer.concat([responseBuffers, socket.read()]);
if (responseBuffers.length >= packageDataLen) { socket.destroy(); resolve(responseBuffers); } } }); }); }
|