mirror of
https://github.com/layue13/MoonBridge.git
synced 2026-10-26 02:52:23 +00:00
- proxy/:core、plugin-api、luckperms 及其上游子模块 vendor/luckperms - messaging/、backend/ 保持在根目录(messaging 是代理与后端共用的 API) - build-logic/(原 proxy-build-logic) - testing/:smoke/ 与 benchmarks/,不进入发行物 Gradle 项目名不变(settings.gradle.kts 映射目录),任务路径与 Maven 坐标照旧。 脚本的仓库根定位、文档与记录中的链接同步更新。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
310 lines
16 KiB
Java
310 lines
16 KiB
Java
package dev.moonbridge.core.session;
|
|
|
|
import dev.moonbridge.core.session.play.KeepAliveBridge;
|
|
|
|
import dev.moonbridge.core.protocol.MinecraftFrameDecoder;
|
|
import dev.moonbridge.core.protocol.ProtocolProfile;
|
|
import dev.moonbridge.core.protocol.ProtocolVarInt;
|
|
import dev.moonbridge.core.session.play.RawRelay;
|
|
import io.netty.bootstrap.Bootstrap;
|
|
import io.netty.bootstrap.ServerBootstrap;
|
|
import io.netty.buffer.ByteBuf;
|
|
import io.netty.channel.Channel;
|
|
import io.netty.channel.ChannelFuture;
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
import io.netty.channel.ChannelInboundHandlerAdapter;
|
|
import io.netty.channel.ChannelOption;
|
|
import io.netty.channel.ChannelInitializer;
|
|
import io.netty.channel.EventLoopGroup;
|
|
import io.netty.channel.nio.NioEventLoopGroup;
|
|
import io.netty.channel.socket.SocketChannel;
|
|
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
|
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.Socket;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import java.util.concurrent.Semaphore;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
import java.util.concurrent.atomic.AtomicLongArray;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.lang.management.GarbageCollectorMXBean;
|
|
import java.lang.management.ManagementFactory;
|
|
import java.lang.management.MemoryMXBean;
|
|
|
|
/** Synthetic loopback echo benchmark for direct and actual relay pipeline variants. */
|
|
public final class RelayBenchmark {
|
|
private enum RelayMode { RAW, FRAMED, FRAMED_KEEP_ALIVE }
|
|
private static final AtomicLong RELAYED = new AtomicLong();
|
|
private final EventLoopGroup acceptors = new NioEventLoopGroup(1);
|
|
private final EventLoopGroup io = new NioEventLoopGroup();
|
|
private final List<Channel> listeners = new ArrayList<>();
|
|
private RelayBenchmark() { }
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
int connections = intArg(args, "connections", 4);
|
|
int messages = intArg(args, "messages", 1000);
|
|
int warmup = intArg(args, "warmup", 100);
|
|
int payload = intArg(args, "payload", 1024);
|
|
int repeats = intArg(args, "repeats", 2);
|
|
int window = intArg(args, "window", 1);
|
|
if (connections < 1 || messages < 1 || warmup < 0 || payload < 1 || payload > 1_048_576
|
|
|| repeats < 1 || repeats > 10 || window < 1 || window > 1024)
|
|
throw new IllegalArgumentException("connections/messages/payload must be positive; warmup nonnegative; payload <= 1048576; repeats 1..10; window 1..1024");
|
|
|
|
var benchmark = new RelayBenchmark();
|
|
try {
|
|
int backendPort = benchmark.startBackend();
|
|
int rawPort = benchmark.startRelay(backendPort, RelayMode.RAW);
|
|
int framedPort = benchmark.startRelay(backendPort, RelayMode.FRAMED);
|
|
int framedKeepAlivePort = benchmark.startRelay(backendPort, RelayMode.FRAMED_KEEP_ALIVE);
|
|
byte[] body = frame(payload);
|
|
System.out.printf("Synthetic loopback TCP echo; frame_payload=%d bytes wire_frame=%d bytes connections=%d measured_messages_per_connection=%d warmup_per_connection=%d repeats=%d window=%d%n",
|
|
payload, body.length, connections, messages, warmup, repeats, window);
|
|
System.out.println("Comparison: direct echo, byte-stream RawRelay, framed RawRelay, and framed RawRelay with the real KeepAliveBridge; connect/setup time excluded.");
|
|
benchmark.run("baseline-start", backendPort, connections, messages, warmup, window, body);
|
|
for (int round = 1; round <= repeats; round++) {
|
|
if ((round & 1) == 1) {
|
|
benchmark.run("raw-relay-" + round, rawPort, connections, messages, warmup, window, body);
|
|
benchmark.run("framed-relay-" + round, framedPort, connections, messages, warmup, window, body);
|
|
benchmark.run("framed-keepalive-" + round, framedKeepAlivePort, connections, messages, warmup, window, body);
|
|
} else {
|
|
benchmark.run("framed-keepalive-" + round, framedKeepAlivePort, connections, messages, warmup, window, body);
|
|
benchmark.run("framed-relay-" + round, framedPort, connections, messages, warmup, window, body);
|
|
benchmark.run("raw-relay-" + round, rawPort, connections, messages, warmup, window, body);
|
|
}
|
|
}
|
|
benchmark.run("baseline-end", backendPort, connections, messages, warmup, window, body);
|
|
long expectedBytes = (long) connections * (messages + warmup) * body.length * (2 + 3L * repeats);
|
|
if (RELAYED.get() != expectedBytes) throw new IllegalStateException("backend byte count mismatch");
|
|
System.out.printf("backend_received_wire_bytes_all_phases=%d%n", RELAYED.get());
|
|
} finally {
|
|
benchmark.close();
|
|
}
|
|
}
|
|
|
|
private int startBackend() throws Exception {
|
|
ChannelFuture bound = new ServerBootstrap().group(acceptors, io).channel(NioServerSocketChannel.class)
|
|
.childOption(ChannelOption.TCP_NODELAY, true)
|
|
.childHandler(new ChannelInitializer<SocketChannel>() {
|
|
@Override protected void initChannel(SocketChannel ch) {
|
|
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
|
|
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
|
if (msg instanceof ByteBuf buf) RELAYED.addAndGet(buf.readableBytes());
|
|
ctx.writeAndFlush(msg);
|
|
}
|
|
});
|
|
}
|
|
}).bind("127.0.0.1", 0).sync();
|
|
listeners.add(bound.channel());
|
|
return ((InetSocketAddress) bound.channel().localAddress()).getPort();
|
|
}
|
|
|
|
private int startRelay(int backendPort, RelayMode mode) throws Exception {
|
|
ChannelFuture bound = new ServerBootstrap().group(acceptors, io).channel(NioServerSocketChannel.class)
|
|
.childOption(ChannelOption.TCP_NODELAY, true)
|
|
.childHandler(new ChannelInitializer<SocketChannel>() {
|
|
@Override protected void initChannel(SocketChannel front) {
|
|
front.config().setAutoRead(false);
|
|
if (mode != RelayMode.RAW) front.pipeline().addLast(new MinecraftFrameDecoder(
|
|
ProtocolProfile.minecraft1710(), true));
|
|
KeepAliveBridge.State keepAlives = mode == RelayMode.FRAMED_KEEP_ALIVE
|
|
? new KeepAliveBridge.State() : null;
|
|
new Bootstrap().group(front.eventLoop()).channel(NioSocketChannel.class)
|
|
.option(ChannelOption.TCP_NODELAY, true)
|
|
.handler(new ChannelInitializer<SocketChannel>() {
|
|
@Override protected void initChannel(SocketChannel ch) {
|
|
if (mode != RelayMode.RAW) ch.pipeline().addLast(new MinecraftFrameDecoder(
|
|
ProtocolProfile.minecraft1710(), true));
|
|
}
|
|
}).connect("127.0.0.1", backendPort).addListener(connect -> {
|
|
if (!connect.isSuccess()) { front.close(); return; }
|
|
Channel back = ((ChannelFuture) connect).channel();
|
|
try {
|
|
if (keepAlives != null) {
|
|
front.pipeline().addLast(new KeepAliveBridge(keepAlives, true, front::close));
|
|
back.pipeline().addLast(new KeepAliveBridge(keepAlives, false, back::close));
|
|
}
|
|
RawRelay.Link link = RawRelay.attach(front, back);
|
|
link.ready().whenComplete((ignored, failure) -> {
|
|
if (failure != null) { front.close(); back.close(); }
|
|
else link.start();
|
|
});
|
|
} catch (RuntimeException failure) { front.close(); back.close(); }
|
|
});
|
|
}
|
|
}).bind("127.0.0.1", 0).sync();
|
|
listeners.add(bound.channel());
|
|
return ((InetSocketAddress) bound.channel().localAddress()).getPort();
|
|
}
|
|
|
|
private static byte[] frame(int payloadBytes) {
|
|
byte[] bytes = new byte[ProtocolVarInt.encodedSize(payloadBytes) + payloadBytes];
|
|
int value = payloadBytes;
|
|
int offset = 0;
|
|
do {
|
|
int part = value & 0x7f;
|
|
value >>>= 7;
|
|
bytes[offset++] = (byte) (value == 0 ? part : part | 0x80);
|
|
} while (value != 0);
|
|
bytes[offset] = 0x03;
|
|
for (int i = offset + 1; i < bytes.length; i++) bytes[i] = (byte) (i * 31 + 7);
|
|
return bytes;
|
|
}
|
|
|
|
private void run(String label, int port, int connections, int messages, int warmup, int window,
|
|
byte[] body) throws Exception {
|
|
List<Worker> workers = new ArrayList<>();
|
|
CountDownLatch ready = new CountDownLatch(connections);
|
|
CountDownLatch start = new CountDownLatch(1);
|
|
CountDownLatch finished = new CountDownLatch(connections);
|
|
MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
|
|
long heapBefore = memory.getHeapMemoryUsage().getUsed();
|
|
long[] gcBefore = gcTotals();
|
|
long wallStart;
|
|
for (int i = 0; i < connections; i++) {
|
|
Worker worker = new Worker(port, messages, warmup, window, body, ready, start, finished);
|
|
workers.add(worker);
|
|
new Thread(worker, "bench-" + label + "-" + i).start();
|
|
}
|
|
if (!ready.await(30, TimeUnit.SECONDS)) throw new IllegalStateException("clients failed to connect");
|
|
wallStart = System.nanoTime();
|
|
start.countDown();
|
|
if (!finished.await(5, TimeUnit.MINUTES)) throw new IllegalStateException("benchmark timed out");
|
|
for (Worker worker : workers) if (worker.failure != null)
|
|
throw new IllegalStateException("benchmark client failed", worker.failure);
|
|
long elapsed = System.nanoTime() - wallStart;
|
|
long heapAfter = memory.getHeapMemoryUsage().getUsed();
|
|
long[] gcAfter = gcTotals();
|
|
long[] samples = workers.stream().flatMapToLong(w -> Arrays.stream(w.latencies)).toArray();
|
|
Arrays.sort(samples);
|
|
long ops = (long) connections * messages;
|
|
double seconds = elapsed / 1_000_000_000.0;
|
|
double mibps = (ops * (double) body.length / (1024 * 1024)) / seconds;
|
|
System.out.printf("%s: %.1f roundtrips/s, %.2f MiB/s one-way-wire-bytes, latency_ms p50=%.3f p95=%.3f p99=%.3f, gc_collections=%d gc_time_ms=%d heap_used_delta_bytes=%d%n",
|
|
label, ops / seconds, mibps, percentile(samples, .50) / 1e6, percentile(samples, .95) / 1e6,
|
|
percentile(samples, .99) / 1e6, gcAfter[0] - gcBefore[0], gcAfter[1] - gcBefore[1], heapAfter - heapBefore);
|
|
}
|
|
|
|
private static long percentile(long[] sorted, double q) {
|
|
return sorted[Math.min(sorted.length - 1, (int) Math.ceil(q * sorted.length) - 1)];
|
|
}
|
|
|
|
private static long[] gcTotals() {
|
|
long count = 0, millis = 0;
|
|
for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
|
|
if (bean.getCollectionCount() >= 0) count += bean.getCollectionCount();
|
|
if (bean.getCollectionTime() >= 0) millis += bean.getCollectionTime();
|
|
}
|
|
return new long[]{count, millis};
|
|
}
|
|
|
|
private void close() {
|
|
for (Channel channel : listeners) channel.close().awaitUninterruptibly();
|
|
io.shutdownGracefully().awaitUninterruptibly();
|
|
acceptors.shutdownGracefully().awaitUninterruptibly();
|
|
}
|
|
|
|
private static int intArg(String[] args, String key, int fallback) {
|
|
for (int i = 0; i < args.length; i++) if (args[i].equals("--" + key)) {
|
|
if (i + 1 >= args.length) throw new IllegalArgumentException("missing value for --" + key);
|
|
return Integer.parseInt(args[i + 1]);
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
private static final class Worker implements Runnable {
|
|
private final int port, messages, warmup, window;
|
|
private final byte[] body;
|
|
private final CountDownLatch ready, start, finished;
|
|
private long[] latencies;
|
|
private volatile Exception failure;
|
|
Worker(int port, int messages, int warmup, int window, byte[] body,
|
|
CountDownLatch ready, CountDownLatch start, CountDownLatch finished) {
|
|
this.port = port; this.messages = messages; this.warmup = warmup; this.window = window; this.body = body;
|
|
this.ready = ready; this.start = start; this.finished = finished;
|
|
this.latencies = new long[messages];
|
|
}
|
|
@Override public void run() {
|
|
try (Socket socket = new Socket()) {
|
|
socket.setTcpNoDelay(true);
|
|
socket.setSoTimeout(30_000);
|
|
socket.connect(new InetSocketAddress("127.0.0.1", port), 10_000);
|
|
InputStream in = socket.getInputStream();
|
|
OutputStream out = socket.getOutputStream();
|
|
byte[] response = new byte[body.length];
|
|
exchangeMessages(socket, out, in, body, response, warmup, window, null);
|
|
ready.countDown();
|
|
start.await();
|
|
exchangeMessages(socket, out, in, body, response, messages, window, latencies);
|
|
} catch (Exception failure) {
|
|
this.failure = failure;
|
|
ready.countDown();
|
|
latencies = new long[0];
|
|
} finally { finished.countDown(); }
|
|
}
|
|
private static void exchangeMessages(Socket socket, OutputStream out, InputStream in,
|
|
byte[] body, byte[] response, int count, int window,
|
|
long[] latencies) throws Exception {
|
|
if (window == 1) {
|
|
for (int i = 0; i < count; i++) {
|
|
long sentAt = latencies == null ? 0 : System.nanoTime();
|
|
out.write(body);
|
|
out.flush();
|
|
readEcho(in, body, response);
|
|
if (latencies != null) latencies[i] = System.nanoTime() - sentAt;
|
|
}
|
|
return;
|
|
}
|
|
|
|
Semaphore inFlight = new Semaphore(window);
|
|
AtomicLongArray sentAt = latencies == null ? null : new AtomicLongArray(count);
|
|
AtomicReference<Exception> writerFailure = new AtomicReference<>();
|
|
Thread writer = new Thread(() -> {
|
|
try {
|
|
for (int i = 0; i < count; i++) {
|
|
inFlight.acquire();
|
|
if (sentAt != null) sentAt.set(i, System.nanoTime());
|
|
out.write(body);
|
|
out.flush();
|
|
}
|
|
} catch (Exception failure) {
|
|
writerFailure.set(failure);
|
|
try { socket.close(); } catch (Exception ignored) { }
|
|
}
|
|
}, "bench-pipelined-writer");
|
|
writer.start();
|
|
try {
|
|
for (int i = 0; i < count; i++) {
|
|
readEcho(in, body, response);
|
|
if (latencies != null) latencies[i] = System.nanoTime() - sentAt.get(i);
|
|
inFlight.release();
|
|
}
|
|
} catch (Exception failure) {
|
|
writer.interrupt();
|
|
socket.close();
|
|
throw failure;
|
|
} finally {
|
|
writer.join();
|
|
}
|
|
if (writerFailure.get() != null) throw writerFailure.get();
|
|
}
|
|
|
|
private static void readEcho(InputStream in, byte[] body, byte[] response) throws Exception {
|
|
int offset = 0;
|
|
while (offset < response.length) {
|
|
int read = in.read(response, offset, response.length - offset);
|
|
if (read < 0) throw new IllegalStateException("echo closed early");
|
|
offset += read;
|
|
}
|
|
if (!Arrays.equals(body, response)) throw new IllegalStateException("echo payload mismatch");
|
|
}
|
|
}
|
|
}
|