You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 06:56:19 +00:00
#2 — tunable thread pools (sensible defaults kept): - io.packet.handler.threads overrides the packet-handler EventExecutorGroup size (default max(16, 2x cores)). - auth.http.pool.size overrides the auth HTTP pool max threads (default 16). #5 — Netty buffer pooling: - Make the crypto handlers pool-safe: GameByteEncryption/GameByteDecryption no longer call ByteBuf.array() on a readBytes-derived buffer (whose arrayOffset is non-zero under a pooled allocator, which would have read/encrypted the wrong region). They now copy the readable region into a plain byte[] (offset-safe) and wrap the result — also drops one intermediate buffer allocation. This is correct for the current unpooled allocator too. (ServerMessage uses its own Unpooled buffer, and ClientMessage reads via buffer methods, so both are already offset-safe.) - Add a shared channel allocator selected by io.netty.allocator.pooled (default false = unpooled-heap, unchanged). Set true for a pooled HEAP allocator (preferDirect=false, so array-backed paths keep working) to cut per-packet alloc/GC churn. Opt-in until validated under load with the Netty leak detector, since unreleased pooled buffers accumulate rather than being GC-reclaimed. New optional config keys (insert into emulator_settings to set/silence the "key not found" notice): io.packet.handler.threads, auth.http.pool.size, io.netty.allocator.pooled.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package com.eu.habbo.networking;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.buffer.PooledByteBufAllocator;
|
||||
import io.netty.buffer.UnpooledByteBufAllocator;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelOption;
|
||||
@@ -18,6 +21,30 @@ public abstract class Server {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Server.class);
|
||||
|
||||
private static volatile ByteBufAllocator sharedAllocator;
|
||||
|
||||
/**
|
||||
* Shared channel allocator. Defaults to unpooled-heap (the long-standing
|
||||
* behaviour); set {@code io.netty.allocator.pooled=true} to switch to a
|
||||
* pooled HEAP allocator (preferDirect=false, so the array-backed crypto
|
||||
* paths keep working) which removes the per-packet alloc/GC churn. Opt-in
|
||||
* until validated under load with the Netty leak detector, since pooled
|
||||
* buffers that aren't released accumulate instead of being GC-reclaimed.
|
||||
*/
|
||||
protected static ByteBufAllocator allocator() {
|
||||
if (sharedAllocator == null) {
|
||||
synchronized (Server.class) {
|
||||
if (sharedAllocator == null) {
|
||||
boolean pooled = Emulator.getConfig() != null
|
||||
&& "true".equalsIgnoreCase(Emulator.getConfig().getValue("io.netty.allocator.pooled", "false"));
|
||||
sharedAllocator = pooled ? new PooledByteBufAllocator(false) : new UnpooledByteBufAllocator(false);
|
||||
LOGGER.info("Netty ByteBuf allocator: {}", pooled ? "pooled-heap" : "unpooled-heap");
|
||||
}
|
||||
}
|
||||
}
|
||||
return sharedAllocator;
|
||||
}
|
||||
|
||||
protected final ServerBootstrap serverBootstrap;
|
||||
protected final EventLoopGroup bossGroup;
|
||||
protected final EventLoopGroup workerGroup;
|
||||
@@ -45,7 +72,7 @@ public abstract class Server {
|
||||
this.serverBootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
|
||||
this.serverBootstrap.childOption(ChannelOption.SO_RCVBUF, 4096);
|
||||
this.serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(4096));
|
||||
this.serverBootstrap.childOption(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(false));
|
||||
this.serverBootstrap.childOption(ChannelOption.ALLOCATOR, allocator());
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
|
||||
@@ -84,7 +84,7 @@ public class GameServer extends Server {
|
||||
this.webSocketBootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
|
||||
this.webSocketBootstrap.childOption(ChannelOption.SO_RCVBUF, 4096);
|
||||
this.webSocketBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(4096));
|
||||
this.webSocketBootstrap.childOption(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(false));
|
||||
this.webSocketBootstrap.childOption(ChannelOption.ALLOCATOR, allocator());
|
||||
this.webSocketBootstrap.childHandler(wsInitializer);
|
||||
|
||||
ChannelFuture wsFuture = this.webSocketBootstrap.bind(wsHost, wsPort);
|
||||
|
||||
+12
-1
@@ -43,9 +43,20 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
|
||||
// cross-client concurrency degree is the same the multi-threaded I/O group
|
||||
// already had. Daemon threads so they don't block JVM shutdown.
|
||||
private static final EventExecutorGroup PACKET_HANDLER_GROUP = new DefaultEventExecutorGroup(
|
||||
Math.max(16, Runtime.getRuntime().availableProcessors() * 2),
|
||||
packetHandlerThreads(),
|
||||
new DefaultThreadFactory("GamePacketHandler", true));
|
||||
|
||||
// Size of the packet-handler pool. Defaults to max(16, 2x CPU cores); set
|
||||
// the optional `io.packet.handler.threads` config key to override.
|
||||
private static int packetHandlerThreads() {
|
||||
int fallback = Math.max(16, Runtime.getRuntime().availableProcessors() * 2);
|
||||
if (Emulator.getConfig() == null) {
|
||||
return fallback;
|
||||
}
|
||||
int configured = Emulator.getConfig().getInt("io.packet.handler.threads", fallback);
|
||||
return configured > 0 ? configured : fallback;
|
||||
}
|
||||
|
||||
private final SslContext sslContext;
|
||||
private final boolean sslEnabled;
|
||||
private final WebSocketServerProtocolConfig wsConfig;
|
||||
|
||||
+13
-1
@@ -34,8 +34,9 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
// BCrypt, JDBC, the Turnstile HTTPS round-trip and SMTP — running that on the
|
||||
// Netty event loop stalls every client on the same worker. A SEPARATE pool
|
||||
// (not the shared game ThreadPooling) also keeps it from starving room cycles.
|
||||
private static final int AUTH_POOL_MAX = authPoolMax();
|
||||
private static final ThreadPoolExecutor AUTH_EXECUTOR = new ThreadPoolExecutor(
|
||||
4, 16, 60L, TimeUnit.SECONDS,
|
||||
Math.min(4, AUTH_POOL_MAX), AUTH_POOL_MAX, 60L, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(512),
|
||||
new java.util.concurrent.ThreadFactory() {
|
||||
private final AtomicInteger counter = new AtomicInteger(1);
|
||||
@@ -47,6 +48,17 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
});
|
||||
|
||||
// Max threads for the auth pool. Defaults to 16; set the optional
|
||||
// `auth.http.pool.size` config key to override.
|
||||
private static int authPoolMax() {
|
||||
int fallback = 16;
|
||||
if (com.eu.habbo.Emulator.getConfig() == null) {
|
||||
return fallback;
|
||||
}
|
||||
int configured = com.eu.habbo.Emulator.getConfig().getInt("auth.http.pool.size", fallback);
|
||||
return configured > 0 ? configured : fallback;
|
||||
}
|
||||
|
||||
static final String LOGIN_PATH = "/api/auth/login";
|
||||
static final String REGISTER_PATH = "/api/auth/register";
|
||||
static final String FORGOT_PATH = "/api/auth/forgot-password";
|
||||
|
||||
+9
-5
@@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.decoders;
|
||||
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
|
||||
@@ -15,14 +16,17 @@ public class GameByteDecryption extends ByteToMessageDecoder {
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
// Read all available bytes.
|
||||
ByteBuf data = in.readBytes(in.readableBytes());
|
||||
// Copy the readable region into a plain array (offset-safe, so this is
|
||||
// correct for pooled buffers too — buf.array() would have read the wrong
|
||||
// region for a pooled/sliced buffer).
|
||||
byte[] bytes = new byte[in.readableBytes()];
|
||||
in.readBytes(bytes);
|
||||
|
||||
// Decrypt.
|
||||
ctx.channel().attr(GameServerAttributes.CRYPTO_CLIENT).get().parse(data.array());
|
||||
// Decrypt in place.
|
||||
ctx.channel().attr(GameServerAttributes.CRYPTO_CLIENT).get().parse(bytes);
|
||||
|
||||
// Continue in the pipeline.
|
||||
out.add(data);
|
||||
out.add(Unpooled.wrappedBuffer(bytes));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-5
@@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.encoders;
|
||||
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelOutboundHandlerAdapter;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
@@ -14,16 +15,19 @@ public class GameByteEncryption extends ChannelOutboundHandlerAdapter {
|
||||
// convert to Bytebuf
|
||||
ByteBuf in = (ByteBuf) msg;
|
||||
|
||||
// read available bytes
|
||||
ByteBuf data = (in).readBytes(in.readableBytes());
|
||||
// Copy the readable region into a plain array (respects readerIndex /
|
||||
// arrayOffset, so this is correct for pooled buffers too — buf.array()
|
||||
// would have returned the wrong region for a pooled/sliced buffer).
|
||||
byte[] bytes = new byte[in.readableBytes()];
|
||||
in.readBytes(bytes);
|
||||
|
||||
//release old object
|
||||
ReferenceCountUtil.release(in);
|
||||
|
||||
// Encrypt.
|
||||
ctx.channel().attr(GameServerAttributes.CRYPTO_SERVER).get().parse(data.array());
|
||||
// Encrypt in place.
|
||||
ctx.channel().attr(GameServerAttributes.CRYPTO_SERVER).get().parse(bytes);
|
||||
|
||||
// Continue in the pipeline.
|
||||
ctx.write(data, promise);
|
||||
ctx.write(Unpooled.wrappedBuffer(bytes), promise);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user