如何读取Minecraft数据包并转换为JSON?自制服务器开发疑问
解决Minecraft服务器二进制数据包解析问题
你看到的乱码是因为Minecraft的网络数据包是二进制序列化格式,不是直接可读的UTF-8字符串,直接调用toString()自然会显示乱码。要解析这些数据并转成JSON,需要严格遵循Minecraft的网络协议规范,核心步骤是解码VarInt、处理数据包分片、解析对应协议段的内容。
核心原理
Minecraft数据包结构:
- 开头是VarInt类型的数据包总长度
- 接着是VarInt类型的数据包ID
- 最后是对应ID的负载数据(可能包含字符串、VarInt、NBT等类型)
无依赖实现步骤
1. 实现VarInt解码函数
VarInt是Minecraft协议的基础整数编码,用1-5字节存储,每个字节的最高位表示是否还有后续字节:
function readVarInt(buffer, offset = 0) { let value = 0; let position = offset; let bytesRead = 0; let currentByte; do { currentByte = buffer.readUInt8(position); value |= (currentByte & 0x7F) << (bytesRead * 7); bytesRead++; position++; if (bytesRead > 5) { throw new Error("VarInt exceeds maximum length (5 bytes)"); } } while ((currentByte & 0x80) !== 0); return { value, bytesRead }; }
2. 缓存分片数据,解析完整数据包
Socket的data事件可能只返回部分数据包,所以需要缓存数据直到收到完整的包:
import { createServer } from "net"; const server = createServer(); server.on("connection", (socket) => { let buffer = Buffer.alloc(0); socket.on("data", (chunk) => { // 拼接新收到的数据到缓存 buffer = Buffer.concat([buffer, chunk]); // 循环解析所有完整的数据包 while (buffer.length > 0) { try { // 先读数据包总长度 const { value: packetLength, bytesRead: lengthBytes } = readVarInt(buffer); // 检查缓存是否包含完整数据包 if (buffer.length < lengthBytes + packetLength) { break; // 数据不够,等待下一次data事件 } // 截取数据包内容(跳过长度前缀) const packetBuffer = buffer.slice(lengthBytes, lengthBytes + packetLength); // 更新缓存,移除已解析的数据包 buffer = buffer.slice(lengthBytes + packetLength); // 解析数据包ID const { value: packetId, bytesRead: idBytes } = readVarInt(packetBuffer); const payload = packetBuffer.slice(idBytes); // 根据数据包ID处理对应逻辑 handlePacket(packetId, payload); } catch (err) { console.error("解析数据包出错:", err); buffer = Buffer.alloc(0); // 出错时清空缓存,避免死循环 break; } } }); }); server.listen(25565, () => { console.log("服务器监听在25565端口"); });
3. 处理不同类型的数据包,转成JSON
以最常见的Handshake(0x00)和Login Start(0x00)数据包为例:
// 维护连接状态(握手/登录/状态/游戏),示例初始为握手状态 let connectionState = "handshake"; function handlePacket(packetId, payload) { switch (connectionState) { case "handshake": if (packetId === 0x00) { const { value: protocolVersion, bytesRead: protoBytes } = readVarInt(payload); const { string: serverAddress, bytesRead: addrBytes } = readString(payload.slice(protoBytes)); const port = payload.readUInt16BE(protoBytes + addrBytes); const { value: nextState, bytesRead: stateBytes } = readVarInt(payload.slice(protoBytes + addrBytes + 2)); // 更新连接状态 connectionState = nextState === 1 ? "status" : "login"; console.log("Handshake数据包转JSON:", JSON.stringify({ protocolVersion, serverAddress, port, nextState }, null, 2)); } break; case "login": if (packetId === 0x00) { const { string: playerName } = readString(payload); console.log("Login Start数据包转JSON:", JSON.stringify({ playerName }, null, 2)); } break; default: console.log(`未处理的连接状态: ${connectionState},数据包ID: 0x${packetId.toString(16)}`); } } // 读取Minecraft协议中的字符串(前缀是VarInt表示长度,后面是UTF-8字节) function readString(buffer, offset = 0) { const { value: length, bytesRead: lengthBytes } = readVarInt(buffer, offset); const string = buffer.toString('utf8', offset + lengthBytes, offset + lengthBytes + length); return { string, bytesRead: lengthBytes + length }; }
关键说明
- Minecraft的协议状态分为握手、登录、状态、游戏四个阶段,相同的数据包ID在不同阶段对应不同的payload结构,必须维护连接状态才能正确解析。
- 如果需要解析NBT格式的数据(比如游戏内的物品、区块数据),可以基于原生Buffer实现NBT解码器,无需第三方依赖。
内容的提问来源于stack exchange,提问作者sujang958
相关产品推荐
相关产品推荐

