diff --git a/L2J_Mobius_01.0_Ertheia/dist/game/config/Server.ini b/L2J_Mobius_01.0_Ertheia/dist/game/config/Server.ini index f3495d50a4..0d01ab8854 100644 --- a/L2J_Mobius_01.0_Ertheia/dist/game/config/Server.ini +++ b/L2J_Mobius_01.0_Ertheia/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/Config.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/Config.java index fabb8c3fd8..4b2e5c2271 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/Config.java @@ -768,6 +768,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1362,6 +1363,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/GameServer.java index 8e65d96198..08cf7fdee5 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/GameServer.java @@ -456,6 +456,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index f9c690b7b6..65cd487eb9 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 73ed0eac95..0e206f2d84 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_02.5_Underground/dist/game/config/Server.ini b/L2J_Mobius_02.5_Underground/dist/game/config/Server.ini index 5c253df7d4..0a553fcfe1 100644 --- a/L2J_Mobius_02.5_Underground/dist/game/config/Server.ini +++ b/L2J_Mobius_02.5_Underground/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/Config.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/Config.java index 4c87d0ee20..1166dfa9f5 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/Config.java @@ -779,6 +779,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1374,6 +1375,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/GameServer.java index 918ea198bf..bf20c50def 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/GameServer.java @@ -464,6 +464,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index f9c690b7b6..65cd487eb9 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 73ed0eac95..0e206f2d84 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_03.0_Helios/dist/game/config/Server.ini b/L2J_Mobius_03.0_Helios/dist/game/config/Server.ini index 7249969504..37eecbe9c1 100644 --- a/L2J_Mobius_03.0_Helios/dist/game/config/Server.ini +++ b/L2J_Mobius_03.0_Helios/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/Config.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/Config.java index 66ee8dd92a..c00e6cfa5b 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/Config.java @@ -780,6 +780,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1387,6 +1388,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/GameServer.java index 918ea198bf..bf20c50def 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/GameServer.java @@ -464,6 +464,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index f9c690b7b6..65cd487eb9 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 73ed0eac95..0e206f2d84 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_04.0_GrandCrusade/dist/game/config/Server.ini b/L2J_Mobius_04.0_GrandCrusade/dist/game/config/Server.ini index c4f49826a5..5a290ea9e0 100644 --- a/L2J_Mobius_04.0_GrandCrusade/dist/game/config/Server.ini +++ b/L2J_Mobius_04.0_GrandCrusade/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/Config.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/Config.java index 2ce3ce4746..54e8552543 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/Config.java @@ -767,6 +767,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1374,6 +1375,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/GameServer.java index 418bbe9387..7c5c8ae2d2 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/GameServer.java @@ -464,6 +464,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index f9c690b7b6..65cd487eb9 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 73ed0eac95..0e206f2d84 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_05.0_Salvation/dist/game/config/Server.ini b/L2J_Mobius_05.0_Salvation/dist/game/config/Server.ini index 603214cce8..351db77ee2 100644 --- a/L2J_Mobius_05.0_Salvation/dist/game/config/Server.ini +++ b/L2J_Mobius_05.0_Salvation/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/Config.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/Config.java index 80b768dd82..5372d44720 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/Config.java @@ -766,6 +766,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1373,6 +1374,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/GameServer.java index 080bb8245b..f720f73e48 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/GameServer.java @@ -468,6 +468,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index f9c690b7b6..65cd487eb9 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 8d0b312d60..6af5184665 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -117,12 +117,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_05.5_EtinasFate/dist/game/config/Server.ini b/L2J_Mobius_05.5_EtinasFate/dist/game/config/Server.ini index ba0e8cfb61..be8f69bbbd 100644 --- a/L2J_Mobius_05.5_EtinasFate/dist/game/config/Server.ini +++ b/L2J_Mobius_05.5_EtinasFate/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/Config.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/Config.java index 5982d38398..525e02c75b 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/Config.java @@ -766,6 +766,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1380,6 +1381,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/GameServer.java index 080bb8245b..f720f73e48 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/GameServer.java @@ -468,6 +468,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index f9c690b7b6..65cd487eb9 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_06.0_Fafurion/dist/game/config/Server.ini b/L2J_Mobius_06.0_Fafurion/dist/game/config/Server.ini index a0905ca306..ea7a706047 100644 --- a/L2J_Mobius_06.0_Fafurion/dist/game/config/Server.ini +++ b/L2J_Mobius_06.0_Fafurion/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/Config.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/Config.java index a2a9eecdaf..06b4f4a63d 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/Config.java @@ -767,6 +767,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1401,6 +1402,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/GameServer.java index 2d8f82f8b7..9073e4ef38 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/GameServer.java @@ -470,6 +470,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index f9c690b7b6..65cd487eb9 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_07.0_PreludeOfWar/dist/game/config/Server.ini b/L2J_Mobius_07.0_PreludeOfWar/dist/game/config/Server.ini index 9458713827..a7defe752a 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/dist/game/config/Server.ini +++ b/L2J_Mobius_07.0_PreludeOfWar/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/Config.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/Config.java index 2a971725df..f2a4402cea 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/Config.java @@ -773,6 +773,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1409,6 +1410,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/GameServer.java index 09f5a020ec..1a1b20df72 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/GameServer.java @@ -476,6 +476,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index bcd0909d31..1b099ea10a 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -202,8 +202,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_08.2_Homunculus/dist/game/config/Server.ini b/L2J_Mobius_08.2_Homunculus/dist/game/config/Server.ini index a8be453ec3..d4fef02bec 100644 --- a/L2J_Mobius_08.2_Homunculus/dist/game/config/Server.ini +++ b/L2J_Mobius_08.2_Homunculus/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/Config.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/Config.java index 55f993e3cc..d762e22de4 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/Config.java @@ -764,6 +764,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1400,6 +1401,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/GameServer.java index f67e84a957..5fc7825ca4 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/GameServer.java @@ -480,6 +480,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index bcd0909d31..1b099ea10a 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -202,8 +202,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/config/Server.ini b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/config/Server.ini index 2cf69b2e80..5b6f5b59a8 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/config/Server.ini +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/Config.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/Config.java index d5f8d380ef..c8f903fdd7 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/Config.java @@ -764,6 +764,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1402,6 +1403,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/GameServer.java index 2a300910be..183066734d 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/GameServer.java @@ -484,6 +484,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index bcd0909d31..1b099ea10a 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -202,8 +202,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_10.2_MasterClass/dist/game/config/Server.ini b/L2J_Mobius_10.2_MasterClass/dist/game/config/Server.ini index 479a65828a..d6ba95f7fa 100644 --- a/L2J_Mobius_10.2_MasterClass/dist/game/config/Server.ini +++ b/L2J_Mobius_10.2_MasterClass/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/Config.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/Config.java index 0f7a2b66d6..acbdf6d325 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/Config.java @@ -764,6 +764,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1403,6 +1404,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/GameServer.java index 14fda4e201..3df1019b65 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/GameServer.java @@ -488,6 +488,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index bcd0909d31..1b099ea10a 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -202,8 +202,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_10.3_MasterClass/dist/game/config/Server.ini b/L2J_Mobius_10.3_MasterClass/dist/game/config/Server.ini index 06e8c13e27..52c74fc448 100644 --- a/L2J_Mobius_10.3_MasterClass/dist/game/config/Server.ini +++ b/L2J_Mobius_10.3_MasterClass/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/Config.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/Config.java index edc36b546a..f180ce26f8 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/Config.java @@ -768,6 +768,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1427,6 +1428,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/GameServer.java index 181f614181..d40592ee04 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/GameServer.java @@ -492,6 +492,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index bcd0909d31..1b099ea10a 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -202,8 +202,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_10.3_MasterClass/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_C4_ScionsOfDestiny/dist/game/config/Server.ini b/L2J_Mobius_C4_ScionsOfDestiny/dist/game/config/Server.ini index 2b29ea6294..5a572dbeb5 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/dist/game/config/Server.ini +++ b/L2J_Mobius_C4_ScionsOfDestiny/dist/game/config/Server.ini @@ -39,6 +39,11 @@ LoginHost = 127.0.0.1 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/Config.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/Config.java index fe935324d3..17a31584b6 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/Config.java @@ -1039,6 +1039,7 @@ public class Config public static String INTERNAL_HOSTNAME; public static String EXTERNAL_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1122,6 +1123,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetConfig.java index 4d3275cc10..d8dce173bc 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/WritablePacket.java index c04322e9ef..816127d59a 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/commons/network/WritablePacket.java @@ -206,6 +206,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/GameServer.java index 7ae8ad0f49..1221df5404 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/GameServer.java @@ -458,6 +458,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/GameClient.java index e9817d49da..0d463d97bb 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/GameClient.java @@ -21,8 +21,6 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.locks.ReentrantLock; @@ -68,7 +66,6 @@ public class GameClient extends NetClient (byte) 0x87 // The last 4 bytes are fixed. }; - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -184,16 +181,11 @@ public class GameClient extends NetClient return; } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() implementation. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index e08c86cea7..6ee0a47df4 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -57,8 +57,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _charName, _text, this); diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/NpcHtmlMessage.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/NpcHtmlMessage.java index f6de7c7c97..b5870ae31b 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/NpcHtmlMessage.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/NpcHtmlMessage.java @@ -160,16 +160,20 @@ public class NpcHtmlMessage extends ServerPacket } @Override - public void run(Player player) + public void run() { - if (Config.BYPASS_VALIDATION && _validate) + final Player player = getPlayer(); + if (player != null) { - buildBypassCache(player); - buildLinksCache(player); - } - if ((_file != null) && player.isGM() && Config.GM_DEBUG_HTML_PATHS) - { - BuilderUtil.sendHtmlMessage(player, _file.substring(10)); + if (Config.BYPASS_VALIDATION && _validate) + { + buildBypassCache(player); + buildLinksCache(player); + } + if ((_file != null) && player.isGM() && Config.GM_DEBUG_HTML_PATHS) + { + BuilderUtil.sendHtmlMessage(player, _file.substring(10)); + } } } diff --git a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 559fe62fce..57c5a3a8e7 100644 --- a/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_C4_ScionsOfDestiny/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -41,11 +41,21 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } } diff --git a/L2J_Mobius_C6_Interlude/dist/game/config/Server.ini b/L2J_Mobius_C6_Interlude/dist/game/config/Server.ini index a90e2fab3a..336ce30474 100644 --- a/L2J_Mobius_C6_Interlude/dist/game/config/Server.ini +++ b/L2J_Mobius_C6_Interlude/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/Config.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/Config.java index b72e211a98..1c8442b67a 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/Config.java @@ -1086,6 +1086,7 @@ public class Config public static List GAME_SERVER_SUBNETS; public static List GAME_SERVER_HOSTS; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1163,6 +1164,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java index c04322e9ef..816127d59a 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java @@ -206,6 +206,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/GameServer.java index 085a14c875..c6459e368b 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/GameServer.java @@ -472,6 +472,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java index f2a87af350..96f3f37df0 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java @@ -21,8 +21,6 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.locks.ReentrantLock; @@ -56,7 +54,6 @@ public class GameClient extends NetClient protected static final Logger LOGGER = Logger.getLogger(GameClient.class.getName()); protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -176,16 +173,11 @@ public class GameClient extends NetClient return; } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() implementation. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index e08c86cea7..6ee0a47df4 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -57,8 +57,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _charName, _text, this); diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/NpcHtmlMessage.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/NpcHtmlMessage.java index f6de7c7c97..b5870ae31b 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/NpcHtmlMessage.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/NpcHtmlMessage.java @@ -160,16 +160,20 @@ public class NpcHtmlMessage extends ServerPacket } @Override - public void run(Player player) + public void run() { - if (Config.BYPASS_VALIDATION && _validate) + final Player player = getPlayer(); + if (player != null) { - buildBypassCache(player); - buildLinksCache(player); - } - if ((_file != null) && player.isGM() && Config.GM_DEBUG_HTML_PATHS) - { - BuilderUtil.sendHtmlMessage(player, _file.substring(10)); + if (Config.BYPASS_VALIDATION && _validate) + { + buildBypassCache(player); + buildLinksCache(player); + } + if ((_file != null) && player.isGM() && Config.GM_DEBUG_HTML_PATHS) + { + BuilderUtil.sendHtmlMessage(player, _file.substring(10)); + } } } diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 559fe62fce..57c5a3a8e7 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -41,11 +41,21 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } } diff --git a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/loginserver/LoginServer.java index ad578ecf6c..a7033374f8 100644 --- a/L2J_Mobius_C6_Interlude/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_C6_Interlude/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_CT_0_Interlude/dist/game/config/Server.ini b/L2J_Mobius_CT_0_Interlude/dist/game/config/Server.ini index 3f4578e0e4..40c62563c2 100644 --- a/L2J_Mobius_CT_0_Interlude/dist/game/config/Server.ini +++ b/L2J_Mobius_CT_0_Interlude/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/Config.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/Config.java index 2394e344d9..b67f60f3c4 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/Config.java @@ -862,6 +862,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1332,6 +1333,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java index c04322e9ef..816127d59a 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java @@ -206,6 +206,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/GameServer.java index 5334ffa303..c7cb9431b0 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/GameServer.java @@ -438,6 +438,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java index ebfff58ba5..d06281ec34 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -55,7 +53,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -193,16 +190,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() implementation. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index 4002611e42..68224915c0 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -114,8 +114,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 559fe62fce..57c5a3a8e7 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -41,11 +41,21 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } } diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java index d1cd301250..952164b60a 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java @@ -33,9 +33,13 @@ public class TutorialCloseHtml extends ServerPacket } @Override - public void run(Player player) + public void run() { - player.clearHtmlActions(HtmlActionScope.TUTORIAL_HTML); + final Player player = getPlayer(); + if (player != null) + { + player.clearHtmlActions(HtmlActionScope.TUTORIAL_HTML); + } } @Override diff --git a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_CT_0_Interlude/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_CT_2.4_Epilogue/dist/game/config/Server.ini b/L2J_Mobius_CT_2.4_Epilogue/dist/game/config/Server.ini index e1164e689a..349db9ce14 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/dist/game/config/Server.ini +++ b/L2J_Mobius_CT_2.4_Epilogue/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/Config.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/Config.java index 4e58605807..3784ec5b52 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/Config.java @@ -887,6 +887,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1385,6 +1386,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/WritablePacket.java index c04322e9ef..816127d59a 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/commons/network/WritablePacket.java @@ -206,6 +206,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/GameServer.java index 98d6250709..f2ad1bf205 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/GameServer.java @@ -468,6 +468,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/GameClient.java index ebfff58ba5..d06281ec34 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -55,7 +53,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -193,16 +190,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() implementation. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index 348f861e3d..bb6cd576fc 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -114,8 +114,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index c14c96ad28..f3069fdc9a 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -77,11 +77,21 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } } diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java index d1cd301250..952164b60a 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java @@ -33,9 +33,13 @@ public class TutorialCloseHtml extends ServerPacket } @Override - public void run(Player player) + public void run() { - player.clearHtmlActions(HtmlActionScope.TUTORIAL_HTML); + final Player player = getPlayer(); + if (player != null) + { + player.clearHtmlActions(HtmlActionScope.TUTORIAL_HTML); + } } @Override diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_CT_2.6_HighFive/dist/game/config/Server.ini b/L2J_Mobius_CT_2.6_HighFive/dist/game/config/Server.ini index 731693a297..f5eb9c3ca0 100644 --- a/L2J_Mobius_CT_2.6_HighFive/dist/game/config/Server.ini +++ b/L2J_Mobius_CT_2.6_HighFive/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/Config.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/Config.java index 182260a0cc..152965f423 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/Config.java @@ -892,6 +892,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1385,6 +1386,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/WritablePacket.java index c04322e9ef..816127d59a 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/commons/network/WritablePacket.java @@ -206,6 +206,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/GameServer.java index 0fa3bc3a82..91e72c4099 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/GameServer.java @@ -470,6 +470,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/GameClient.java index ce62450a14..f33b6a7570 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -57,7 +55,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -200,16 +197,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() implementation. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index 50b9f8b78c..29f0c4bd55 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -111,8 +111,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/ItemList.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/ItemList.java index 27328eea0c..204085e198 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/ItemList.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/ItemList.java @@ -56,8 +56,12 @@ public class ItemList extends AbstractItemPacket } @Override - public void run(Player player) + public void run() { - player.sendPacket(new ExQuestItemList(_player)); + final Player player = getPlayer(); + if (player != null) + { + player.sendPacket(new ExQuestItemList(_player)); + } } } diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index c14c96ad28..f3069fdc9a 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -77,11 +77,21 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } } diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java index d1cd301250..952164b60a 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/network/serverpackets/TutorialCloseHtml.java @@ -33,9 +33,13 @@ public class TutorialCloseHtml extends ServerPacket } @Override - public void run(Player player) + public void run() { - player.clearHtmlActions(HtmlActionScope.TUTORIAL_HTML); + final Player player = getPlayer(); + if (player != null) + { + player.clearHtmlActions(HtmlActionScope.TUTORIAL_HTML); + } } @Override diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Classic_1.0/dist/game/config/Server.ini b/L2J_Mobius_Classic_1.0/dist/game/config/Server.ini index 9f1a10a6ba..45bc6cb74b 100644 --- a/L2J_Mobius_Classic_1.0/dist/game/config/Server.ini +++ b/L2J_Mobius_Classic_1.0/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/Config.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/Config.java index 10d4960ab7..268ae6a2ae 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/Config.java @@ -768,6 +768,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1302,6 +1303,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/GameServer.java index 1307fb38a2..9c294a9e3c 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/GameServer.java @@ -451,6 +451,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index cda3642199..7879d73baa 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -158,8 +158,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 73ed0eac95..0e206f2d84 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Classic_1.0/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Classic_1.0/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/dist/game/config/Server.ini b/L2J_Mobius_Classic_1.5_AgeOfSplendor/dist/game/config/Server.ini index 87312b5efd..89c280035c 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/dist/game/config/Server.ini +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/Config.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/Config.java index 4db8647afd..08e662e533 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/Config.java @@ -778,6 +778,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1320,6 +1321,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/GameServer.java index 95ffedbf07..e079635023 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/GameServer.java @@ -459,6 +459,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index cda3642199..7879d73baa 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -158,8 +158,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 73ed0eac95..0e206f2d84 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Classic_1.5_AgeOfSplendor/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Classic_2.0_Saviors/dist/game/config/Server.ini b/L2J_Mobius_Classic_2.0_Saviors/dist/game/config/Server.ini index d047a66e76..fc99806f74 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/dist/game/config/Server.ini +++ b/L2J_Mobius_Classic_2.0_Saviors/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/Config.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/Config.java index 65e2d34a6e..5ba1a7fe07 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/Config.java @@ -781,6 +781,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1323,6 +1324,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/GameServer.java index 50c3bd401f..74b7911485 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/GameServer.java @@ -465,6 +465,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index cda3642199..7879d73baa 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -158,8 +158,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 73ed0eac95..0e206f2d84 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Classic_2.5_Zaken/dist/game/config/Server.ini b/L2J_Mobius_Classic_2.5_Zaken/dist/game/config/Server.ini index d0871d49a7..bf3a916dc3 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/dist/game/config/Server.ini +++ b/L2J_Mobius_Classic_2.5_Zaken/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/Config.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/Config.java index fb2923150c..577bccd2fd 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/Config.java @@ -781,6 +781,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1327,6 +1328,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/GameServer.java index 50c3bd401f..74b7911485 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/GameServer.java @@ -465,6 +465,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index cda3642199..7879d73baa 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -158,8 +158,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 73ed0eac95..0e206f2d84 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Classic_2.5_Zaken/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Classic_2.7_Antharas/dist/game/config/Server.ini b/L2J_Mobius_Classic_2.7_Antharas/dist/game/config/Server.ini index 7f0a2f9403..d0b71f054d 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/dist/game/config/Server.ini +++ b/L2J_Mobius_Classic_2.7_Antharas/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/Config.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/Config.java index fb2923150c..577bccd2fd 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/Config.java @@ -781,6 +781,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1327,6 +1328,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/GameServer.java index fed266db24..6aa3f024d4 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/GameServer.java @@ -467,6 +467,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index cda3642199..7879d73baa 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -158,8 +158,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 8d0b312d60..6af5184665 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -117,12 +117,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Classic_2.7_Antharas/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/dist/game/config/Server.ini b/L2J_Mobius_Classic_2.8_SevenSigns/dist/game/config/Server.ini index e27776266e..cd7b233867 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/dist/game/config/Server.ini +++ b/L2J_Mobius_Classic_2.8_SevenSigns/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/Config.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/Config.java index fb2923150c..577bccd2fd 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/Config.java @@ -781,6 +781,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1327,6 +1328,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/GameServer.java index d97805cede..674f77fc88 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/GameServer.java @@ -469,6 +469,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index cda3642199..7879d73baa 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -158,8 +158,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Classic_2.8_SevenSigns/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/dist/game/config/Server.ini b/L2J_Mobius_Classic_2.9.5_Saviors/dist/game/config/Server.ini index f1cdaad49f..300b5907ab 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/dist/game/config/Server.ini +++ b/L2J_Mobius_Classic_2.9.5_Saviors/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/Config.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/Config.java index a64f760bed..463aa23ea4 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/Config.java @@ -787,6 +787,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1338,6 +1339,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/GameServer.java index ab468373cc..bf40293575 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/GameServer.java @@ -473,6 +473,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index cda3642199..7879d73baa 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -158,8 +158,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Classic_2.9.5_Saviors/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/dist/game/config/Server.ini b/L2J_Mobius_Classic_2.9_SecretOfEmpire/dist/game/config/Server.ini index 352fb1bf55..dfb4cd73f4 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/dist/game/config/Server.ini +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/Config.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/Config.java index 450468a02c..3044e4ac50 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/Config.java @@ -781,6 +781,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1331,6 +1332,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/GameServer.java index 566ae6fb7a..8b0d4f2cab 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/GameServer.java @@ -471,6 +471,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index cda3642199..7879d73baa 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -158,8 +158,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Classic_2.9_SecretOfEmpire/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Classic_3.0_TheKamael/dist/game/config/Server.ini b/L2J_Mobius_Classic_3.0_TheKamael/dist/game/config/Server.ini index 7b2f7f48fe..ca5e0686f6 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/dist/game/config/Server.ini +++ b/L2J_Mobius_Classic_3.0_TheKamael/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/Config.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/Config.java index 80867aae33..ef2625471f 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/Config.java @@ -786,6 +786,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1338,6 +1339,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/GameServer.java index 1f92c17276..02f74b2b90 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/GameServer.java @@ -477,6 +477,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index 0fb68bb0fa..f4050aabba 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -198,8 +198,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Classic_Interlude/dist/game/config/Server.ini b/L2J_Mobius_Classic_Interlude/dist/game/config/Server.ini index eecc17f44d..fcdf066d4b 100644 --- a/L2J_Mobius_Classic_Interlude/dist/game/config/Server.ini +++ b/L2J_Mobius_Classic_Interlude/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/Config.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/Config.java index 5b14f6f97e..691d3242a4 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/Config.java @@ -788,6 +788,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1331,6 +1332,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/GameServer.java index 094685f086..0240819746 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/GameServer.java @@ -461,6 +461,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java index d88646c083..d4fc196761 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index cda3642199..7879d73baa 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -158,8 +158,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 73ed0eac95..0e206f2d84 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/config/Server.ini b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/config/Server.ini index e767945562..5c5cb9fc93 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/config/Server.ini +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/Config.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/Config.java index 10b6f2737c..155dcac5ff 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/Config.java @@ -797,6 +797,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1363,6 +1364,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/GameServer.java index 9acc07fd1c..ec7ea0393e 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/GameServer.java @@ -497,6 +497,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index 106def7b05..e6f20eaba5 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -231,8 +231,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Essence_5.2_FrostLord/dist/game/config/Server.ini b/L2J_Mobius_Essence_5.2_FrostLord/dist/game/config/Server.ini index f21f63a37a..2dd6d60e87 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/dist/game/config/Server.ini +++ b/L2J_Mobius_Essence_5.2_FrostLord/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/Config.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/Config.java index 411e1cc3b0..b12c304fcc 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/Config.java @@ -800,6 +800,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1382,6 +1383,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/GameServer.java index 88bcf3cd00..28006b54f2 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/GameServer.java @@ -509,6 +509,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index 106def7b05..e6f20eaba5 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -231,8 +231,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Essence_6.2_Vanguard/dist/game/config/Server.ini b/L2J_Mobius_Essence_6.2_Vanguard/dist/game/config/Server.ini index 5bc0dfa2f4..57a87474e7 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/dist/game/config/Server.ini +++ b/L2J_Mobius_Essence_6.2_Vanguard/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/Config.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/Config.java index 10fb999401..21a3f50f6f 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/Config.java @@ -804,6 +804,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1412,6 +1413,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/GameServer.java index e4b9b843fc..da08bfcc45 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/GameServer.java @@ -519,6 +519,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index 106def7b05..e6f20eaba5 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -231,8 +231,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true); diff --git a/L2J_Mobius_Essence_6.3_Crusader/dist/game/config/Server.ini b/L2J_Mobius_Essence_6.3_Crusader/dist/game/config/Server.ini index 9c2f327f4b..2026a09fe0 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/dist/game/config/Server.ini +++ b/L2J_Mobius_Essence_6.3_Crusader/dist/game/config/Server.ini @@ -36,6 +36,11 @@ GameserverPort = 7777 # Default: 100 ClientReadPoolSize = 100 +# Client pool size for sending server packets. +# Each pool is executed on a separate thread. +# Default: 25 +ClientSendPoolSize = 25 + # Client pool size for executing client packets. # Each pool is executed on a separate thread. # Default: 50 diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/Config.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/Config.java index 10fb999401..21a3f50f6f 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/Config.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/Config.java @@ -804,6 +804,7 @@ public class Config public static int LOGIN_BLOCK_AFTER_BAN; public static String GAMESERVER_HOSTNAME; public static int CLIENT_READ_POOL_SIZE; + public static int CLIENT_SEND_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE; public static int PACKET_QUEUE_LIMIT; public static boolean PACKET_FLOOD_DISCONNECT; @@ -1412,6 +1413,7 @@ public class Config GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); + CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/ExecuteThread.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/ExecuteThread.java index 2957abfdbf..549447122b 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/ExecuteThread.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/ExecuteThread.java @@ -16,6 +16,7 @@ public class ExecuteThread implements Runnable private final Set _pool; private final PacketHandlerInterface _packetHandler; + private boolean _idle; public ExecuteThread(Set pool, PacketHandlerInterface packetHandler) { @@ -31,6 +32,8 @@ public class ExecuteThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -40,7 +43,7 @@ public class ExecuteThread implements Runnable continue ITERATE; } - final byte[] data = client.getPacketData().poll(); + final byte[] data = client.getReceivedData().poll(); if (data == null) { continue ITERATE; @@ -64,16 +67,29 @@ public class ExecuteThread implements Runnable } } _packetHandler.handle(client, new ReadablePacket(data)); + + _idle = false; + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetClient.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetClient.java index bcbe709314..052421ca65 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetClient.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetClient.java @@ -15,12 +15,15 @@ public class NetClient { protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); + private final Queue _receivedData = new ConcurrentLinkedQueue<>(); + private final Queue _sendPacketQueue = new ConcurrentLinkedQueue<>(); + private String _ip; private Socket _socket; private InputStream _inputStream; private OutputStream _outputStream; private NetConfig _netConfig; - private Queue _packetData; + private byte[] _pendingData; private int _pendingPacketSize; @@ -33,7 +36,6 @@ public class NetClient { _socket = socket; _netConfig = netConfig; - _packetData = new ConcurrentLinkedQueue<>(); _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 try @@ -82,10 +84,8 @@ public class NetClient } } - if (_packetData != null) - { - _packetData.clear(); - } + _receivedData.clear(); + _sendPacketQueue.clear(); if (_pendingData != null) { @@ -97,10 +97,10 @@ public class NetClient * Add packet data to the queue. * @param data */ - public void addPacketData(byte[] data) + public void addReceivedData(byte[] data) { // Check packet flooding. - final int size = _packetData.size(); + final int size = _receivedData.size(); if (size >= _netConfig.getPacketQueueLimit()) { if (_netConfig.isPacketFloodDisconnect()) @@ -127,15 +127,15 @@ public class NetClient } // Add to queue. - _packetData.add(data); + _receivedData.add(data); } /** - * @return the pending packet data. + * @return the pending received data. */ - public Queue getPacketData() + public Queue getReceivedData() { - return _packetData; + return _receivedData; } /** @@ -172,34 +172,21 @@ public class NetClient _pendingPacketSize = pendingPacketSize; } + /** + * @return the writable packet queue waiting to be sent. + */ + public Queue getSendPacketQueue() + { + return _sendPacketQueue; + } + /** * Sends a packet over the network using the default encryption. * @param packet The packet to send. */ public void sendPacket(WritablePacket packet) { - if ((_socket == null) || !_socket.isConnected()) - { - return; - } - - final byte[] sendableBytes = packet.getSendableBytes(getEncryption()); - if (sendableBytes == null) - { - return; - } - - try - { - synchronized (this) - { - _outputStream.write(sendableBytes); - _outputStream.flush(); - } - } - catch (Exception ignored) - { - } + _sendPacketQueue.add(packet); } /** diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetConfig.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetConfig.java index 9cb8854339..ce6efe507e 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetConfig.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetConfig.java @@ -7,6 +7,7 @@ package org.l2jmobius.commons.network; public class NetConfig { private int _readPoolSize = 100; + private int _sendPoolSize = 25; private int _executePoolSize = 50; private int _packetQueueLimit = 80; private boolean _packetFloodDisconnect = false; @@ -32,6 +33,23 @@ public class NetConfig _readPoolSize = clientPoolSize; } + /** + * @return the NetClient pool size for sending server packets. + */ + public int getSendPoolSize() + { + return _sendPoolSize; + } + + /** + * Sets the NetClient pool size for sending server packets. + * @param clientPoolSize + */ + public void setSendPoolSize(int clientPoolSize) + { + _sendPoolSize = clientPoolSize; + } + /** * @return the NetClient pool size for executing client packets. */ diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetServer.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetServer.java index 49969adb53..9a9164b307 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetServer.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/NetServer.java @@ -20,6 +20,7 @@ public class NetServer protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected final List> _clientReadPools = new LinkedList<>(); + protected final List> _clientSendPools = new LinkedList<>(); protected final List> _clientExecutePools = new LinkedList<>(); protected final NetConfig _netConfig = new NetConfig(); protected final String _hostname; @@ -155,6 +156,35 @@ public class NetServer _clientReadPools.add(newReadPool); } + // Add to send pool. + + // Find a pool that is not full. + boolean sendPoolFound = false; + SEND_POOLS: for (Set pool : _clientSendPools) + { + if (pool.size() < _netConfig.getSendPoolSize()) + { + pool.add(client); + sendPoolFound = true; + break SEND_POOLS; + } + } + + // All pools are full. + if (!sendPoolFound) + { + // Create a new client pool. + final Set newSendPool = ConcurrentHashMap.newKeySet(_netConfig.getSendPoolSize()); + newSendPool.add(client); + // Create a new task for the new pool. + final Thread sendThread = new Thread(new SendThread<>(newSendPool), _name + ": Packet send thread " + _clientSendPools.size()); + sendThread.setPriority(Thread.MAX_PRIORITY); + sendThread.setDaemon(true); + sendThread.start(); + // Add the new pool to the pool list. + _clientSendPools.add(newSendPool); + } + // Add to execute pool. // Find a pool that is not full. diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/ReadThread.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/ReadThread.java index 64153965fc..b238c399ad 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/ReadThread.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/ReadThread.java @@ -14,6 +14,7 @@ public class ReadThread implements Runnable private final Set _pool; private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. + private boolean _idle; public ReadThread(Set pool) { @@ -28,6 +29,8 @@ public class ReadThread implements Runnable // No need to iterate when pool is empty. if (!_pool.isEmpty()) { + _idle = true; + // Iterate client pool. ITERATE: for (E client : _pool) { @@ -80,14 +83,16 @@ public class ReadThread implements Runnable // Read was complete. if (currentSize >= pendingPacketSize) { - // Add packet data to client. - client.addPacketData(mergedData); + // Add received data to client. + client.addReceivedData(mergedData); client.setPendingData(null); } else { client.setPendingData(mergedData); } + + _idle = false; } } continue ITERATE; @@ -188,10 +193,12 @@ public class ReadThread implements Runnable client.setPendingData(pendindBytes); client.setPendingPacketSize(packetSize); } - else // Add packet data to client. + else // Add received data to client. { - client.addPacketData(packetData); + client.addReceivedData(packetData); } + + _idle = false; } } } @@ -206,15 +213,26 @@ public class ReadThread implements Runnable onDisconnection(client); } } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } } - - // Prevent high CPU caused by repeatedly looping. - try - { - Thread.sleep(1); - } - catch (Exception ignored) + else // Remain idle for 1 second. { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } } } } diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/SendThread.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/SendThread.java new file mode 100644 index 0000000000..96b03292dc --- /dev/null +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/SendThread.java @@ -0,0 +1,106 @@ +package org.l2jmobius.commons.network; + +import java.io.OutputStream; +import java.net.Socket; +import java.util.Queue; +import java.util.Set; + +/** + * @author Pantelis Andrianakis + * @since June 22nd 2023 + * @param extends NetClient + */ +public class SendThread implements Runnable +{ + // Throttle packets sent per cycle to limit flooding from waiting one client. + private static final int MAX_PACKETS_SENT_PER_CYCLE = 2000; + + private final Set _pool; + private boolean _idle; + + public SendThread(Set pool) + { + _pool = pool; + } + + @Override + public void run() + { + while (true) + { + // No need to iterate when pool is empty. + if (!_pool.isEmpty()) + { + _idle = true; + + // Iterate client pool. + ITERATE: for (E client : _pool) + { + final Socket socket = client.getSocket(); + if (socket == null) + { + _pool.remove(client); + continue ITERATE; + } + + try + { + final Queue packetQueue = client.getSendPacketQueue(); + if (packetQueue.isEmpty()) + { + continue ITERATE; + } + + final OutputStream outputStream = client.getOutputStream(); + SEND_CYCLE: for (int count = 0; count < MAX_PACKETS_SENT_PER_CYCLE; count++) + { + final WritablePacket writablePacket = packetQueue.poll(); + if (writablePacket == null) + { + continue ITERATE; + } + + final byte[] sendableBytes = writablePacket.getSendableBytes(client.getEncryption()); + if (sendableBytes == null) + { + continue SEND_CYCLE; + } + + // Send the packet data. + outputStream.write(sendableBytes); + outputStream.flush(); + + // Run packet implementation. + writablePacket.run(); + + _idle = false; + } + } + catch (Exception ignored) + { + } + } + + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(_idle ? 10 : 1); + } + catch (Exception ignored) + { + } + } + else // Remain idle for 1 second. + { + // Prevent high CPU caused by repeatedly looping. + try + { + Thread.sleep(1000); + } + catch (Exception ignored) + { + } + } + } + } +} diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/WritablePacket.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/WritablePacket.java index 98988f3886..453442a930 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/WritablePacket.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/commons/network/WritablePacket.java @@ -223,6 +223,14 @@ public abstract class WritablePacket // Overridden by server implementation. } + /** + * Method that runs after packet is sent. + */ + public void run() + { + // Overridden by server implementation. + } + /** * @return byte[] of the sendable packet data, including a size header. */ diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/GameServer.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/GameServer.java index e4b9b843fc..da08bfcc45 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/GameServer.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/GameServer.java @@ -519,6 +519,7 @@ public class GameServer final NetServer server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); + server.getNetConfig().setSendPoolSize(Config.CLIENT_SEND_POOL_SIZE); server.getNetConfig().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/GameClient.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/GameClient.java index f79e5bec8d..db26ac149b 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/GameClient.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/GameClient.java @@ -20,8 +20,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,7 +64,6 @@ public class GameClient extends NetClient { protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); - private final Queue _pendingPackets = new ConcurrentLinkedQueue<>(); private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final ReentrantLock _playerLock = new ReentrantLock(); private ConnectionState _connectionState = ConnectionState.CONNECTED; @@ -218,16 +215,11 @@ public class GameClient extends NetClient } } - // Keep the order of packets if sent by multiple threads. - _pendingPackets.add(packet); - synchronized (_pendingPackets) - { - // Send the packet data. - super.sendPacket(packet); - - // Run packet implementation. - packet.run(_player); - } + // Used by packet run() method. + packet.setPlayer(_player); + + // Send the packet data. + super.sendPacket(packet); } public void sendPacket(SystemMessageId systemMessageId) diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java index 21e379f20e..d73c3186c8 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/AbstractHtmlPacket.java @@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.clearHtmlActions(getScope()); } + if (_disabledValidation) { return; diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java index 106def7b05..e6f20eaba5 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/CreatureSay.java @@ -231,8 +231,9 @@ public class CreatureSay extends ServerPacket } @Override - public void run(Player player) + public void run() { + final Player player = getPlayer(); if (player != null) { player.broadcastSnoop(_chatType, _senderName, _text); diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java index 9280f924bc..1a655a6e0d 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/gameserver/network/serverpackets/ServerPacket.java @@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket super(initialSize); } + private Player _player; + /** - * Method that runs after packet is sent. - * @param player + * @return the Player */ - public void run(Player player) + public Player getPlayer() { + return _player; + } + + /** + * @param player the Player to set. + */ + public void setPlayer(Player player) + { + _player = player; } protected void writeOptionalInt(int value) diff --git a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/loginserver/LoginServer.java b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/loginserver/LoginServer.java index 338c37b154..acbc036834 100644 --- a/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/loginserver/LoginServer.java +++ b/L2J_Mobius_Essence_6.3_Crusader/java/org/l2jmobius/loginserver/LoginServer.java @@ -136,6 +136,7 @@ public class LoginServer final NetServer server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); server.setName(getClass().getSimpleName()); server.getNetConfig().setReadPoolSize(2000); + server.getNetConfig().setSendPoolSize(2000); server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketFloodDisconnect(true);