First NIO attempt

Signed-off-by: TheMode <themode@outlook.fr>
This commit is contained in:
TheMode 2021-08-03 12:57:13 +02:00
parent e0cfd4c33c
commit 1c3bb5b0ff
43 changed files with 595 additions and 1571 deletions

View File

@ -107,12 +107,6 @@ dependencies {
// Only here to ensure J9 module support for extensions and our classloaders
testCompileOnly 'org.mockito:mockito-core:3.11.1'
// Netty
api 'io.netty:netty-handler:4.1.65.Final'
api 'io.netty:netty-codec:4.1.65.Final'
api 'io.netty:netty-transport-native-epoll:4.1.65.Final:linux-x86_64'
api 'io.netty:netty-transport-native-kqueue:4.1.65.Final:osx-x86_64'
// https://mvnrepository.com/artifact/it.unimi.dsi/fastutil
api 'it.unimi.dsi:fastutil:8.5.4'

View File

@ -21,10 +21,10 @@ import net.minestom.server.listener.manager.PacketListenerManager;
import net.minestom.server.monitoring.BenchmarkManager;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.netty.NettyServer;
import net.minestom.server.network.packet.server.play.PluginMessagePacket;
import net.minestom.server.network.packet.server.play.ServerDifficultyPacket;
import net.minestom.server.network.packet.server.play.UpdateViewDistancePacket;
import net.minestom.server.network.socket.Server;
import net.minestom.server.ping.ResponseDataConsumer;
import net.minestom.server.recipe.RecipeManager;
import net.minestom.server.scoreboard.TeamManager;
@ -44,6 +44,9 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
/**
* The main server class used to start the server and retrieve all the managers.
* <p>
@ -85,9 +88,7 @@ public final class MinecraftServer {
// Network
private static PacketListenerManager packetListenerManager;
private static PacketProcessor packetProcessor;
private static NettyServer nettyServer;
private static int nettyThreadCount = Runtime.getRuntime().availableProcessors();
private static boolean processNettyErrors = true;
private static Server server;
private static ExceptionManager exceptionManager;
@ -122,7 +123,6 @@ public final class MinecraftServer {
private static int chunkViewDistance = 8;
private static int entityViewDistance = 5;
private static int compressionThreshold = 256;
private static boolean packetCaching = true;
private static boolean groupedPacket = true;
private static boolean terminalEnabled = System.getProperty("minestom.terminal.disabled") == null;
private static ResponseDataConsumer responseDataConsumer;
@ -168,7 +168,11 @@ public final class MinecraftServer {
tagManager = new TagManager();
nettyServer = new NettyServer(packetProcessor);
try {
server = new Server(packetProcessor);
} catch (IOException e) {
e.printStackTrace();
}
initialized = true;
@ -277,16 +281,6 @@ public final class MinecraftServer {
return packetListenerManager;
}
/**
* Gets the netty server.
*
* @return the netty server
*/
public static NettyServer getNettyServer() {
checkInitStatus(nettyServer);
return nettyServer;
}
/**
* Gets the manager handling all registered instances.
*
@ -519,34 +513,6 @@ public final class MinecraftServer {
MinecraftServer.compressionThreshold = compressionThreshold;
}
/**
* Gets if the packet caching feature is enabled.
* <p>
* This feature allows some packets (implementing the {@link net.minestom.server.utils.cache.CacheablePacket} to be cached
* in order to do not have to be written and compressed over and over again), this is especially useful for chunk and light packets.
* <p>
* It is enabled by default and it is our recommendation,
* you should only disable it if you want to focus on low memory usage
* at the cost of many packet writing and compression.
*
* @return true if the packet caching feature is enabled, false otherwise
*/
public static boolean hasPacketCaching() {
return packetCaching;
}
/**
* Enables or disable packet caching.
*
* @param packetCaching true to enable packet caching
* @throws IllegalStateException if this is called after the server started
* @see #hasPacketCaching()
*/
public static void setPacketCaching(boolean packetCaching) {
Check.stateCondition(started, "You cannot change the packet caching value after the server has been started.");
MinecraftServer.packetCaching = packetCaching;
}
/**
* Gets if the packet caching feature is enabled.
* <p>
@ -666,45 +632,9 @@ public final class MinecraftServer {
return updateManager;
}
/**
* Gets the number of threads used by Netty.
* <p>
* Is the number of vCPU by default.
*
* @return the number of netty threads
*/
public static int getNettyThreadCount() {
return nettyThreadCount;
}
/**
* Changes the number of threads used by Netty.
*
* @param nettyThreadCount the number of threads
* @throws IllegalStateException if the server is already started
*/
public static void setNettyThreadCount(int nettyThreadCount) {
Check.stateCondition(started, "Netty thread count can only be changed before the server starts!");
MinecraftServer.nettyThreadCount = nettyThreadCount;
}
/**
* Gets if the server should process netty errors and other unnecessary netty events.
*
* @return should process netty errors
*/
public static boolean shouldProcessNettyErrors() {
return processNettyErrors;
}
/**
* Sets if the server should process netty errors and other unnecessary netty events.
* false is faster
*
* @param processNettyErrors should process netty errors
*/
public static void setShouldProcessNettyErrors(boolean processNettyErrors) {
MinecraftServer.processNettyErrors = processNettyErrors;
public static Server getServer() {
checkInitStatus(server);
return server;
}
/**
@ -744,8 +674,11 @@ public final class MinecraftServer {
updateManager.start();
// Init & start the TCP server
nettyServer.init();
nettyServer.start(address, port);
try {
server.start(new InetSocketAddress(address, port));
} catch (IOException e) {
e.printStackTrace();
}
if (extensionManager.shouldLoadOnStartup()) {
final long loadStartTime = System.nanoTime();
@ -779,7 +712,7 @@ public final class MinecraftServer {
updateManager.stop();
schedulerManager.shutdown();
connectionManager.shutdown();
nettyServer.stop();
server.stop();
storageManager.getLoadedLocations().forEach(StorageLocation::close);
LOGGER.info("Unloading all extensions.");
extensionManager.shutdown();

View File

@ -1,6 +1,5 @@
package net.minestom.server.command.builder.arguments;
import io.netty.util.internal.StringUtil;
import net.minestom.server.command.builder.NodeMaker;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
@ -48,11 +47,10 @@ public class ArgumentString extends Argument<String> {
*/
@Deprecated
public static String staticParse(@NotNull String input) throws ArgumentSyntaxException {
// Return if not quoted
if (!input.contains(String.valueOf(DOUBLE_QUOTE)) &&
!input.contains(String.valueOf(QUOTE)) &&
!input.contains(String.valueOf(StringUtil.SPACE))) {
!input.contains(StringUtils.SPACE)) {
return input;
}

View File

@ -121,7 +121,7 @@ public class OpenToLAN {
* Performs the ping.
*/
private static void ping() {
if (MinecraftServer.getNettyServer().getPort() != 0) {
if (MinecraftServer.getServer().getPort() != 0) {
if (packet == null || eventCooldown.isReady(System.currentTimeMillis())) {
final ServerListPingEvent event = new ServerListPingEvent(OPEN_TO_LAN);
EventDispatcher.call(event);

View File

@ -1,54 +0,0 @@
package net.minestom.server.extras.mojangAuth;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import org.jetbrains.annotations.NotNull;
import javax.crypto.Cipher;
import javax.crypto.ShortBufferException;
public class CipherBase {
private final Cipher cipher;
private byte[] inTempArray = new byte[0];
private byte[] outTempArray = new byte[0];
protected CipherBase(@NotNull Cipher cipher) {
this.cipher = cipher;
}
private byte[] bufToByte(ByteBuf buffer) {
int remainingBytes = buffer.readableBytes();
// Need to resize temp array
if (inTempArray.length < remainingBytes) {
inTempArray = new byte[remainingBytes];
}
buffer.readBytes(inTempArray, 0, remainingBytes);
return inTempArray;
}
protected ByteBuf decrypt(ChannelHandlerContext channelHandlerContext, ByteBuf byteBufIn) throws ShortBufferException {
int remainingBytes = byteBufIn.readableBytes();
byte[] bytes = bufToByte(byteBufIn);
ByteBuf outputBuffer = channelHandlerContext.alloc().heapBuffer(cipher.getOutputSize(remainingBytes));
outputBuffer.writerIndex(cipher.update(bytes, 0, remainingBytes, outputBuffer.array(), outputBuffer.arrayOffset()));
return outputBuffer;
}
protected void encrypt(ByteBuf byteBufIn, ByteBuf byteBufOut) throws ShortBufferException {
int remainingBytes = byteBufIn.readableBytes();
byte[] bytes = bufToByte(byteBufIn);
int newSize = cipher.getOutputSize(remainingBytes);
// Need to resize temp array
if (outTempArray.length < newSize) {
outTempArray = new byte[newSize];
}
byteBufOut.writeBytes(outTempArray, 0, cipher.update(bytes, 0, remainingBytes, outTempArray));
}
}

View File

@ -1,20 +0,0 @@
package net.minestom.server.extras.mojangAuth;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import javax.crypto.Cipher;
import java.util.List;
public class Decrypter extends MessageToMessageDecoder<ByteBuf> {
private final CipherBase cipher;
public Decrypter(Cipher cipher) {
this.cipher = new CipherBase(cipher);
}
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
list.add(this.cipher.decrypt(channelHandlerContext, byteBuf));
}
}

View File

@ -1,19 +0,0 @@
package net.minestom.server.extras.mojangAuth;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import javax.crypto.Cipher;
public class Encrypter extends MessageToByteEncoder<ByteBuf> {
private final CipherBase cipher;
public Encrypter(Cipher cipher) {
this.cipher = new CipherBase(cipher);
}
protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBufIn, ByteBuf byteBufOut) throws Exception {
this.cipher.encrypt(byteBufIn, byteBufOut);
}
}

View File

@ -1,7 +1,5 @@
package net.minestom.server.extras.query;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@ -23,6 +21,7 @@ import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Random;
@ -151,18 +150,18 @@ public class Query {
}
// get the contents
ByteBuf data = Unpooled.wrappedBuffer(packet.getData());
ByteBuffer data = ByteBuffer.wrap(packet.getData());
// check the magic field
if (data.readUnsignedShort() != 0xFEFD) {
if ((data.getShort() & 0xFFFF) != 0xFEFD) {
continue;
}
// now check the query type
byte type = data.readByte();
byte type = data.get();
if (type == 9) { // handshake
int sessionID = data.readInt();
int sessionID = data.getInt();
int challengeToken = RANDOM.nextInt();
CHALLENGE_TOKENS.put(challengeToken, packet.getSocketAddress());
@ -184,12 +183,12 @@ public class Query {
}
}
} else if (type == 0) { // stat
int sessionID = data.readInt();
int challengeToken = data.readInt();
int sessionID = data.getInt();
int challengeToken = data.getInt();
SocketAddress sender = packet.getSocketAddress();
if (CHALLENGE_TOKENS.containsKey(challengeToken) && CHALLENGE_TOKENS.get(challengeToken).equals(sender)) {
int remaining = data.readableBytes();
int remaining = data.remaining();
if (remaining == 0) { // basic
BasicQueryEvent event = new BasicQueryEvent(sender, sessionID);

View File

@ -142,7 +142,7 @@ public class BasicQueryResponse implements Writeable {
writer.writeNullTerminatedString(this.map, Query.CHARSET);
writer.writeNullTerminatedString(this.numPlayers, Query.CHARSET);
writer.writeNullTerminatedString(this.maxPlayers, Query.CHARSET);
writer.getBuffer().writeShortLE(MinecraftServer.getNettyServer().getPort());
writer.writeNullTerminatedString(Objects.requireNonNullElse(MinecraftServer.getNettyServer().getAddress(), ""), Query.CHARSET);
writer.getBuffer().putShort((short) MinecraftServer.getServer().getPort()); // TODO little endian?
writer.writeNullTerminatedString(Objects.requireNonNullElse(MinecraftServer.getServer().getAddress(), ""), Query.CHARSET);
}
}

View File

@ -20,8 +20,8 @@ public enum QueryKey {
MAP(() -> "world"),
NUM_PLAYERS("numplayers", () -> String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayers().size())),
MAX_PLAYERS("maxplayers", () -> String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayers().size() + 1)),
HOST_PORT("hostport", () -> String.valueOf(MinecraftServer.getNettyServer().getPort())),
HOST_IP("hostip", () -> Objects.requireNonNullElse(MinecraftServer.getNettyServer().getAddress(), "localhost"));
HOST_PORT("hostport", () -> String.valueOf(MinecraftServer.getServer().getPort())),
HOST_IP("hostip", () -> Objects.requireNonNullElse(MinecraftServer.getServer().getAddress(), "localhost"));
static QueryKey[] VALUES = QueryKey.values();

View File

@ -1,6 +1,5 @@
package net.minestom.server.extras.velocity;
import io.netty.buffer.ByteBuf;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.PlayerSkin;
import net.minestom.server.utils.binary.BinaryReader;
@ -10,6 +9,7 @@ import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -54,9 +54,11 @@ public final class VelocityProxy {
final byte[] signature = reader.readBytes(32);
ByteBuf buf = reader.getBuffer();
final byte[] data = new byte[buf.readableBytes()];
buf.getBytes(buf.readerIndex(), data);
ByteBuffer buf = reader.getBuffer();
buf.mark();
final byte[] data = new byte[buf.remaining()];
buf.get(data);
buf.reset();
try {
final Mac mac = Mac.getInstance("HmacSHA256");

View File

@ -256,7 +256,7 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka
List<byte[]> skyLights = new ArrayList<>();
List<byte[]> blockLights = new ArrayList<>();
UpdateLightPacket updateLightPacket = new UpdateLightPacket(getIdentifier(), getLastChangeTime());
UpdateLightPacket updateLightPacket = new UpdateLightPacket();
updateLightPacket.chunkX = getChunkX();
updateLightPacket.chunkZ = getChunkZ();
@ -392,7 +392,5 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka
*/
protected void unload() {
this.loaded = false;
ChunkDataPacket.CACHE.invalidate(getIdentifier());
UpdateLightPacket.CACHE.invalidate(getIdentifier());
}
}

View File

@ -124,7 +124,7 @@ public class DynamicChunk extends Chunk {
if (packet != null && cachedPacketTime == getLastChangeTime()) {
return packet;
}
packet = new ChunkDataPacket(getIdentifier(), getLastChangeTime());
packet = new ChunkDataPacket();
packet.biomes = biomes;
packet.chunkX = chunkX;
packet.chunkZ = chunkZ;

View File

@ -1,6 +1,5 @@
package net.minestom.server.item;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.text.Component;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.attribute.ItemAttribute;
@ -13,6 +12,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.function.Consumer;
@ -36,7 +36,7 @@ public class ItemMeta implements TagReadable, Writeable {
private final NBTCompound nbt;
private String cachedSNBT;
private ByteBuf cachedBuffer;
private ByteBuffer cachedBuffer;
protected ItemMeta(@NotNull ItemMetaBuilder metaBuilder) {
this.damage = metaBuilder.damage;
@ -154,6 +154,6 @@ public class ItemMeta implements TagReadable, Writeable {
this.cachedBuffer = w.getBuffer();
}
writer.write(cachedBuffer);
this.cachedBuffer.resetReaderIndex();
this.cachedBuffer.position(0);
}
}

View File

@ -1,6 +1,5 @@
package net.minestom.server.network;
import io.netty.channel.Channel;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.MinecraftServer;
@ -24,6 +23,7 @@ import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
@ -294,7 +294,7 @@ public final class ConnectionManager {
// Close the player channel if he has been disconnected (kick)
if (!player.isOnline()) {
if (playerConnection instanceof NettyPlayerConnection) {
((NettyPlayerConnection) playerConnection).getChannel().flush();
((NettyPlayerConnection) playerConnection).flush();
}
//playerConnection.disconnect();
return;
@ -350,9 +350,12 @@ public final class ConnectionManager {
final PlayerConnection playerConnection = player.getPlayerConnection();
if (playerConnection instanceof NettyPlayerConnection) {
final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) playerConnection;
final Channel channel = nettyPlayerConnection.getChannel();
channel.writeAndFlush(disconnectPacket);
channel.close();
nettyPlayerConnection.writeAndFlush(disconnectPacket);
try {
nettyPlayerConnection.getChannel().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
this.players.clear();

View File

@ -1,7 +1,5 @@
package net.minestom.server.network;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.SocketChannel;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.network.netty.packet.InboundPacket;
@ -16,13 +14,9 @@ import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.Readable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Responsible for processing client packets.
* <p>
@ -34,11 +28,8 @@ import java.util.concurrent.ConcurrentHashMap;
* the same meaning as it is a login or play packet).
*/
public final class PacketProcessor {
private final static Logger LOGGER = LoggerFactory.getLogger(PacketProcessor.class);
private final Map<ChannelHandlerContext, PlayerConnection> connectionPlayerConnectionMap = new ConcurrentHashMap<>();
// Protocols state
private final ClientStatusPacketsHandler statusPacketsHandler;
private final ClientLoginPacketsHandler loginPacketsHandler;
@ -50,33 +41,15 @@ public final class PacketProcessor {
this.playPacketsHandler = new ClientPlayPacketsHandler();
}
public void process(@NotNull ChannelHandlerContext context, @NotNull InboundPacket packet) {
final SocketChannel socketChannel = (SocketChannel) context.channel();
// Create the netty player connection object if not existing
PlayerConnection playerConnection = connectionPlayerConnectionMap.get(context);
if (playerConnection == null) {
// Should never happen
context.close();
return;
}
// Prevent the client from sending packets when disconnected (kick)
if (!playerConnection.isOnline() || !socketChannel.isActive()) {
playerConnection.disconnect();
return;
}
public void process(@NotNull NettyPlayerConnection playerConnection, @NotNull InboundPacket packet) {
// Increment packet count (checked in PlayerConnection#update)
if (MinecraftServer.getRateLimit() > 0) {
playerConnection.getPacketCounter().incrementAndGet();
}
final ConnectionState connectionState = playerConnection.getConnectionState();
final int packetId = packet.getPacketId();
BinaryReader binaryReader = new BinaryReader(packet.getBody());
final ConnectionState connectionState = playerConnection.getConnectionState();
if (connectionState == ConnectionState.UNKNOWN) {
// Should be handshake packet
if (packetId == 0) {
@ -86,7 +59,6 @@ public final class PacketProcessor {
}
return;
}
switch (connectionState) {
case PLAY:
final Player player = playerConnection.getPlayer();
@ -108,34 +80,13 @@ public final class PacketProcessor {
}
}
/**
* Retrieves a player connection from its channel.
*
* @param context the connection context
* @return the connection of this channel, null if not found
*/
@Nullable
public PlayerConnection getPlayerConnection(ChannelHandlerContext context) {
return connectionPlayerConnectionMap.get(context);
}
public void createPlayerConnection(@NotNull ChannelHandlerContext context) {
final PlayerConnection playerConnection = new NettyPlayerConnection((SocketChannel) context.channel());
connectionPlayerConnectionMap.put(context, playerConnection);
}
public PlayerConnection removePlayerConnection(@NotNull ChannelHandlerContext context) {
return connectionPlayerConnectionMap.remove(context);
}
/**
* Gets the handler for client status packets.
*
* @return the status packets handler
* @see <a href="https://wiki.vg/Protocol#Status">Status packets</a>
*/
@NotNull
public ClientStatusPacketsHandler getStatusPacketsHandler() {
public @NotNull ClientStatusPacketsHandler getStatusPacketsHandler() {
return statusPacketsHandler;
}
@ -145,8 +96,7 @@ public final class PacketProcessor {
* @return the status login handler
* @see <a href="https://wiki.vg/Protocol#Login">Login packets</a>
*/
@NotNull
public ClientLoginPacketsHandler getLoginPacketsHandler() {
public @NotNull ClientLoginPacketsHandler getLoginPacketsHandler() {
return loginPacketsHandler;
}
@ -156,8 +106,7 @@ public final class PacketProcessor {
* @return the play packets handler
* @see <a href="https://wiki.vg/Protocol#Play">Play packets</a>
*/
@NotNull
public ClientPlayPacketsHandler getPlayPacketsHandler() {
public @NotNull ClientPlayPacketsHandler getPlayPacketsHandler() {
return playPacketsHandler;
}
@ -170,12 +119,10 @@ public final class PacketProcessor {
*/
private void safeRead(@NotNull PlayerConnection connection, @NotNull Readable readable, @NotNull BinaryReader reader) {
final int readableBytes = reader.available();
// Check if there is anything to read
if (readableBytes == 0) {
return;
}
try {
readable.read(reader);
} catch (Exception e) {

View File

@ -1,202 +0,0 @@
package net.minestom.server.network.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.kqueue.KQueue;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.kqueue.KQueueServerSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import net.minestom.server.MinecraftServer;
import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.netty.channel.ClientChannel;
import net.minestom.server.network.netty.codec.*;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
@ApiStatus.Internal
public final class NettyServer {
public static final Logger LOGGER = LoggerFactory.getLogger(NettyServer.class);
public static final int BUFFER_SIZE = Integer.getInteger("minestom.channel-buffer-size", 65535);
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20,
1 << 21);
public static final String LEGACY_PING_HANDLER_NAME = "legacy-ping"; // Read
public static final String ENCRYPT_HANDLER_NAME = "encrypt"; // Write
public static final String DECRYPT_HANDLER_NAME = "decrypt"; // Read
public static final String GROUPED_PACKET_HANDLER_NAME = "grouped-packet"; // Write
public static final String FRAMER_HANDLER_NAME = "framer"; // Read/write
public static final String COMPRESSOR_HANDLER_NAME = "compressor"; // Read/write
public static final String DECODER_HANDLER_NAME = "decoder"; // Read
public static final String ENCODER_HANDLER_NAME = "encoder"; // Write
public static final String CLIENT_CHANNEL_NAME = "handler"; // Read
private boolean initialized = false;
private final PacketProcessor packetProcessor;
private EventLoopGroup boss, worker;
private ServerBootstrap bootstrap;
private ServerSocketChannel serverChannel;
private String address;
private int port;
public NettyServer(@NotNull PacketProcessor packetProcessor) {
this.packetProcessor = packetProcessor;
}
/**
* Inits the server by choosing which transport layer to use, number of threads, pipeline order, etc...
* <p>
* Called just before {@link #start(String, int)}.
*/
public void init() {
Check.stateCondition(initialized, "Netty server has already been initialized!");
this.initialized = true;
Class<? extends ServerChannel> channel;
final int workerThreadCount = MinecraftServer.getNettyThreadCount();
// Find boss/worker event group
{
if (Epoll.isAvailable()) {
boss = new EpollEventLoopGroup(2);
worker = new EpollEventLoopGroup(workerThreadCount);
channel = EpollServerSocketChannel.class;
LOGGER.info("Using epoll");
} else if (KQueue.isAvailable()) {
boss = new KQueueEventLoopGroup(2);
worker = new KQueueEventLoopGroup(workerThreadCount);
channel = KQueueServerSocketChannel.class;
LOGGER.info("Using kqueue");
} else {
boss = new NioEventLoopGroup(2);
worker = new NioEventLoopGroup(workerThreadCount);
channel = NioServerSocketChannel.class;
LOGGER.info("Using NIO");
}
}
bootstrap = new ServerBootstrap()
.group(boss, worker)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, SERVER_WRITE_MARK)
.channel(channel);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(@NotNull SocketChannel ch) {
ChannelConfig config = ch.config();
config.setOption(ChannelOption.TCP_NODELAY, true);
config.setOption(ChannelOption.SO_KEEPALIVE, true);
config.setOption(ChannelOption.SO_SNDBUF, BUFFER_SIZE);
config.setAllocator(ByteBufAllocator.DEFAULT);
ChannelPipeline pipeline = ch.pipeline();
// First check should verify if the packet is a legacy ping (from 1.6 version and earlier)
// Removed from the pipeline later in LegacyPingHandler if unnecessary (>1.6)
pipeline.addLast(LEGACY_PING_HANDLER_NAME, new LegacyPingHandler());
// Used to bypass all the previous handlers by directly sending a framed buffer
pipeline.addLast(GROUPED_PACKET_HANDLER_NAME, new GroupedPacketHandler());
// Adds packetLength at start | Reads framed buffer
pipeline.addLast(FRAMER_HANDLER_NAME, new PacketFramer(packetProcessor));
// Reads buffer and create inbound packet
pipeline.addLast(DECODER_HANDLER_NAME, new PacketDecoder());
// Writes packet to buffer
pipeline.addLast(ENCODER_HANDLER_NAME, new PacketEncoder());
pipeline.addLast(CLIENT_CHANNEL_NAME, new ClientChannel(packetProcessor));
}
});
}
/**
* Binds the address to start the server.
*
* @param address the server address
* @param port the server port
*/
public void start(@NotNull String address, int port) {
this.address = address;
this.port = port;
// Bind address
try {
ChannelFuture cf = bootstrap.bind(new InetSocketAddress(address, port)).sync();
if (!cf.isSuccess()) {
throw new IllegalStateException("Unable to bind server at " + address + ":" + port);
}
this.serverChannel = (ServerSocketChannel) cf.channel();
} catch (InterruptedException ex) {
MinecraftServer.getExceptionManager().handleException(ex);
}
}
public ServerSocketChannel getServerChannel() {
return serverChannel;
}
/**
* Gets the address of the server.
*
* @return the server address, null if the address isn't bound yet
*/
@Nullable
public String getAddress() {
return address;
}
/**
* Gets the port used by the server.
*
* @return the server port, 0 if the address isn't bound yet
*/
public int getPort() {
return port;
}
/**
* Stops the server.
*/
public void stop() {
try {
this.boss.shutdownGracefully().sync();
this.worker.shutdownGracefully().sync();
this.serverChannel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

View File

@ -1,90 +0,0 @@
package net.minestom.server.network.netty.channel;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.netty.packet.InboundPacket;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerConnection;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ClientChannel extends SimpleChannelInboundHandler<InboundPacket> {
private final static Logger LOGGER = LoggerFactory.getLogger(ClientChannel.class);
private final static ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
private final PacketProcessor packetProcessor;
public ClientChannel(@NotNull PacketProcessor packetProcessor) {
this.packetProcessor = packetProcessor;
}
@Override
public void channelActive(@NotNull ChannelHandlerContext ctx) {
//System.out.println("CONNECTION");
packetProcessor.createPlayerConnection(ctx);
}
@Override
public void channelRead0(ChannelHandlerContext ctx, InboundPacket packet) {
try {
packetProcessor.process(ctx, packet);
} catch (Exception e) {
MinecraftServer.getExceptionManager().handleException(e);
} finally {
// Check remaining
final ByteBuf body = packet.getBody();
final int packetId = packet.getPacketId();
final int availableBytes = body.readableBytes();
if (availableBytes > 0) {
final PlayerConnection playerConnection = packetProcessor.getPlayerConnection(ctx);
LOGGER.warn("WARNING: Packet 0x{} not fully read ({} bytes left), {}",
Integer.toHexString(packetId),
availableBytes,
playerConnection);
body.skipBytes(availableBytes);
}
}
}
@Override
public void channelInactive(@NotNull ChannelHandlerContext ctx) {
PlayerConnection playerConnection = packetProcessor.removePlayerConnection(ctx);
if (playerConnection != null) {
// Remove the connection
playerConnection.refreshOnline(false);
Player player = playerConnection.getPlayer();
if (player != null) {
player.remove();
CONNECTION_MANAGER.removePlayer(playerConnection);
}
// Release tick buffer
if (playerConnection instanceof NettyPlayerConnection) {
((NettyPlayerConnection) playerConnection).releaseTickBuffer();
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (!ctx.channel().isActive()) {
return;
}
if (MinecraftServer.shouldProcessNettyErrors()) {
MinecraftServer.getExceptionManager().handleException(cause);
}
ctx.close();
}
}

View File

@ -1,19 +0,0 @@
package net.minestom.server.network.netty.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import net.minestom.server.network.netty.packet.FramedPacket;
public class GroupedPacketHandler extends MessageToByteEncoder<FramedPacket> {
@Override
protected void encode(ChannelHandlerContext ctx, FramedPacket msg, ByteBuf out) {
}
@Override
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, FramedPacket msg, boolean preferDirect) {
return msg.getBody().retainedSlice();
}
}

View File

@ -1,184 +0,0 @@
package net.minestom.server.network.netty.codec;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.ping.ServerListPingType;
import org.jetbrains.annotations.NotNull;
import java.nio.charset.StandardCharsets;
public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
private ByteBuf buf;
@Override
public void channelRead(@NotNull ChannelHandlerContext ctx, @NotNull Object object) {
final ByteBuf buf = (ByteBuf) object;
if (this.buf != null) {
try {
handle1_6(ctx, buf);
} finally {
buf.release();
}
return;
}
buf.markReaderIndex();
boolean flag = true;
try {
if (buf.readUnsignedByte() == 0xFE) {
int length = buf.readableBytes();
switch (length) {
case 0:
if (trySendResponse(ServerListPingType.LEGACY_UNVERSIONED, ctx)) return;
break;
case 1:
if (buf.readUnsignedByte() != 1) return;
if (trySendResponse(ServerListPingType.LEGACY_VERSIONED, ctx)) return;
break;
default:
if (buf.readUnsignedByte() != 0x01 || buf.readUnsignedByte() != 0xFA) return;
handle1_6(ctx, buf);
break;
}
buf.release();
flag = false;
}
} finally {
if (flag) {
buf.resetReaderIndex();
ctx.channel().pipeline().remove("legacy-ping");
ctx.fireChannelRead(object);
}
}
}
private void handle1_6(ChannelHandlerContext ctx, ByteBuf part) {
ByteBuf buf = this.buf;
if (buf == null) {
this.buf = buf = ctx.alloc().buffer();
buf.markReaderIndex();
} else {
buf.resetReaderIndex();
}
buf.writeBytes(part);
if (!buf.isReadable(Short.BYTES + Short.BYTES + Byte.BYTES + Short.BYTES + Integer.BYTES)) {
return;
}
final String s = readLegacyString(buf);
if (s == null) {
return;
}
if (!s.equals("MC|PingHost")) {
removeHandler(ctx);
return;
}
if (!buf.isReadable(Short.BYTES) || !buf.isReadable(buf.readShort())) {
return;
}
int protocolVersion = buf.readByte();
if (readLegacyString(buf) == null) {
removeHandler(ctx);
return;
}
buf.skipBytes(4); // port
if (buf.isReadable()) {
removeHandler(ctx);
return;
}
buf.release();
this.buf = null;
trySendResponse(ServerListPingType.LEGACY_VERSIONED, ctx);
}
private void removeHandler(ChannelHandlerContext ctx) {
ByteBuf buf = this.buf;
this.buf = null;
buf.resetReaderIndex();
ctx.pipeline().remove(this);
ctx.fireChannelRead(buf);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
if (this.buf != null) {
this.buf.release();
this.buf = null;
}
}
/**
* Calls a {@link ServerListPingEvent} and sends the response, if the event was not cancelled.
*
* @param version the version
* @param ctx the context
* @return {@code true} if the response was cancelled, {@code false} otherwise
*/
private static boolean trySendResponse(@NotNull ServerListPingType version, @NotNull ChannelHandlerContext ctx) {
final ServerListPingEvent event = new ServerListPingEvent(version);
EventDispatcher.call(event);
if (event.isCancelled()) {
return true;
} else {
// get the response string
String s = version.getPingResponse(event.getResponseData());
// create the buffer
ByteBuf response = Unpooled.buffer();
response.writeByte(255);
final char[] chars = s.toCharArray();
response.writeShort(chars.length);
for (char c : chars) {
response.writeChar(c);
}
// write the buffer
ctx.pipeline().firstContext().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
return false;
}
}
private static String readLegacyString(ByteBuf buf) {
int size = buf.readShort() * Character.BYTES;
if (!buf.isReadable(size)) {
return null;
}
final String result = buf.toString(buf.readerIndex(), size, StandardCharsets.UTF_16BE);
buf.skipBytes(size);
return result;
}
}

View File

@ -1,79 +0,0 @@
/*
* Copyright (2020) [artem]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.minestom.server.network.netty.codec;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import io.netty.handler.codec.DecoderException;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.Utils;
import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
public class PacketCompressor extends ByteToMessageCodec<ByteBuf> {
private final static int MAX_SIZE = 2097152;
private final int threshold;
private final Deflater deflater = new Deflater();
private final Inflater inflater = new Inflater();
public PacketCompressor(int threshold) {
this.threshold = threshold;
}
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf from, ByteBuf to) {
PacketUtils.compressBuffer(deflater, from, to);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() != 0) {
final int claimedUncompressedSize = Utils.readVarInt(in);
if (claimedUncompressedSize == 0) {
out.add(in.readRetainedSlice(in.readableBytes()));
} else {
if (claimedUncompressedSize < this.threshold) {
throw new DecoderException("Badly compressed packet - size of " + claimedUncompressedSize + " is below server threshold of " + this.threshold);
}
if (claimedUncompressedSize > MAX_SIZE) {
throw new DecoderException("Badly compressed packet - size of " + claimedUncompressedSize + " is larger than protocol maximum of " + MAX_SIZE);
}
// TODO optimize to do not initialize arrays each time
byte[] input = new byte[in.readableBytes()];
in.readBytes(input);
inflater.setInput(input);
byte[] output = new byte[claimedUncompressedSize];
inflater.inflate(output);
inflater.reset();
out.add(Unpooled.wrappedBuffer(output));
}
}
}
}

View File

@ -1,20 +0,0 @@
package net.minestom.server.network.netty.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import net.minestom.server.network.netty.packet.InboundPacket;
import net.minestom.server.utils.Utils;
import java.util.List;
public class PacketDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) {
if (buf.readableBytes() > 0) {
final int packetId = Utils.readVarInt(buf);
list.add(new InboundPacket(packetId, buf));
}
}
}

View File

@ -1,16 +0,0 @@
package net.minestom.server.network.netty.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.utils.PacketUtils;
public class PacketEncoder extends MessageToByteEncoder<ServerPacket> {
@Override
protected void encode(ChannelHandlerContext ctx, ServerPacket packet, ByteBuf buf) {
PacketUtils.writePacket(buf, packet);
}
}

View File

@ -1,74 +0,0 @@
package net.minestom.server.network.netty.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import io.netty.handler.codec.CorruptedFrameException;
import net.minestom.server.MinecraftServer;
import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class PacketFramer extends ByteToMessageCodec<ByteBuf> {
public final static Logger LOGGER = LoggerFactory.getLogger(PacketFramer.class);
private final PacketProcessor packetProcessor;
public PacketFramer(PacketProcessor packetProcessor) {
this.packetProcessor = packetProcessor;
}
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf from, ByteBuf to) {
PacketUtils.frameBuffer(from, to);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) {
buf.markReaderIndex();
for (int i = 0; i < 3; ++i) {
if (!buf.isReadable()) {
buf.resetReaderIndex();
return;
}
final byte b = buf.readByte();
if (b >= 0) {
buf.resetReaderIndex();
final int packetSize = Utils.readVarInt(buf);
// Max packet size check
if (packetSize >= MinecraftServer.getMaxPacketSize()) {
final PlayerConnection playerConnection = packetProcessor.getPlayerConnection(ctx);
if (playerConnection != null) {
final String identifier = playerConnection.getIdentifier();
LOGGER.warn("An user ({}) sent a packet over the maximum size ({})",
identifier, packetSize);
} else {
LOGGER.warn("An unregistered user sent a packet over the maximum size ({})", packetSize);
}
ctx.close();
}
if (buf.readableBytes() < packetSize) {
buf.resetReaderIndex();
return;
}
out.add(buf.readRetainedSlice(packetSize));
return;
}
}
throw new CorruptedFrameException("length wider than 21-bit");
}
}

View File

@ -1,22 +1,22 @@
package net.minestom.server.network.netty.packet;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
import java.nio.ByteBuffer;
/**
* Represents a packet which is already framed. (packet id+payload) + optional compression
* Can be used if you want to send the exact same buffer to multiple clients without processing it more than once.
*/
public class FramedPacket {
private final ByteBuf body;
private final ByteBuffer body;
public FramedPacket(@NotNull ByteBuf body) {
public FramedPacket(@NotNull ByteBuffer body) {
this.body = body;
}
@NotNull
public ByteBuf getBody() {
public @NotNull ByteBuffer getBody() {
return body;
}
}

View File

@ -1,14 +1,14 @@
package net.minestom.server.network.netty.packet;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
import java.nio.ByteBuffer;
public class InboundPacket {
private final int packetId;
private final ByteBuf body;
private final ByteBuffer body;
public InboundPacket(int id, @NotNull ByteBuf body) {
public InboundPacket(int id, @NotNull ByteBuffer body) {
this.packetId = id;
this.body = body;
}
@ -17,8 +17,7 @@ public class InboundPacket {
return packetId;
}
@NotNull
public ByteBuf getBody() {
public @NotNull ByteBuffer getBody() {
return body;
}
}

View File

@ -1,7 +1,5 @@
package net.minestom.server.network.packet.server.play;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.Int2LongRBTreeMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minestom.server.MinecraftServer;
@ -15,22 +13,17 @@ import net.minestom.server.tag.Tag;
import net.minestom.server.utils.Utils;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.cache.CacheablePacket;
import net.minestom.server.utils.cache.TemporaryPacketCache;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.world.biomes.Biome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import org.jglrxavpok.hephaistos.nbt.NBTException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.TimeUnit;
public class ChunkDataPacket implements ServerPacket, CacheablePacket {
public static final TemporaryPacketCache CACHE = new TemporaryPacketCache(5, TimeUnit.MINUTES);
public class ChunkDataPacket implements ServerPacket {
public Biome[] biomes;
public int chunkX, chunkZ;
@ -42,22 +35,13 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket {
private static final int MAX_BITS_PER_ENTRY = 16;
private static final int MAX_BUFFER_SIZE = (Short.BYTES + Byte.BYTES + 5 * Byte.BYTES + (4096 * MAX_BITS_PER_ENTRY / Long.SIZE * Long.BYTES)) * CHUNK_SECTION_COUNT + 256 * Integer.BYTES;
// Cacheable data
private final UUID identifier;
private final long timestamp;
/**
* Heightmaps NBT, as read from raw packet data.
* Only filled by #read, and unused at the moment.
*/
public NBTCompound heightmapsNBT;
private ChunkDataPacket() {
this(new UUID(0, 0), 0);
}
public ChunkDataPacket(@Nullable UUID identifier, long timestamp) {
this.identifier = identifier;
this.timestamp = timestamp;
public ChunkDataPacket() {
}
@Override
@ -65,7 +49,7 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket {
writer.writeInt(chunkX);
writer.writeInt(chunkZ);
ByteBuf blocks = Unpooled.buffer(MAX_BUFFER_SIZE);
ByteBuffer blocks = ByteBuffer.allocate(MAX_BUFFER_SIZE);
Int2LongRBTreeMap maskMap = new Int2LongRBTreeMap();
@ -120,9 +104,8 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket {
}
// Data
writer.writeVarInt(blocks.writerIndex());
writer.write(blocks);
blocks.release();
writer.writeVarInt(blocks.position());
writer.write(blocks.flip());
// Block entities
if (entries == null || entries.isEmpty()) {
@ -244,19 +227,4 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket {
public int getId() {
return ServerPacketIdentifier.CHUNK_DATA;
}
@Override
public @NotNull TemporaryPacketCache getCache() {
return CACHE;
}
@Override
public UUID getIdentifier() {
return identifier;
}
@Override
public long getTimestamp() {
return timestamp;
}
}

View File

@ -4,19 +4,12 @@ import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.ServerPacketIdentifier;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.cache.CacheablePacket;
import net.minestom.server.utils.cache.TemporaryPacketCache;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class UpdateLightPacket implements ServerPacket, CacheablePacket {
public static final TemporaryPacketCache CACHE = new TemporaryPacketCache(5, TimeUnit.MINUTES);
public class UpdateLightPacket implements ServerPacket {
public int chunkX;
public int chunkZ;
@ -32,21 +25,10 @@ public class UpdateLightPacket implements ServerPacket, CacheablePacket {
public List<byte[]> skyLight = new ArrayList<>();
public List<byte[]> blockLight = new ArrayList<>();
// Cacheable data
private final UUID identifier;
private final long timestamp;
/**
* Default constructor, required for reflection operations.
* This one will make a packet that is not meant to be cached
*/
public UpdateLightPacket() {
this(UUID.randomUUID(), Long.MAX_VALUE);
}
public UpdateLightPacket(@Nullable UUID identifier, long timestamp) {
this.identifier = identifier;
this.timestamp = timestamp;
}
@Override
@ -118,19 +100,4 @@ public class UpdateLightPacket implements ServerPacket, CacheablePacket {
public int getId() {
return ServerPacketIdentifier.UPDATE_LIGHT;
}
@Override
public @NotNull TemporaryPacketCache getCache() {
return CACHE;
}
@Override
public UUID getIdentifier() {
return identifier;
}
@Override
public long getTimestamp() {
return timestamp;
}
}

View File

@ -1,36 +1,36 @@
package net.minestom.server.network.player;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.socket.SocketChannel;
import net.kyori.adventure.translation.GlobalTranslator;
import net.minestom.server.MinecraftServer;
import net.minestom.server.adventure.MinestomAdventure;
import net.minestom.server.entity.PlayerSkin;
import net.minestom.server.extras.mojangAuth.Decrypter;
import net.minestom.server.extras.mojangAuth.Encrypter;
import net.minestom.server.extras.mojangAuth.MojangCrypt;
import net.minestom.server.network.ConnectionState;
import net.minestom.server.network.netty.NettyServer;
import net.minestom.server.network.netty.codec.PacketCompressor;
import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.netty.packet.FramedPacket;
import net.minestom.server.network.netty.packet.InboundPacket;
import net.minestom.server.network.packet.server.ComponentHoldingServerPacket;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.login.SetCompressionPacket;
import net.minestom.server.utils.BufUtils;
import net.minestom.server.network.socket.Server;
import net.minestom.server.network.socket.Worker;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.cache.CacheablePacket;
import net.minestom.server.utils.Utils;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.DataFormatException;
/**
* Represents a networking connection with Netty.
@ -43,8 +43,8 @@ public class NettyPlayerConnection extends PlayerConnection {
private SocketAddress remoteAddress;
private boolean encrypted = false;
private boolean compressed = false;
private volatile boolean encrypted = false;
private volatile boolean compressed = false;
//Could be null. Only used for Mojang Auth
private byte[] nonce = new byte[4];
@ -64,12 +64,89 @@ public class NettyPlayerConnection extends PlayerConnection {
private PlayerSkin bungeeSkin;
private final Object tickBufferLock = new Object();
private volatile ByteBuf tickBuffer = BufUtils.direct();
private final ByteBuffer tickBuffer = ByteBuffer.allocateDirect(Server.SOCKET_BUFFER_SIZE);
private ByteBuffer cacheBuffer;
public NettyPlayerConnection(@NotNull SocketChannel channel) {
super();
this.channel = channel;
this.remoteAddress = channel.remoteAddress();
try {
this.remoteAddress = channel.getRemoteAddress();
} catch (IOException e) {
e.printStackTrace();
}
}
public void processPackets(Worker.Context workerContext, PacketProcessor packetProcessor) {
final var readBuffer = workerContext.readBuffer;
final int limit = readBuffer.limit();
// Read all packets
while (readBuffer.remaining() > 0) {
readBuffer.mark(); // Mark the beginning of the packet
try {
// Read packet
final int packetLength = Utils.readVarInt(readBuffer);
final int packetEnd = readBuffer.position() + packetLength;
if (packetEnd > readBuffer.limit()) {
// Integrity fail
throw new BufferUnderflowException();
}
readBuffer.limit(packetEnd); // Ensure that the reader doesn't exceed packet bound
// Read protocol
var content = workerContext.contentBuffer.clear();
{
if (!compressed) {
// Compression disabled, payload is following
content = readBuffer;
} else {
final int dataLength = Utils.readVarInt(readBuffer);
if (dataLength == 0) {
// Data is too small to be compressed, payload is following
content = readBuffer;
} else {
// Decompress to content buffer
try {
final var inflater = workerContext.inflater;
inflater.setInput(readBuffer);
inflater.inflate(content);
inflater.reset();
} catch (DataFormatException e) {
e.printStackTrace();
}
content.flip();
}
}
}
// Process packet
final int packetId = Utils.readVarInt(content);
try {
packetProcessor.process(this, new InboundPacket(packetId, content));
} catch (Exception e) {
// Error while reading the packet
e.printStackTrace();
break;
}
// Return to original state (before writing)
readBuffer.limit(limit).position(packetEnd);
} catch (BufferUnderflowException e) {
readBuffer.reset();
this.cacheBuffer = ByteBuffer.allocateDirect(readBuffer.remaining());
this.cacheBuffer.put(readBuffer).flip();
break;
}
}
}
public void consumeCache(ByteBuffer buffer) {
if (cacheBuffer == null) {
return;
}
buffer.put(cacheBuffer);
this.cacheBuffer = null;
}
/**
@ -81,10 +158,7 @@ public class NettyPlayerConnection extends PlayerConnection {
public void setEncryptionKey(@NotNull SecretKey secretKey) {
Check.stateCondition(encrypted, "Encryption is already enabled!");
this.encrypted = true;
channel.pipeline().addBefore(NettyServer.GROUPED_PACKET_HANDLER_NAME, NettyServer.DECRYPT_HANDLER_NAME,
new Decrypter(MojangCrypt.getCipher(2, secretKey)));
channel.pipeline().addBefore(NettyServer.GROUPED_PACKET_HANDLER_NAME, NettyServer.ENCRYPT_HANDLER_NAME,
new Encrypter(MojangCrypt.getCipher(1, secretKey)));
// TODO
}
/**
@ -96,11 +170,8 @@ public class NettyPlayerConnection extends PlayerConnection {
Check.stateCondition(compressed, "Compression is already enabled!");
final int threshold = MinecraftServer.getCompressionThreshold();
Check.stateCondition(threshold == 0, "Compression cannot be enabled because the threshold is equal to 0");
this.compressed = true;
writeAndFlush(new SetCompressionPacket(threshold));
channel.pipeline().addAfter(NettyServer.FRAMER_HANDLER_NAME, NettyServer.COMPRESSOR_HANDLER_NAME,
new PacketCompressor(threshold));
}
/**
@ -112,21 +183,12 @@ public class NettyPlayerConnection extends PlayerConnection {
*/
@Override
public void sendPacket(@NotNull ServerPacket serverPacket, boolean skipTranslating) {
if (!channel.isActive())
if (!channel.isConnected())
return;
if (shouldSendPacket(serverPacket)) {
if (getPlayer() != null) {
// Flush happen during #update()
if (serverPacket instanceof CacheablePacket && MinecraftServer.hasPacketCaching()) {
synchronized (tickBufferLock) {
if (tickBuffer.refCnt() == 0)
return;
CacheablePacket.writeCache(tickBuffer, serverPacket);
}
} else {
write(serverPacket, skipTranslating);
}
write(serverPacket, skipTranslating);
} else {
// Player is probably not logged yet
writeAndFlush(serverPacket);
@ -141,93 +203,65 @@ public class NettyPlayerConnection extends PlayerConnection {
public void write(@NotNull Object message, boolean skipTranslating) {
if (message instanceof FramedPacket) {
final FramedPacket framedPacket = (FramedPacket) message;
synchronized (tickBufferLock) {
if (tickBuffer.refCnt() == 0)
return;
final ByteBuf body = framedPacket.getBody();
tickBuffer.writeBytes(body, body.readerIndex(), body.readableBytes());
}
attemptWrite(framedPacket.getBody());
return;
} else if (message instanceof ServerPacket) {
ServerPacket serverPacket = (ServerPacket) message;
if ((MinestomAdventure.AUTOMATIC_COMPONENT_TRANSLATION && !skipTranslating) && getPlayer() != null && serverPacket instanceof ComponentHoldingServerPacket) {
serverPacket = ((ComponentHoldingServerPacket) serverPacket).copyWithOperator(component ->
GlobalTranslator.render(component, Objects.requireNonNullElseGet(getPlayer().getLocale(), MinestomAdventure::getDefaultLocale)));
}
synchronized (tickBufferLock) {
if (tickBuffer.refCnt() == 0)
return;
PacketUtils.writeFramedPacket(tickBuffer, serverPacket);
}
return;
} else if (message instanceof ByteBuf) {
synchronized (tickBufferLock) {
if (tickBuffer.refCnt() == 0)
return;
tickBuffer.writeBytes((ByteBuf) message);
}
} else if (message instanceof ByteBuffer) {
attemptWrite((ByteBuffer) message);
return;
}
throw new UnsupportedOperationException("type " + message.getClass() + " is not supported");
}
public void writeAndFlush(@NotNull Object message) {
writeWaitingPackets();
ChannelFuture channelFuture = channel.writeAndFlush(message);
if (MinecraftServer.shouldProcessNettyErrors()) {
channelFuture.addListener(future -> {
if (!future.isSuccess() && channel.isActive()) {
MinecraftServer.getExceptionManager().handleException(future.cause());
}
});
}
public void writeAndFlush(@NotNull ServerPacket packet) {
attemptWrite(PacketUtils.createFramedPacket(packet));
flush();
}
public void writeWaitingPackets() {
if (tickBuffer.writerIndex() == 0) {
// Nothing to write
return;
}
// Retrieve safe copy
final ByteBuf copy;
public void attemptWrite(ByteBuffer buffer) {
synchronized (tickBufferLock) {
if (tickBuffer.refCnt() == 0)
return;
copy = tickBuffer;
tickBuffer = tickBuffer.alloc().buffer(tickBuffer.writerIndex());
}
// Write copied buffer to netty
ChannelFuture channelFuture = channel.write(new FramedPacket(copy));
channelFuture.addListener(future -> copy.release());
// Netty debug
if (MinecraftServer.shouldProcessNettyErrors()) {
channelFuture.addListener(future -> {
if (!future.isSuccess() && channel.isActive()) {
MinecraftServer.getExceptionManager().handleException(future.cause());
try {
this.tickBuffer.put(buffer);
} catch (BufferOverflowException e) {
try {
this.channel.write(tickBuffer);
this.channel.write(buffer);
} catch (IOException ex) {
MinecraftServer.getExceptionManager().handleException(ex);
} finally {
this.tickBuffer.clear();
}
});
}
}
public void flush() {
final int bufferSize = tickBuffer.writerIndex();
if (bufferSize > 0) {
if (channel.isActive()) {
writeWaitingPackets();
channel.flush();
}
}
}
@NotNull
public void flush() {
if (tickBuffer.remaining() == 0) {
// Nothing to write
return;
}
// Retrieve safe copy
synchronized (tickBufferLock) {
try {
channel.write(tickBuffer);
} catch (IOException e) {
MinecraftServer.getExceptionManager().handleException(e);
}
this.tickBuffer.clear();
}
}
@Override
public SocketAddress getRemoteAddress() {
public @NotNull SocketAddress getRemoteAddress() {
return remoteAddress;
}
@ -246,11 +280,14 @@ public class NettyPlayerConnection extends PlayerConnection {
@Override
public void disconnect() {
refreshOnline(false);
this.channel.close();
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@NotNull
public Channel getChannel() {
public @NotNull SocketChannel getChannel() {
return channel;
}
@ -261,8 +298,7 @@ public class NettyPlayerConnection extends PlayerConnection {
*
* @return the username given by the client, unchecked
*/
@Nullable
public String getLoginUsername() {
public @Nullable String getLoginUsername() {
return loginUsername;
}
@ -323,9 +359,7 @@ public class NettyPlayerConnection extends PlayerConnection {
this.protocolVersion = protocolVersion;
}
@Nullable
public UUID getBungeeUuid() {
public @Nullable UUID getBungeeUuid() {
return bungeeUuid;
}
@ -333,8 +367,7 @@ public class NettyPlayerConnection extends PlayerConnection {
this.bungeeUuid = bungeeUuid;
}
@Nullable
public PlayerSkin getBungeeSkin() {
public @Nullable PlayerSkin getBungeeSkin() {
return bungeeSkin;
}
@ -367,8 +400,7 @@ public class NettyPlayerConnection extends PlayerConnection {
* @param messageId the message id
* @return the channel linked to the message id, null if not found
*/
@Nullable
public String getPluginRequestChannel(int messageId) {
public @Nullable String getPluginRequestChannel(int messageId) {
return pluginRequestMap.get(messageId);
}
@ -382,9 +414,6 @@ public class NettyPlayerConnection extends PlayerConnection {
}
public void releaseTickBuffer() {
synchronized (tickBufferLock) {
tickBuffer.release();
}
}
public byte[] getNonce() {

View File

@ -135,7 +135,7 @@ public abstract class PlayerConnection {
* @return the server address used
*/
public @Nullable String getServerAddress() {
return MinecraftServer.getNettyServer().getAddress();
return MinecraftServer.getServer().getAddress();
}
@ -147,7 +147,7 @@ public abstract class PlayerConnection {
* @return the server port used
*/
public int getServerPort() {
return MinecraftServer.getNettyServer().getPort();
return MinecraftServer.getServer().getPort();
}
/**

View File

@ -0,0 +1,97 @@
package net.minestom.server.network.socket;
import net.minestom.server.network.PacketProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class Server {
public static final Logger LOGGER = LoggerFactory.getLogger(Server.class);
public static final int WORKER_COUNT = Integer.getInteger("minestom.workers",
Runtime.getRuntime().availableProcessors() * 2);
public static final int SOCKET_BUFFER_SIZE = Integer.getInteger("minestom.buffer-size", 262143);
public static final int MAX_PACKET_SIZE = 2097151; // 3 bytes var-int
public static final boolean NO_DELAY = true;
private volatile boolean stop;
private final List<Worker> workers = new ArrayList<>(WORKER_COUNT);
private int index;
private ServerSocketChannel serverSocket;
private String address;
private int port;
public Server(PacketProcessor packetProcessor) throws IOException {
// Create all workers
for (int i = 0; i < WORKER_COUNT; i++) {
this.workers.add(new Worker(this, packetProcessor));
}
}
public void start(SocketAddress address) throws IOException {
Selector selector = Selector.open();
this.serverSocket = ServerSocketChannel.open();
serverSocket.bind(address);
serverSocket.configureBlocking(false);
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
serverSocket.socket().setReceiveBufferSize(SOCKET_BUFFER_SIZE);
LOGGER.info("Server starting, wait for connections");
new Thread(() -> {
while (!stop) {
// Busy wait for connections
try {
serverTick(selector, serverSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
public boolean isOpen() {
return !stop;
}
public void stop() {
this.stop = true;
}
public String getAddress() {
return address;
}
public int getPort() {
return port;
}
private void serverTick(Selector selector, ServerSocketChannel socketChannel) throws IOException {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
for (SelectionKey key : selectedKeys) {
if (key.isAcceptable()) {
// Register socket and forward to thread
Worker thread = findWorker();
final SocketChannel client = socketChannel.accept();
thread.receiveConnection(client);
LOGGER.info("new connection: " + client);
}
}
selectedKeys.clear();
}
private Worker findWorker() {
this.index = ++index % WORKER_COUNT;
return workers.get(index);
}
}

View File

@ -0,0 +1,149 @@
package net.minestom.server.network.socket;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.player.NettyPlayerConnection;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
public class Worker {
private final Map<SocketChannel, NettyPlayerConnection> connectionMap = new ConcurrentHashMap<>();
private final Selector selector = Selector.open();
private final PacketProcessor packetProcessor;
public Worker(Server server, PacketProcessor packetProcessor) throws IOException {
this.packetProcessor = packetProcessor;
Thread.start(server, this::threadTick);
}
private void threadTick(Context workerContext) {
try {
selector.select();
} catch (IOException e) {
e.printStackTrace();
return;
}
Set<SelectionKey> selectedKeys = selector.selectedKeys();
for (SelectionKey key : selectedKeys) {
SocketChannel channel = (SocketChannel) key.channel();
if (!channel.isOpen()) {
continue;
}
if (!key.isReadable()) {
// We only care about read
continue;
}
var connection = connectionMap.get(channel);
try {
ByteBuffer readBuffer = workerContext.readBuffer;
// Consume last incomplete packet
connection.consumeCache(readBuffer);
// Read socket
if (channel.read(readBuffer) == -1) {
// EOS
throw new IOException("Disconnected");
}
// Process data
readBuffer.flip();
connection.processPackets(workerContext, packetProcessor);
} catch (IOException e) {
e.printStackTrace();
try {
disconnect(connection, channel);
} catch (IOException ioException) {
ioException.printStackTrace();
}
} finally {
workerContext.clearBuffers();
}
}
selectedKeys.clear();
}
public void receiveConnection(SocketChannel channel) throws IOException {
var connection = new NettyPlayerConnection(channel);
this.connectionMap.put(channel, connection);
register(channel);
this.selector.wakeup();
}
private void register(SocketChannel channel) throws IOException {
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
var socket = channel.socket();
socket.setSendBufferSize(Server.SOCKET_BUFFER_SIZE);
socket.setReceiveBufferSize(Server.SOCKET_BUFFER_SIZE);
socket.setTcpNoDelay(Server.NO_DELAY);
}
private void disconnect(NettyPlayerConnection connection, SocketChannel channel) throws IOException {
// Client close
channel.close();
connectionMap.remove(channel);
// Remove the connection
connection.refreshOnline(false);
Player player = connection.getPlayer();
if (player != null) {
player.remove();
MinecraftServer.getConnectionManager().removePlayer(connection);
}
}
static class Thread extends java.lang.Thread {
private static final AtomicInteger COUNTER = new AtomicInteger();
private Thread(Runnable runnable) {
super(null, runnable, "worker-" + COUNTER.getAndIncrement());
}
protected static void start(Server server, Consumer<Context> runnable) {
new Thread(() -> {
Context workerContext = new Context();
while (server.isOpen()) {
try {
runnable.accept(workerContext);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
/**
* Contains objects that we can be shared across all the connection of a {@link Worker worker}.
*/
public static final class Context {
public final ByteBuffer readBuffer = allocate(Server.SOCKET_BUFFER_SIZE);
public final ByteBuffer writeBuffer = allocate(Server.SOCKET_BUFFER_SIZE);
/**
* Stores a single packet payload to be read.
*/
public final ByteBuffer contentBuffer = allocate(Server.MAX_PACKET_SIZE);
public final Deflater deflater = new Deflater();
public final Inflater inflater = new Inflater();
public void clearBuffers() {
this.readBuffer.clear();
this.writeBuffer.clear();
}
private static ByteBuffer allocate(int size) {
return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
}
}
}

View File

@ -75,13 +75,13 @@ public enum ServerListPingType {
* @see OpenToLAN
*/
public static @NotNull String getOpenToLANPing(@NotNull ResponseData data) {
return String.format(LAN_PING_FORMAT, SECTION.serialize(data.getDescription()), MinecraftServer.getNettyServer().getPort());
return String.format(LAN_PING_FORMAT, SECTION.serialize(data.getDescription()), MinecraftServer.getServer().getPort());
}
/**
* Creates a legacy ping response for client versions below the Netty rewrite (1.6-).
*
* @param data the response data
* @param data the response data
* @param supportsVersions if the client supports recieving the versions of the server
* @return the response
*/
@ -99,7 +99,7 @@ public enum ServerListPingType {
/**
* Creates a modern ping response for client versions above the Netty rewrite (1.7+).
*
* @param data the response data
* @param data the response data
* @param supportsFullRgb if the client supports full RGB
* @return the response
*/

View File

@ -1,13 +0,0 @@
package net.minestom.server.utils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
public class BufUtils {
private static final PooledByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT;
public static ByteBuf direct() {
return alloc.ioBuffer();
}
}

View File

@ -1,6 +1,5 @@
package net.minestom.server.utils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience;
import net.minestom.server.MinecraftServer;
@ -23,11 +22,10 @@ import java.util.Collection;
import java.util.zip.Deflater;
/**
* Utils class for packets. Including writing a {@link ServerPacket} into a {@link ByteBuf}
* Utils class for packets. Including writing a {@link ServerPacket} into a {@link ByteBuffer}
* for network processing.
*/
public final class PacketUtils {
private static final PacketListenerManager PACKET_LISTENER_MANAGER = MinecraftServer.getPacketListenerManager();
private static final ThreadLocal<Deflater> COMPRESSOR = ThreadLocal.withInitial(Deflater::new);
@ -91,9 +89,8 @@ public final class PacketUtils {
// Send grouped packet...
final boolean success = PACKET_LISTENER_MANAGER.processServerPacket(packet, players);
if (success) {
final ByteBuf finalBuffer = createFramedPacket(packet);
final ByteBuffer finalBuffer = createFramedPacket(packet);
final FramedPacket framedPacket = new FramedPacket(finalBuffer);
// Send packet to all players
for (Player player : players) {
if (!player.isOnline())
@ -110,7 +107,6 @@ public final class PacketUtils {
playerConnection.sendPacket(packet);
}
}
finalBuffer.release(); // Release last reference
}
} else {
// Write the same packet for each individual players
@ -118,7 +114,6 @@ public final class PacketUtils {
// Verify if the player should receive the packet
if (playerValidator != null && !playerValidator.isValid(player))
continue;
final PlayerConnection playerConnection = player.getPlayerConnection();
playerConnection.sendPacket(packet, false);
}
@ -136,23 +131,14 @@ public final class PacketUtils {
}
/**
* Writes a {@link ServerPacket} into a {@link ByteBuf}.
* Writes a {@link ServerPacket} into a {@link ByteBuffer}.
*
* @param buf the recipient of {@code packet}
* @param packet the packet to write into {@code buf}
*/
public static void writePacket(@NotNull ByteBuf buf, @NotNull ServerPacket packet) {
public static void writePacket(@NotNull ByteBuffer buf, @NotNull ServerPacket packet) {
Utils.writeVarInt(buf, packet.getId());
writePacketPayload(buf, packet);
}
/**
* Writes a packet payload.
*
* @param packet the packet to write
*/
private static void writePacketPayload(@NotNull ByteBuf buffer, @NotNull ServerPacket packet) {
BinaryWriter writer = new BinaryWriter(buffer);
BinaryWriter writer = new BinaryWriter(buf);
try {
packet.write(writer);
} catch (Exception e) {
@ -160,96 +146,42 @@ public final class PacketUtils {
}
}
/**
* Frames a buffer for it to be understood by a Minecraft client.
* <p>
* The content of {@code packetBuffer} can be either a compressed or uncompressed packet buffer,
* it depends of it the client did receive a {@link net.minestom.server.network.packet.server.login.SetCompressionPacket} packet before.
*
* @param packetBuffer the buffer containing compressed or uncompressed packet data
* @param frameTarget the buffer which will receive the framed version of {@code from}
*/
public static void frameBuffer(@NotNull ByteBuf packetBuffer, @NotNull ByteBuf frameTarget) {
final int packetSize = packetBuffer.readableBytes();
final int headerSize = Utils.getVarIntSize(packetSize);
if (headerSize > 3) {
throw new IllegalStateException("Unable to fit " + headerSize + " into 3");
}
frameTarget.ensureWritable(packetSize + headerSize);
Utils.writeVarInt(frameTarget, packetSize);
frameTarget.writeBytes(packetBuffer, packetBuffer.readerIndex(), packetSize);
}
/**
* Compress using zlib the content of a packet.
* <p>
* {@code packetBuffer} needs to be the packet content without any header (if you want to use it to write a Minecraft packet).
*
* @param deflater the deflater for zlib compression
* @param packetBuffer the buffer containing all the packet fields
* @param compressionTarget the buffer which will receive the compressed version of {@code packetBuffer}
*/
public static void compressBuffer(@NotNull Deflater deflater, @NotNull ByteBuf packetBuffer, @NotNull ByteBuf compressionTarget) {
final int packetLength = packetBuffer.readableBytes();
final boolean compression = packetLength > MinecraftServer.getCompressionThreshold();
Utils.writeVarInt(compressionTarget, compression ? packetLength : 0);
if (compression) {
compress(deflater, packetBuffer, compressionTarget);
} else {
compressionTarget.writeBytes(packetBuffer);
}
}
private static void compress(@NotNull Deflater deflater, @NotNull ByteBuf uncompressed, @NotNull ByteBuf compressed) {
deflater.setInput(uncompressed.nioBuffer());
deflater.finish();
while (!deflater.finished()) {
ByteBuffer nioBuffer = compressed.nioBuffer(compressed.writerIndex(), compressed.writableBytes());
compressed.writerIndex(deflater.deflate(nioBuffer) + compressed.writerIndex());
if (compressed.writableBytes() == 0) {
compressed.ensureWritable(8192);
}
}
deflater.reset();
}
public static void writeFramedPacket(@NotNull ByteBuf buffer,
public static void writeFramedPacket(@NotNull ByteBuffer buffer,
@NotNull ServerPacket serverPacket) {
final int compressionThreshold = MinecraftServer.getCompressionThreshold();
// Index of the var-int containing the complete packet length
final int packetLengthIndex = Utils.writeEmpty3BytesVarInt(buffer);
final int startIndex = buffer.writerIndex(); // Index where the content starts (after length)
final int packetLengthIndex = Utils.writeEmptyVarIntHeader(buffer);
final int startIndex = buffer.position(); // Index where the content starts (after length)
if (compressionThreshold > 0) {
// Index of the uncompressed payload length
final int dataLengthIndex = Utils.writeEmpty3BytesVarInt(buffer);
final int dataLengthIndex = Utils.writeEmptyVarIntHeader(buffer);
// Write packet
final int contentIndex = buffer.writerIndex();
final int contentIndex = buffer.position();
writePacket(buffer, serverPacket);
final int packetSize = buffer.writerIndex() - contentIndex;
final int packetSize = buffer.position() - contentIndex;
final int uncompressedLength = packetSize >= compressionThreshold ? packetSize : 0;
Utils.write3BytesVarInt(buffer, dataLengthIndex, uncompressedLength);
Utils.writeVarIntHeader(buffer, dataLengthIndex, uncompressedLength);
if (uncompressedLength > 0) {
// Packet large enough, compress
ByteBuf uncompressedCopy = buffer.copy(contentIndex, packetSize);
buffer.writerIndex(contentIndex);
compress(COMPRESSOR.get(), uncompressedCopy, buffer);
uncompressedCopy.release();
ByteBuffer uncompressedCopy = buffer.duplicate().position(contentIndex).limit(contentIndex + packetSize);
buffer.position(contentIndex);
var deflater = COMPRESSOR.get();
deflater.setInput(uncompressedCopy);
deflater.finish();
deflater.deflate(buffer);
deflater.reset();
}
} else {
// No compression, write packet id + payload
writePacket(buffer, serverPacket);
}
// Total length
final int totalPacketLength = buffer.writerIndex() - startIndex;
Utils.write3BytesVarInt(buffer, packetLengthIndex, totalPacketLength);
final int totalPacketLength = buffer.position() - startIndex;
Utils.writeVarIntHeader(buffer, packetLengthIndex, totalPacketLength);
}
/**
@ -259,8 +191,8 @@ public final class PacketUtils {
* Can be used if you want to store a raw buffer and send it later without the additional writing cost.
* Compression is applied if {@link MinecraftServer#getCompressionThreshold()} is greater than 0.
*/
public static @NotNull ByteBuf createFramedPacket(@NotNull ServerPacket serverPacket) {
ByteBuf packetBuf = BufUtils.direct();
public static @NotNull ByteBuffer createFramedPacket(@NotNull ServerPacket serverPacket) {
ByteBuffer packetBuf = ByteBuffer.allocate(2_000_000);
writeFramedPacket(packetBuf, serverPacket);
return packetBuf;
}

View File

@ -1,13 +1,15 @@
package net.minestom.server.utils;
import io.netty.buffer.ByteBuf;
import it.unimi.dsi.fastutil.shorts.Short2ShortLinkedOpenHashMap;
import net.minestom.server.instance.palette.Palette;
import net.minestom.server.utils.binary.BinaryWriter;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.nio.ByteBuffer;
import java.util.UUID;
@ApiStatus.Internal
public final class Utils {
private Utils() {
@ -21,43 +23,46 @@ public final class Utils {
? 4 : 5;
}
public static void writeVarInt(@NotNull ByteBuf buf, int value) {
// Took from velocity
public static void writeVarInt(ByteBuffer buf, int value) {
if ((value & (0xFFFFFFFF << 7)) == 0) {
buf.writeByte(value);
buf.put((byte) value);
} else if ((value & (0xFFFFFFFF << 14)) == 0) {
int w = (value & 0x7F | 0x80) << 8 | (value >>> 7);
buf.writeShort(w);
buf.putShort((short) ((value & 0x7F | 0x80) << 8 | (value >>> 7)));
} else if ((value & (0xFFFFFFFF << 21)) == 0) {
int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
buf.writeMedium(w);
buf.put((byte) (value & 0x7F | 0x80));
buf.put((byte) ((value >>> 7) & 0x7F | 0x80));
buf.put((byte) (value >>> 14));
} else if ((value & (0xFFFFFFFF << 28)) == 0) {
buf.putInt((value & 0x7F | 0x80) << 24 | (((value >>> 7) & 0x7F | 0x80) << 16)
| ((value >>> 14) & 0x7F | 0x80) << 8 | (value >>> 21));
} else {
int w = (value & 0x7F | 0x80) << 24 | ((value >>> 7) & 0x7F | 0x80) << 16
| ((value >>> 14) & 0x7F | 0x80) << 8 | ((value >>> 21) & 0x7F | 0x80);
buf.writeInt(w);
buf.writeByte(value >>> 28);
buf.putInt((value & 0x7F | 0x80) << 24 | ((value >>> 7) & 0x7F | 0x80) << 16
| ((value >>> 14) & 0x7F | 0x80) << 8 | ((value >>> 21) & 0x7F | 0x80));
buf.put((byte) (value >>> 28));
}
}
public static void write3BytesVarInt(@NotNull ByteBuf buffer, int startIndex, int value) {
final int indexCache = buffer.writerIndex();
buffer.writerIndex(startIndex);
final int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
buffer.writeMedium(w);
buffer.writerIndex(indexCache);
public static void writeVarIntHeader(@NotNull ByteBuffer buffer, int startIndex, int value) {
final int indexCache = buffer.position();
buffer.position(startIndex);
buffer.put((byte) (value & 0x7F | 0x80));
buffer.put((byte) ((value >>> 7) & 0x7F | 0x80));
buffer.put((byte) (value >>> 14));
buffer.position(indexCache);
}
public static int writeEmpty3BytesVarInt(@NotNull ByteBuf buffer) {
final int index = buffer.writerIndex();
buffer.writeMedium(0);
public static int writeEmptyVarIntHeader(@NotNull ByteBuffer buffer) {
final int index = buffer.position();
buffer.putShort((short) 0);
buffer.put((byte) 0);
return index;
}
public static int readVarInt(ByteBuf buf) {
public static int readVarInt(ByteBuffer buf) {
int i = 0;
final int maxRead = Math.min(5, buf.readableBytes());
final int maxRead = Math.min(5, buf.remaining());
for (int j = 0; j < maxRead; j++) {
final int k = buf.readByte();
final int k = buf.get();
i |= (k & 0x7F) << j * 7;
if ((k & 0x80) != 128) {
return i;
@ -66,12 +71,12 @@ public final class Utils {
throw new RuntimeException("VarInt is too big");
}
public static long readVarLong(@NotNull ByteBuf buffer) {
public static long readVarLong(@NotNull ByteBuffer buffer) {
int numRead = 0;
long result = 0;
byte read;
do {
read = buffer.readByte();
read = buffer.get();
long value = (read & 0b01111111);
result |= (value << (7 * numRead));
@ -117,13 +122,12 @@ public final class Utils {
return new UUID(uuidMost, uuidLeast);
}
public static void writePaletteBlocks(ByteBuf buffer, Palette palette) {
public static void writePaletteBlocks(ByteBuffer buffer, Palette palette) {
final short blockCount = palette.getBlockCount();
final int bitsPerEntry = palette.getBitsPerEntry();
buffer.writeShort(blockCount);
buffer.writeByte((byte) bitsPerEntry);
buffer.putShort(blockCount);
buffer.put((byte) bitsPerEntry);
// Palette
if (bitsPerEntry < 9) {
@ -138,7 +142,7 @@ public final class Utils {
final long[] blocks = palette.getBlocks();
writeVarInt(buffer, blocks.length);
for (long datum : blocks) {
buffer.writeLong(datum);
buffer.putLong(datum);
}
}

View File

@ -1,7 +1,5 @@
package net.minestom.server.utils.binary;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.minestom.server.coordinate.Point;
@ -17,6 +15,7 @@ import org.jglrxavpok.hephaistos.nbt.NBTReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.Supplier;
@ -27,16 +26,15 @@ import java.util.function.Supplier;
* WARNING: not thread-safe.
*/
public class BinaryReader extends InputStream {
private final ByteBuf buffer;
private final ByteBuffer buffer;
private final NBTReader nbtReader = new NBTReader(this, false);
public BinaryReader(@NotNull ByteBuf buffer) {
public BinaryReader(@NotNull ByteBuffer buffer) {
this.buffer = buffer;
}
public BinaryReader(byte[] bytes) {
this(Unpooled.wrappedBuffer(bytes));
this(ByteBuffer.wrap(bytes));
}
public int readVarInt() {
@ -48,49 +46,49 @@ public class BinaryReader extends InputStream {
}
public boolean readBoolean() {
return buffer.readBoolean();
return buffer.get() == 1;
}
public byte readByte() {
return buffer.readByte();
return buffer.get();
}
public short readShort() {
return buffer.readShort();
return buffer.getShort();
}
public char readChar() {
return buffer.readChar();
return buffer.getChar();
}
public int readUnsignedShort() {
return buffer.readUnsignedShort();
return buffer.getShort() & 0xFFFF;
}
/**
* Same as readInt
*/
public int readInteger() {
return buffer.readInt();
return buffer.getInt();
}
/**
* Same as readInteger, created for parity with BinaryWriter
*/
public int readInt() {
return buffer.readInt();
return buffer.getInt();
}
public long readLong() {
return buffer.readLong();
return buffer.getLong();
}
public float readFloat() {
return buffer.readFloat();
return buffer.getFloat();
}
public double readDouble() {
return buffer.readDouble();
return buffer.getDouble();
}
/**
@ -105,12 +103,9 @@ public class BinaryReader extends InputStream {
*/
public String readSizedString(int maxLength) {
final int length = readVarInt();
Check.stateCondition(!buffer.isReadable(length),
"Trying to read a string that is too long (wanted {0}, only have {1})",
length,
buffer.readableBytes());
final String str = buffer.toString(buffer.readerIndex(), length, StandardCharsets.UTF_8);
buffer.skipBytes(length);
byte[] bytes = new byte[length];
buffer.get(bytes);
final String str = new String(bytes, StandardCharsets.UTF_8);
Check.stateCondition(str.length() > maxLength,
"String length ({0}) was higher than the max length of {1}", length, maxLength);
return str;
@ -121,10 +116,8 @@ public class BinaryReader extends InputStream {
}
public byte[] readBytes(int length) {
ByteBuf buf = buffer.readBytes(length);
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
buf.release();
byte[] bytes = new byte[length];
buffer.get(bytes);
return bytes;
}
@ -164,14 +157,11 @@ public class BinaryReader extends InputStream {
}
public Point readBlockPosition() {
final long value = buffer.readLong();
return SerializerUtils.longToBlockPosition(value);
return SerializerUtils.longToBlockPosition(buffer.getLong());
}
public UUID readUuid() {
final long most = readLong();
final long least = readLong();
return new UUID(most, least);
return new UUID(readLong(), readLong());
}
/**
@ -225,7 +215,7 @@ public class BinaryReader extends InputStream {
return (T[]) result;
}
public ByteBuf getBuffer() {
public ByteBuffer getBuffer() {
return buffer;
}
@ -236,7 +226,7 @@ public class BinaryReader extends InputStream {
@Override
public int available() {
return buffer.readableBytes();
return buffer.remaining();
}
public NBT readTag() throws IOException, NBTException {
@ -251,11 +241,12 @@ public class BinaryReader extends InputStream {
* @param extractor the extraction code, simply call the reader's read* methods here.
*/
public byte[] extractBytes(Runnable extractor) {
int startingPosition = getBuffer().readerIndex();
int startingPosition = buffer.position();
extractor.run();
int endingPosition = getBuffer().readerIndex();
int endingPosition = getBuffer().position();
byte[] output = new byte[endingPosition - startingPosition];
getBuffer().getBytes(startingPosition, output);
buffer.get(output, 0, output.length);
//buffer.get(startingPosition, output);
return output;
}
}

View File

@ -1,8 +1,5 @@
package net.minestom.server.utils.binary;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.minestom.server.MinecraftServer;
@ -16,8 +13,11 @@ import org.jglrxavpok.hephaistos.nbt.NBTWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.UUID;
import java.util.function.Consumer;
@ -26,8 +26,7 @@ import java.util.function.Consumer;
* WARNING: not thread-safe.
*/
public class BinaryWriter extends OutputStream {
private ByteBuf buffer;
private ByteBuffer buffer;
private NBTWriter nbtWriter; // Lazily initialized
/**
@ -36,7 +35,7 @@ public class BinaryWriter extends OutputStream {
* @param initialCapacity the initial capacity of the binary writer
*/
public BinaryWriter(int initialCapacity) {
this.buffer = Unpooled.buffer(initialCapacity);
this.buffer = ByteBuffer.allocate(initialCapacity);
}
/**
@ -44,24 +43,15 @@ public class BinaryWriter extends OutputStream {
*
* @param buffer the writer buffer
*/
public BinaryWriter(@NotNull ByteBuf buffer) {
public BinaryWriter(@NotNull ByteBuffer buffer) {
this.buffer = buffer;
}
/**
* Creates a {@link BinaryWriter} from multiple buffers.
*
* @param buffers the buffers making this
*/
public BinaryWriter(@NotNull ByteBuf... buffers) {
this.buffer = Unpooled.wrappedBuffer(buffers);
}
/**
* Creates a {@link BinaryWriter} with a "reasonably small initial capacity".
*/
public BinaryWriter() {
this.buffer = Unpooled.buffer();
this(500); // TODO prevent OOB
}
/**
@ -79,7 +69,7 @@ public class BinaryWriter extends OutputStream {
* @param b the boolean to write
*/
public void writeBoolean(boolean b) {
buffer.writeBoolean(b);
buffer.put((byte) (b ? 1 : 0));
}
/**
@ -88,7 +78,7 @@ public class BinaryWriter extends OutputStream {
* @param b the byte to write
*/
public void writeByte(byte b) {
buffer.writeByte(b);
buffer.put(b);
}
/**
@ -97,7 +87,7 @@ public class BinaryWriter extends OutputStream {
* @param c the char to write
*/
public void writeChar(char c) {
buffer.writeChar(c);
buffer.putChar(c);
}
/**
@ -106,7 +96,7 @@ public class BinaryWriter extends OutputStream {
* @param s the short to write
*/
public void writeShort(short s) {
buffer.writeShort(s);
buffer.putShort(s);
}
/**
@ -115,7 +105,7 @@ public class BinaryWriter extends OutputStream {
* @param i the int to write
*/
public void writeInt(int i) {
buffer.writeInt(i);
buffer.putInt(i);
}
/**
@ -124,7 +114,7 @@ public class BinaryWriter extends OutputStream {
* @param l the long to write
*/
public void writeLong(long l) {
buffer.writeLong(l);
buffer.putLong(l);
}
/**
@ -133,7 +123,7 @@ public class BinaryWriter extends OutputStream {
* @param f the float to write
*/
public void writeFloat(float f) {
buffer.writeFloat(f);
buffer.putFloat(f);
}
/**
@ -142,7 +132,7 @@ public class BinaryWriter extends OutputStream {
* @param d the double to write
*/
public void writeDouble(double d) {
buffer.writeDouble(d);
buffer.putDouble(d);
}
/**
@ -171,9 +161,9 @@ public class BinaryWriter extends OutputStream {
* @param string the string to write
*/
public void writeSizedString(@NotNull String string) {
final int utf8Bytes = ByteBufUtil.utf8Bytes(string);
writeVarInt(utf8Bytes);
buffer.writeCharSequence(string, StandardCharsets.UTF_8);
final var bytes = string.getBytes(StandardCharsets.UTF_8);
writeVarInt(bytes.length);
writeBytes(bytes);
}
/**
@ -184,7 +174,8 @@ public class BinaryWriter extends OutputStream {
* @param charset the charset to encode in
*/
public void writeNullTerminatedString(@NotNull String string, @NotNull Charset charset) {
buffer.writeCharSequence(string + '\0', charset);
final var bytes = (string + '\0').getBytes(charset);
writeBytes(bytes);
}
/**
@ -223,8 +214,8 @@ public class BinaryWriter extends OutputStream {
*
* @param bytes the byte array to write
*/
public void writeBytes(@NotNull byte[] bytes) {
buffer.writeBytes(bytes);
public void writeBytes(byte @NotNull [] bytes) {
buffer.put(bytes);
}
/**
@ -296,12 +287,12 @@ public class BinaryWriter extends OutputStream {
writeable.write(this);
}
public void write(@NotNull BinaryWriter writer) {
this.buffer.writeBytes(writer.getBuffer());
public void write(@NotNull ByteBuffer buffer) {
this.buffer.put(buffer);
}
public void write(@NotNull ByteBuf buffer) {
this.buffer.writeBytes(buffer);
public void write(@NotNull BinaryWriter writer) {
write(writer.getBuffer());
}
/**
@ -323,46 +314,51 @@ public class BinaryWriter extends OutputStream {
* @return the byte array containing all the {@link BinaryWriter} data
*/
public byte[] toByteArray() {
byte[] bytes = new byte[buffer.readableBytes()];
final int readerIndex = buffer.readerIndex();
buffer.getBytes(readerIndex, bytes);
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
return bytes;
}
/**
* Adds a {@link BinaryWriter}'s {@link ByteBuf} at the beginning of this writer.
* Adds a {@link BinaryWriter}'s {@link ByteBuffer} at the beginning of this writer.
*
* @param headerWriter the {@link BinaryWriter} to add at the beginning
*/
public void writeAtStart(@NotNull BinaryWriter headerWriter) {
// Get the buffer of the header
final ByteBuf headerBuf = headerWriter.getBuffer();
final var headerBuf = headerWriter.getBuffer();
// Merge both the headerBuf and this buffer
final ByteBuf finalBuffer = Unpooled.wrappedBuffer(headerBuf, buffer);
final var finalBuffer = concat(headerBuf, buffer);
// Change the buffer used by this writer
setBuffer(finalBuffer);
}
/**
* Adds a {@link BinaryWriter}'s {@link ByteBuf} at the end of this writer.
* Adds a {@link BinaryWriter}'s {@link ByteBuffer} at the end of this writer.
*
* @param footerWriter the {@link BinaryWriter} to add at the end
*/
public void writeAtEnd(@NotNull BinaryWriter footerWriter) {
// Get the buffer of the footer
final ByteBuf footerBuf = footerWriter.getBuffer();
final var footerBuf = footerWriter.getBuffer();
// Merge both this buffer and the footerBuf
final ByteBuf finalBuffer = Unpooled.wrappedBuffer(buffer, footerBuf);
final var finalBuffer = concat(buffer, footerBuf);
// Change the buffer used by this writer
setBuffer(finalBuffer);
}
public static ByteBuffer concat(final ByteBuffer... buffers) {
final ByteBuffer combined = ByteBuffer.allocate(Arrays.stream(buffers).mapToInt(Buffer::remaining).sum());
Arrays.stream(buffers).forEach(b -> combined.put(b.duplicate()));
return combined;
}
/**
* Gets the raw buffer used by this binary writer.
*
* @return the raw buffer
*/
public @NotNull ByteBuf getBuffer() {
public @NotNull ByteBuffer getBuffer() {
return buffer;
}
@ -371,7 +367,7 @@ public class BinaryWriter extends OutputStream {
*
* @param buffer the new buffer used by this binary writer
*/
public void setBuffer(ByteBuf buffer) {
public void setBuffer(ByteBuffer buffer) {
this.buffer = buffer;
}
@ -381,7 +377,8 @@ public class BinaryWriter extends OutputStream {
}
public void writeUnsignedShort(int yourShort) {
buffer.writeShort(yourShort & 0xFFFF);
// FIXME unsigned
buffer.putShort((short) (yourShort & 0xFFFF));
}
/**

View File

@ -1,91 +0,0 @@
package net.minestom.server.utils.cache;
import io.netty.buffer.ByteBuf;
import net.minestom.server.network.netty.packet.FramedPacket;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.utils.PacketUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
/**
* Implemented by {@link ServerPacket server packets} which can be temporary cached in memory to be re-sent later
* without having to go through all the writing and compression.
* <p>
* {@link #getIdentifier()} is to differentiate this packet from the others of the same type.
*/
public interface CacheablePacket {
/**
* Gets the cache linked to this packet.
* <p>
* WARNING: the cache needs to be shared between all the object instances, tips is to make it static.
*
* @return the temporary packet cache
*/
@NotNull TemporaryPacketCache getCache();
/**
* Gets the identifier of this packet.
* <p>
* Used to verify if this packet is already cached or not.
*
* @return this packet identifier, null to prevent caching
*/
@Nullable UUID getIdentifier();
/**
* Gets the last time this packet changed.
*
* @return the last packet update time in milliseconds
*/
long getTimestamp();
static @Nullable FramedPacket getCache(@NotNull ServerPacket serverPacket) {
if (!(serverPacket instanceof CacheablePacket))
return null;
final CacheablePacket cacheablePacket = (CacheablePacket) serverPacket;
final UUID identifier = cacheablePacket.getIdentifier();
if (identifier == null) {
// This packet explicitly asks to do not retrieve the cache
return null;
} else {
final long timestamp = cacheablePacket.getTimestamp();
// Try to retrieve the cached buffer
TemporaryCache<TimedBuffer> temporaryCache = cacheablePacket.getCache();
TimedBuffer timedBuffer = temporaryCache.retrieve(identifier);
// Update the buffer if non-existent or outdated
final boolean shouldUpdate = timedBuffer == null ||
timestamp > timedBuffer.getTimestamp();
if (shouldUpdate) {
// Buffer freed by guava cache #removalListener
final ByteBuf buffer = PacketUtils.createFramedPacket(serverPacket);
timedBuffer = new TimedBuffer(buffer, timestamp);
temporaryCache.cache(identifier, timedBuffer);
}
return new FramedPacket(timedBuffer.getBuffer());
}
}
static void writeCache(@NotNull ByteBuf buffer, @NotNull ServerPacket serverPacket) {
FramedPacket framedPacket = CacheablePacket.getCache(serverPacket);
if (framedPacket == null) {
PacketUtils.writeFramedPacket(buffer, serverPacket);
return;
}
final ByteBuf body = framedPacket.getBody();
synchronized (body) {
if (framedPacket.getBody().refCnt() != 0) {
buffer.writeBytes(body, body.readerIndex(), body.readableBytes());
} else {
PacketUtils.writeFramedPacket(buffer, serverPacket);
}
}
}
}

View File

@ -1,57 +0,0 @@
package net.minestom.server.utils.cache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Cache objects with a timeout.
*
* @param <T> the object type to cache
*/
public class TemporaryCache<T> {
private final Cache<UUID, T> cache;
/**
* Creates a new temporary cache.
*
* @param duration the time before considering an object unused
*/
public TemporaryCache(long duration, TimeUnit timeUnit, RemovalListener<UUID, T> removalListener) {
this.cache = Caffeine.newBuilder()
.expireAfterWrite(duration, timeUnit)
.removalListener(removalListener)
.build();
}
/**
* Caches an object.
*
* @param identifier the object identifier
* @param value the object to cache
*/
public void cache(@NotNull UUID identifier, T value) {
this.cache.put(identifier, value);
}
public void invalidate(@NotNull UUID identifier) {
this.cache.invalidate(identifier);
}
/**
* Retrieves an object from cache.
*
* @param identifier the object identifier
* @return the retrieved object or null if not found
*/
@Nullable
public T retrieve(@NotNull UUID identifier) {
return cache.getIfPresent(identifier);
}
}

View File

@ -1,18 +0,0 @@
package net.minestom.server.utils.cache;
import io.netty.buffer.ByteBuf;
import java.util.concurrent.TimeUnit;
public class TemporaryPacketCache extends TemporaryCache<TimedBuffer> {
public TemporaryPacketCache(long duration, TimeUnit timeUnit) {
super(duration, timeUnit, (key, value, cause) -> {
if (value == null)
return;
final ByteBuf buffer = value.getBuffer();
synchronized (buffer) {
buffer.release();
}
});
}
}

View File

@ -1,28 +0,0 @@
package net.minestom.server.utils.cache;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
/**
* Object containing a {@link ByteBuf buffer} and its timestamp.
* Used for packet-caching to use the most recent.
*/
public class TimedBuffer {
private final ByteBuf buffer;
private final long timestamp;
public TimedBuffer(@NotNull ByteBuf buffer, long timestamp) {
this.buffer = buffer;
this.timestamp = timestamp;
}
@NotNull
public ByteBuf getBuffer() {
return buffer;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@ -24,6 +24,7 @@ public class Main {
public static void main(String[] args) {
MinecraftServer minecraftServer = MinecraftServer.init();
MinecraftServer.setCompressionThreshold(0);
BlockManager blockManager = MinecraftServer.getBlockManager();