diff --git a/Emulator/src/main/java/com/eu/habbo/networking/Server.java b/Emulator/src/main/java/com/eu/habbo/networking/Server.java index 7ae6f43f..4062141c 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/Server.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/Server.java @@ -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() { diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java index b3f81c91..fe0939e5 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java @@ -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); diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java index 44a1b6be..a5ca2c87 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java @@ -43,9 +43,20 @@ public class WebSocketChannelInitializer extends ChannelInitializer 0 ? configured : fallback; + } + private final SslContext sslContext; private final boolean sslEnabled; private final WebSocketServerProtocolConfig wsConfig; diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java index bf1e9369..0944b6b6 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java @@ -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"; diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java index 126648ed..6845206e 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java @@ -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 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)); } } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/encoders/GameByteEncryption.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/encoders/GameByteEncryption.java index d2930c52..e7a9a09b 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/encoders/GameByteEncryption.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/encoders/GameByteEncryption.java @@ -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); } }