B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
193 lines
7.7 KiB
JavaScript
193 lines
7.7 KiB
JavaScript
/**
|
|
* raw-connect.mjs — raw Minecraft protocol handshake for any server version.
|
|
*
|
|
* Performs the LOGIN handshake only (does not need minecraft-data for the version).
|
|
* Measures connection accept latency and capacity: how many simultaneous TCP
|
|
* connections the server accepts before refusing.
|
|
*
|
|
* Protocol: Minecraft Handshake → Login Start → (server login success or kick)
|
|
* Works with any version since the handshake packet structure is stable.
|
|
*
|
|
* Usage: node raw-connect.mjs <host> <port> <count>
|
|
*/
|
|
import net from 'net';
|
|
|
|
const HOST = process.argv[2] || 'localhost';
|
|
const PORT = parseInt(process.argv[3] || '25565');
|
|
const COUNT = parseInt(process.argv[4] || '20');
|
|
|
|
// ── Minecraft VarInt encoding ─────────────────────────────────────────────────
|
|
function writeVarInt(val) {
|
|
const bytes = [];
|
|
do {
|
|
let b = val & 0x7f;
|
|
val >>>= 7;
|
|
if (val !== 0) b |= 0x80;
|
|
bytes.push(b);
|
|
} while (val !== 0);
|
|
return Buffer.from(bytes);
|
|
}
|
|
|
|
function writeString(str) {
|
|
const strBuf = Buffer.from(str, 'utf8');
|
|
return Buffer.concat([writeVarInt(strBuf.length), strBuf]);
|
|
}
|
|
|
|
function packet(id, ...bufs) {
|
|
const payload = Buffer.concat([writeVarInt(id), ...bufs]);
|
|
return Buffer.concat([writeVarInt(payload.length), payload]);
|
|
}
|
|
|
|
// ── Handshake packet (ID 0x00, state HANDSHAKING) ────────────────────────────
|
|
// Fields: protocol_version (VarInt), server_address (string), server_port (u16), next_state (VarInt=2 login)
|
|
function handshakePacket(protocolVersion, host, port) {
|
|
return packet(0x00,
|
|
writeVarInt(protocolVersion), // protocol version
|
|
writeString(host),
|
|
Buffer.from([port >> 8, port & 0xff]),
|
|
writeVarInt(2) // next state: LOGIN
|
|
);
|
|
}
|
|
|
|
// ── Login Start packet (ID 0x00, state LOGIN) ─────────────────────────────────
|
|
// Fields: name (string), uuid (optional)
|
|
function loginStartPacket(name) {
|
|
return packet(0x00, writeString(name));
|
|
}
|
|
|
|
// ── Connect one bot ───────────────────────────────────────────────────────────
|
|
async function connectBot(index, protocolVersion) {
|
|
return new Promise((resolve) => {
|
|
const name = `bench_${String(index).padStart(3, '0')}`;
|
|
const t0 = Date.now();
|
|
const sock = new net.Socket();
|
|
let done = false;
|
|
|
|
const finish = (status, detail) => {
|
|
if (done) return;
|
|
done = true;
|
|
const ms = Date.now() - t0;
|
|
sock.destroy();
|
|
resolve({ index, name, status, detail, ms });
|
|
};
|
|
|
|
sock.setTimeout(15000);
|
|
sock.on('timeout', () => finish('timeout', 'connection timed out'));
|
|
sock.on('error', (e) => finish('error', e.message));
|
|
|
|
sock.connect(PORT, HOST, () => {
|
|
// Connected — send handshake then login start
|
|
sock.write(handshakePacket(protocolVersion, HOST, PORT));
|
|
sock.write(loginStartPacket(name));
|
|
});
|
|
|
|
sock.on('data', (buf) => {
|
|
// We just need to know the server responded (any response = accepted)
|
|
// Response could be: Login Success (0x02), Login Disconnect (0x00), Set Compression (0x03)
|
|
if (buf.length > 0) {
|
|
// Check first packet byte after length VarInt: try to identify packet type
|
|
const packetByte = buf[buf.length > 2 ? 2 : 1];
|
|
let statusStr = 'connected';
|
|
if (packetByte === 0x00) statusStr = 'kicked'; // Disconnect
|
|
else if (packetByte === 0x02) statusStr = 'accepted'; // Login Success
|
|
else if (packetByte === 0x03) statusStr = 'compress'; // Set Compression (pre-auth)
|
|
finish(statusStr, `${buf.length} bytes`);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── Probe server for protocol version ────────────────────────────────────────
|
|
async function probeVersion() {
|
|
return new Promise((resolve) => {
|
|
const sock = new net.Socket();
|
|
sock.setTimeout(5000);
|
|
sock.on('error', () => resolve(0));
|
|
sock.on('timeout', () => { sock.destroy(); resolve(0); });
|
|
|
|
sock.connect(PORT, HOST, () => {
|
|
// Status handshake: next_state=1 (STATUS)
|
|
const handshake = packet(0x00,
|
|
writeVarInt(0), // version 0 = unknown
|
|
writeString(HOST),
|
|
Buffer.from([PORT >> 8, PORT & 0xff]),
|
|
writeVarInt(1) // STATUS
|
|
);
|
|
const statusReq = packet(0x00); // Status Request
|
|
sock.write(Buffer.concat([handshake, statusReq]));
|
|
});
|
|
|
|
sock.on('data', (buf) => {
|
|
// Try to parse JSON from status response
|
|
try {
|
|
const str = buf.toString();
|
|
const jsonStart = str.indexOf('{');
|
|
const jsonEnd = str.lastIndexOf('}');
|
|
if (jsonStart >= 0 && jsonEnd > jsonStart) {
|
|
const json = JSON.parse(str.slice(jsonStart, jsonEnd + 1));
|
|
const protocol = json?.version?.protocol || 0;
|
|
const versionName = json?.version?.name || 'unknown';
|
|
resolve({ protocol, name: versionName, players: json?.players });
|
|
}
|
|
} catch (_) {}
|
|
sock.destroy();
|
|
resolve(0);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
console.log(`=== Minecraft Raw Connection Benchmark ===`);
|
|
console.log(` server: ${HOST}:${PORT}`);
|
|
console.log(` bots: ${COUNT}`);
|
|
|
|
// Probe server version
|
|
const serverInfo = await probeVersion();
|
|
const protocolVersion = typeof serverInfo === 'object' ? serverInfo.protocol : 0;
|
|
if (typeof serverInfo === 'object') {
|
|
console.log(` server version: ${serverInfo.name} (protocol ${protocolVersion})`);
|
|
if (serverInfo.players) {
|
|
console.log(` online: ${serverInfo.players.online}/${serverInfo.players.max}`);
|
|
}
|
|
}
|
|
console.log(``);
|
|
|
|
if (!protocolVersion) {
|
|
console.log('Could not probe server. Is it running?');
|
|
process.exit(1);
|
|
}
|
|
|
|
const wallStart = Date.now();
|
|
const results = [];
|
|
|
|
// Spawn all connections with stagger
|
|
for (let i = 0; i < COUNT; i++) {
|
|
await new Promise(r => setTimeout(r, 100));
|
|
results.push(connectBot(i, protocolVersion));
|
|
process.stdout.write(`\r connecting ${i + 1}/${COUNT}...`);
|
|
}
|
|
console.log('');
|
|
const all = await Promise.all(results);
|
|
const wallMs = Date.now() - wallStart;
|
|
|
|
// Analyse
|
|
const accepted = all.filter(r => ['accepted','compress','connected','kicked'].includes(r.status));
|
|
const errors = all.filter(r => r.status === 'error' || r.status === 'timeout');
|
|
const latencies = accepted.map(r => r.ms).sort((a,b) => a-b);
|
|
|
|
function pct(arr, p) {
|
|
const i = Math.ceil(p/100*arr.length)-1;
|
|
return arr[Math.max(0, Math.min(arr.length-1, i))];
|
|
}
|
|
|
|
console.log(`=== Results ===`);
|
|
console.log(` wall time: ${(wallMs/1000).toFixed(1)} s`);
|
|
console.log(` accepted: ${accepted.length}/${COUNT}`);
|
|
console.log(` errors: ${errors.length}`);
|
|
if (latencies.length > 0) {
|
|
console.log(` handshake latency:`);
|
|
console.log(` p50: ${pct(latencies, 50)} ms`);
|
|
console.log(` p95: ${pct(latencies, 95)} ms`);
|
|
console.log(` max: ${latencies[latencies.length-1]} ms`);
|
|
}
|
|
console.log(``);
|