Addition of SendThread class and other related adjustments.

This commit is contained in:
MobiusDevelopment 2023-06-25 19:37:46 +03:00
parent 4708f17e41
commit 4bd9210c69
468 changed files with 8349 additions and 2223 deletions

View File

@ -36,6 +36,11 @@ GameserverPort = 7777
# Default: 100 # Default: 100
ClientReadPoolSize = 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. # Client pool size for executing client packets.
# Each pool is executed on a separate thread. # Each pool is executed on a separate thread.
# Default: 50 # Default: 50

View File

@ -768,6 +768,7 @@ public class Config
public static int LOGIN_BLOCK_AFTER_BAN; public static int LOGIN_BLOCK_AFTER_BAN;
public static String GAMESERVER_HOSTNAME; public static String GAMESERVER_HOSTNAME;
public static int CLIENT_READ_POOL_SIZE; public static int CLIENT_READ_POOL_SIZE;
public static int CLIENT_SEND_POOL_SIZE;
public static int CLIENT_EXECUTE_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE;
public static int PACKET_QUEUE_LIMIT; public static int PACKET_QUEUE_LIMIT;
public static boolean PACKET_FLOOD_DISCONNECT; public static boolean PACKET_FLOOD_DISCONNECT;
@ -1362,6 +1363,7 @@ public class Config
GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014);
GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1");
CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100);
CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25);
CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50);
PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80);
PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false);

View File

@ -16,6 +16,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final PacketHandlerInterface<E> _packetHandler; private final PacketHandlerInterface<E> _packetHandler;
private boolean _idle;
public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler) public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler)
{ {
@ -31,6 +32,8 @@ public class ExecuteThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -40,7 +43,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
continue ITERATE; continue ITERATE;
} }
final byte[] data = client.getPacketData().poll(); final byte[] data = client.getReceivedData().poll();
if (data == null) if (data == null)
{ {
continue ITERATE; continue ITERATE;
@ -64,16 +67,29 @@ public class ExecuteThread<E extends NetClient> implements Runnable
} }
} }
_packetHandler.handle(client, new ReadablePacket(data)); _packetHandler.handle(client, new ReadablePacket(data));
_idle = false;
}
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -15,12 +15,15 @@ public class NetClient
{ {
protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName());
private final Queue<byte[]> _receivedData = new ConcurrentLinkedQueue<>();
private final Queue<WritablePacket> _sendPacketQueue = new ConcurrentLinkedQueue<>();
private String _ip; private String _ip;
private Socket _socket; private Socket _socket;
private InputStream _inputStream; private InputStream _inputStream;
private OutputStream _outputStream; private OutputStream _outputStream;
private NetConfig _netConfig; private NetConfig _netConfig;
private Queue<byte[]> _packetData;
private byte[] _pendingData; private byte[] _pendingData;
private int _pendingPacketSize; private int _pendingPacketSize;
@ -33,7 +36,6 @@ public class NetClient
{ {
_socket = socket; _socket = socket;
_netConfig = netConfig; _netConfig = netConfig;
_packetData = new ConcurrentLinkedQueue<>();
_ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1
try try
@ -82,10 +84,8 @@ public class NetClient
} }
} }
if (_packetData != null) _receivedData.clear();
{ _sendPacketQueue.clear();
_packetData.clear();
}
if (_pendingData != null) if (_pendingData != null)
{ {
@ -97,10 +97,10 @@ public class NetClient
* Add packet data to the queue. * Add packet data to the queue.
* @param data * @param data
*/ */
public void addPacketData(byte[] data) public void addReceivedData(byte[] data)
{ {
// Check packet flooding. // Check packet flooding.
final int size = _packetData.size(); final int size = _receivedData.size();
if (size >= _netConfig.getPacketQueueLimit()) if (size >= _netConfig.getPacketQueueLimit())
{ {
if (_netConfig.isPacketFloodDisconnect()) if (_netConfig.isPacketFloodDisconnect())
@ -127,15 +127,15 @@ public class NetClient
} }
// Add to queue. // Add to queue.
_packetData.add(data); _receivedData.add(data);
} }
/** /**
* @return the pending packet data. * @return the pending received data.
*/ */
public Queue<byte[]> getPacketData() public Queue<byte[]> getReceivedData()
{ {
return _packetData; return _receivedData;
} }
/** /**
@ -172,34 +172,21 @@ public class NetClient
_pendingPacketSize = pendingPacketSize; _pendingPacketSize = pendingPacketSize;
} }
/**
* @return the writable packet queue waiting to be sent.
*/
public Queue<WritablePacket> getSendPacketQueue()
{
return _sendPacketQueue;
}
/** /**
* Sends a packet over the network using the default encryption. * Sends a packet over the network using the default encryption.
* @param packet The packet to send. * @param packet The packet to send.
*/ */
public void sendPacket(WritablePacket packet) public void sendPacket(WritablePacket packet)
{ {
if ((_socket == null) || !_socket.isConnected()) _sendPacketQueue.add(packet);
{
return;
}
final byte[] sendableBytes = packet.getSendableBytes(getEncryption());
if (sendableBytes == null)
{
return;
}
try
{
synchronized (this)
{
_outputStream.write(sendableBytes);
_outputStream.flush();
}
}
catch (Exception ignored)
{
}
} }
/** /**

View File

@ -7,6 +7,7 @@ package org.l2jmobius.commons.network;
public class NetConfig public class NetConfig
{ {
private int _readPoolSize = 100; private int _readPoolSize = 100;
private int _sendPoolSize = 25;
private int _executePoolSize = 50; private int _executePoolSize = 50;
private int _packetQueueLimit = 80; private int _packetQueueLimit = 80;
private boolean _packetFloodDisconnect = false; private boolean _packetFloodDisconnect = false;
@ -32,6 +33,23 @@ public class NetConfig
_readPoolSize = clientPoolSize; _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. * @return the NetClient pool size for executing client packets.
*/ */

View File

@ -20,6 +20,7 @@ public class NetServer<E extends NetClient>
protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName());
protected final List<Set<E>> _clientReadPools = new LinkedList<>(); protected final List<Set<E>> _clientReadPools = new LinkedList<>();
protected final List<Set<E>> _clientSendPools = new LinkedList<>();
protected final List<Set<E>> _clientExecutePools = new LinkedList<>(); protected final List<Set<E>> _clientExecutePools = new LinkedList<>();
protected final NetConfig _netConfig = new NetConfig(); protected final NetConfig _netConfig = new NetConfig();
protected final String _hostname; protected final String _hostname;
@ -155,6 +156,35 @@ public class NetServer<E extends NetClient>
_clientReadPools.add(newReadPool); _clientReadPools.add(newReadPool);
} }
// Add to send pool.
// Find a pool that is not full.
boolean sendPoolFound = false;
SEND_POOLS: for (Set<E> 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<E> 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. // Add to execute pool.
// Find a pool that is not full. // Find a pool that is not full.

View File

@ -14,6 +14,7 @@ public class ReadThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer.
private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer.
private boolean _idle;
public ReadThread(Set<E> pool) public ReadThread(Set<E> pool)
{ {
@ -28,6 +29,8 @@ public class ReadThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -80,14 +83,16 @@ public class ReadThread<E extends NetClient> implements Runnable
// Read was complete. // Read was complete.
if (currentSize >= pendingPacketSize) if (currentSize >= pendingPacketSize)
{ {
// Add packet data to client. // Add received data to client.
client.addPacketData(mergedData); client.addReceivedData(mergedData);
client.setPendingData(null); client.setPendingData(null);
} }
else else
{ {
client.setPendingData(mergedData); client.setPendingData(mergedData);
} }
_idle = false;
} }
} }
continue ITERATE; continue ITERATE;
@ -188,10 +193,12 @@ public class ReadThread<E extends NetClient> implements Runnable
client.setPendingData(pendindBytes); client.setPendingData(pendindBytes);
client.setPendingPacketSize(packetSize); 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<E extends NetClient> implements Runnable
onDisconnection(client); onDisconnection(client);
} }
} }
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -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 <E> extends NetClient
*/
public class SendThread<E extends NetClient> 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<E> _pool;
private boolean _idle;
public SendThread(Set<E> 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<WritablePacket> 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)
{
}
}
}
}
}

View File

@ -223,6 +223,14 @@ public abstract class WritablePacket
// Overridden by server implementation. // Overridden by server implementation.
} }
/**
* Method that runs after packet is sent.
*/
public void run()
{
// Overridden by server implementation.
}
/** /**
* @return <b>byte[]</b> of the sendable packet data, including a size header. * @return <b>byte[]</b> of the sendable packet data, including a size header.
*/ */

View File

@ -456,6 +456,7 @@ public class GameServer
final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); 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().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE);
server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT);
server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT);

View File

@ -20,8 +20,6 @@ import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -66,7 +64,6 @@ public class GameClient extends NetClient
{ {
protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting");
private final Queue<ServerPacket> _pendingPackets = new ConcurrentLinkedQueue<>();
private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final FloodProtectors _floodProtectors = new FloodProtectors(this);
private final ReentrantLock _playerLock = new ReentrantLock(); private final ReentrantLock _playerLock = new ReentrantLock();
private ConnectionState _connectionState = ConnectionState.CONNECTED; private ConnectionState _connectionState = ConnectionState.CONNECTED;
@ -218,16 +215,11 @@ public class GameClient extends NetClient
} }
} }
// Keep the order of packets if sent by multiple threads. // Used by packet run() method.
_pendingPackets.add(packet); packet.setPlayer(_player);
synchronized (_pendingPackets)
{ // Send the packet data.
// Send the packet data. super.sendPacket(packet);
super.sendPacket(packet);
// Run packet implementation.
packet.run(_player);
}
} }
public void sendPacket(SystemMessageId systemMessageId) public void sendPacket(SystemMessageId systemMessageId)

View File

@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.clearHtmlActions(getScope()); player.clearHtmlActions(getScope());
} }
if (_disabledValidation) if (_disabledValidation)
{ {
return; return;

View File

@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.broadcastSnoop(_chatType, _senderName, _text); player.broadcastSnoop(_chatType, _senderName, _text);

View File

@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket
super(initialSize); super(initialSize);
} }
private Player _player;
/** /**
* Method that runs after packet is sent. * @return the Player
* @param 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) protected void writeOptionalInt(int value)

View File

@ -136,6 +136,7 @@ public class LoginServer
final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(2000); server.getNetConfig().setReadPoolSize(2000);
server.getNetConfig().setSendPoolSize(2000);
server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setExecutePoolSize(2000);
server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketQueueLimit(10);
server.getNetConfig().setPacketFloodDisconnect(true); server.getNetConfig().setPacketFloodDisconnect(true);

View File

@ -36,6 +36,11 @@ GameserverPort = 7777
# Default: 100 # Default: 100
ClientReadPoolSize = 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. # Client pool size for executing client packets.
# Each pool is executed on a separate thread. # Each pool is executed on a separate thread.
# Default: 50 # Default: 50

View File

@ -779,6 +779,7 @@ public class Config
public static int LOGIN_BLOCK_AFTER_BAN; public static int LOGIN_BLOCK_AFTER_BAN;
public static String GAMESERVER_HOSTNAME; public static String GAMESERVER_HOSTNAME;
public static int CLIENT_READ_POOL_SIZE; public static int CLIENT_READ_POOL_SIZE;
public static int CLIENT_SEND_POOL_SIZE;
public static int CLIENT_EXECUTE_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE;
public static int PACKET_QUEUE_LIMIT; public static int PACKET_QUEUE_LIMIT;
public static boolean PACKET_FLOOD_DISCONNECT; public static boolean PACKET_FLOOD_DISCONNECT;
@ -1374,6 +1375,7 @@ public class Config
GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014);
GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1");
CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100);
CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25);
CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50);
PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80);
PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false);

View File

@ -16,6 +16,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final PacketHandlerInterface<E> _packetHandler; private final PacketHandlerInterface<E> _packetHandler;
private boolean _idle;
public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler) public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler)
{ {
@ -31,6 +32,8 @@ public class ExecuteThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -40,7 +43,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
continue ITERATE; continue ITERATE;
} }
final byte[] data = client.getPacketData().poll(); final byte[] data = client.getReceivedData().poll();
if (data == null) if (data == null)
{ {
continue ITERATE; continue ITERATE;
@ -64,16 +67,29 @@ public class ExecuteThread<E extends NetClient> implements Runnable
} }
} }
_packetHandler.handle(client, new ReadablePacket(data)); _packetHandler.handle(client, new ReadablePacket(data));
_idle = false;
}
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -15,12 +15,15 @@ public class NetClient
{ {
protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName());
private final Queue<byte[]> _receivedData = new ConcurrentLinkedQueue<>();
private final Queue<WritablePacket> _sendPacketQueue = new ConcurrentLinkedQueue<>();
private String _ip; private String _ip;
private Socket _socket; private Socket _socket;
private InputStream _inputStream; private InputStream _inputStream;
private OutputStream _outputStream; private OutputStream _outputStream;
private NetConfig _netConfig; private NetConfig _netConfig;
private Queue<byte[]> _packetData;
private byte[] _pendingData; private byte[] _pendingData;
private int _pendingPacketSize; private int _pendingPacketSize;
@ -33,7 +36,6 @@ public class NetClient
{ {
_socket = socket; _socket = socket;
_netConfig = netConfig; _netConfig = netConfig;
_packetData = new ConcurrentLinkedQueue<>();
_ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1
try try
@ -82,10 +84,8 @@ public class NetClient
} }
} }
if (_packetData != null) _receivedData.clear();
{ _sendPacketQueue.clear();
_packetData.clear();
}
if (_pendingData != null) if (_pendingData != null)
{ {
@ -97,10 +97,10 @@ public class NetClient
* Add packet data to the queue. * Add packet data to the queue.
* @param data * @param data
*/ */
public void addPacketData(byte[] data) public void addReceivedData(byte[] data)
{ {
// Check packet flooding. // Check packet flooding.
final int size = _packetData.size(); final int size = _receivedData.size();
if (size >= _netConfig.getPacketQueueLimit()) if (size >= _netConfig.getPacketQueueLimit())
{ {
if (_netConfig.isPacketFloodDisconnect()) if (_netConfig.isPacketFloodDisconnect())
@ -127,15 +127,15 @@ public class NetClient
} }
// Add to queue. // Add to queue.
_packetData.add(data); _receivedData.add(data);
} }
/** /**
* @return the pending packet data. * @return the pending received data.
*/ */
public Queue<byte[]> getPacketData() public Queue<byte[]> getReceivedData()
{ {
return _packetData; return _receivedData;
} }
/** /**
@ -172,34 +172,21 @@ public class NetClient
_pendingPacketSize = pendingPacketSize; _pendingPacketSize = pendingPacketSize;
} }
/**
* @return the writable packet queue waiting to be sent.
*/
public Queue<WritablePacket> getSendPacketQueue()
{
return _sendPacketQueue;
}
/** /**
* Sends a packet over the network using the default encryption. * Sends a packet over the network using the default encryption.
* @param packet The packet to send. * @param packet The packet to send.
*/ */
public void sendPacket(WritablePacket packet) public void sendPacket(WritablePacket packet)
{ {
if ((_socket == null) || !_socket.isConnected()) _sendPacketQueue.add(packet);
{
return;
}
final byte[] sendableBytes = packet.getSendableBytes(getEncryption());
if (sendableBytes == null)
{
return;
}
try
{
synchronized (this)
{
_outputStream.write(sendableBytes);
_outputStream.flush();
}
}
catch (Exception ignored)
{
}
} }
/** /**

View File

@ -7,6 +7,7 @@ package org.l2jmobius.commons.network;
public class NetConfig public class NetConfig
{ {
private int _readPoolSize = 100; private int _readPoolSize = 100;
private int _sendPoolSize = 25;
private int _executePoolSize = 50; private int _executePoolSize = 50;
private int _packetQueueLimit = 80; private int _packetQueueLimit = 80;
private boolean _packetFloodDisconnect = false; private boolean _packetFloodDisconnect = false;
@ -32,6 +33,23 @@ public class NetConfig
_readPoolSize = clientPoolSize; _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. * @return the NetClient pool size for executing client packets.
*/ */

View File

@ -20,6 +20,7 @@ public class NetServer<E extends NetClient>
protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName());
protected final List<Set<E>> _clientReadPools = new LinkedList<>(); protected final List<Set<E>> _clientReadPools = new LinkedList<>();
protected final List<Set<E>> _clientSendPools = new LinkedList<>();
protected final List<Set<E>> _clientExecutePools = new LinkedList<>(); protected final List<Set<E>> _clientExecutePools = new LinkedList<>();
protected final NetConfig _netConfig = new NetConfig(); protected final NetConfig _netConfig = new NetConfig();
protected final String _hostname; protected final String _hostname;
@ -155,6 +156,35 @@ public class NetServer<E extends NetClient>
_clientReadPools.add(newReadPool); _clientReadPools.add(newReadPool);
} }
// Add to send pool.
// Find a pool that is not full.
boolean sendPoolFound = false;
SEND_POOLS: for (Set<E> 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<E> 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. // Add to execute pool.
// Find a pool that is not full. // Find a pool that is not full.

View File

@ -14,6 +14,7 @@ public class ReadThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer.
private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer.
private boolean _idle;
public ReadThread(Set<E> pool) public ReadThread(Set<E> pool)
{ {
@ -28,6 +29,8 @@ public class ReadThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -80,14 +83,16 @@ public class ReadThread<E extends NetClient> implements Runnable
// Read was complete. // Read was complete.
if (currentSize >= pendingPacketSize) if (currentSize >= pendingPacketSize)
{ {
// Add packet data to client. // Add received data to client.
client.addPacketData(mergedData); client.addReceivedData(mergedData);
client.setPendingData(null); client.setPendingData(null);
} }
else else
{ {
client.setPendingData(mergedData); client.setPendingData(mergedData);
} }
_idle = false;
} }
} }
continue ITERATE; continue ITERATE;
@ -188,10 +193,12 @@ public class ReadThread<E extends NetClient> implements Runnable
client.setPendingData(pendindBytes); client.setPendingData(pendindBytes);
client.setPendingPacketSize(packetSize); 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<E extends NetClient> implements Runnable
onDisconnection(client); onDisconnection(client);
} }
} }
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -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 <E> extends NetClient
*/
public class SendThread<E extends NetClient> 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<E> _pool;
private boolean _idle;
public SendThread(Set<E> 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<WritablePacket> 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)
{
}
}
}
}
}

View File

@ -223,6 +223,14 @@ public abstract class WritablePacket
// Overridden by server implementation. // Overridden by server implementation.
} }
/**
* Method that runs after packet is sent.
*/
public void run()
{
// Overridden by server implementation.
}
/** /**
* @return <b>byte[]</b> of the sendable packet data, including a size header. * @return <b>byte[]</b> of the sendable packet data, including a size header.
*/ */

View File

@ -464,6 +464,7 @@ public class GameServer
final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); 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().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE);
server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT);
server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT);

View File

@ -20,8 +20,6 @@ import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -66,7 +64,6 @@ public class GameClient extends NetClient
{ {
protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting");
private final Queue<ServerPacket> _pendingPackets = new ConcurrentLinkedQueue<>();
private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final FloodProtectors _floodProtectors = new FloodProtectors(this);
private final ReentrantLock _playerLock = new ReentrantLock(); private final ReentrantLock _playerLock = new ReentrantLock();
private ConnectionState _connectionState = ConnectionState.CONNECTED; private ConnectionState _connectionState = ConnectionState.CONNECTED;
@ -218,16 +215,11 @@ public class GameClient extends NetClient
} }
} }
// Keep the order of packets if sent by multiple threads. // Used by packet run() method.
_pendingPackets.add(packet); packet.setPlayer(_player);
synchronized (_pendingPackets)
{ // Send the packet data.
// Send the packet data. super.sendPacket(packet);
super.sendPacket(packet);
// Run packet implementation.
packet.run(_player);
}
} }
public void sendPacket(SystemMessageId systemMessageId) public void sendPacket(SystemMessageId systemMessageId)

View File

@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.clearHtmlActions(getScope()); player.clearHtmlActions(getScope());
} }
if (_disabledValidation) if (_disabledValidation)
{ {
return; return;

View File

@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.broadcastSnoop(_chatType, _senderName, _text); player.broadcastSnoop(_chatType, _senderName, _text);

View File

@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket
super(initialSize); super(initialSize);
} }
private Player _player;
/** /**
* Method that runs after packet is sent. * @return the Player
* @param 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) protected void writeOptionalInt(int value)

View File

@ -136,6 +136,7 @@ public class LoginServer
final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(2000); server.getNetConfig().setReadPoolSize(2000);
server.getNetConfig().setSendPoolSize(2000);
server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setExecutePoolSize(2000);
server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketQueueLimit(10);
server.getNetConfig().setPacketFloodDisconnect(true); server.getNetConfig().setPacketFloodDisconnect(true);

View File

@ -36,6 +36,11 @@ GameserverPort = 7777
# Default: 100 # Default: 100
ClientReadPoolSize = 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. # Client pool size for executing client packets.
# Each pool is executed on a separate thread. # Each pool is executed on a separate thread.
# Default: 50 # Default: 50

View File

@ -780,6 +780,7 @@ public class Config
public static int LOGIN_BLOCK_AFTER_BAN; public static int LOGIN_BLOCK_AFTER_BAN;
public static String GAMESERVER_HOSTNAME; public static String GAMESERVER_HOSTNAME;
public static int CLIENT_READ_POOL_SIZE; public static int CLIENT_READ_POOL_SIZE;
public static int CLIENT_SEND_POOL_SIZE;
public static int CLIENT_EXECUTE_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE;
public static int PACKET_QUEUE_LIMIT; public static int PACKET_QUEUE_LIMIT;
public static boolean PACKET_FLOOD_DISCONNECT; public static boolean PACKET_FLOOD_DISCONNECT;
@ -1387,6 +1388,7 @@ public class Config
GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014);
GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1");
CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100);
CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25);
CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50);
PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80);
PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false);

View File

@ -16,6 +16,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final PacketHandlerInterface<E> _packetHandler; private final PacketHandlerInterface<E> _packetHandler;
private boolean _idle;
public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler) public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler)
{ {
@ -31,6 +32,8 @@ public class ExecuteThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -40,7 +43,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
continue ITERATE; continue ITERATE;
} }
final byte[] data = client.getPacketData().poll(); final byte[] data = client.getReceivedData().poll();
if (data == null) if (data == null)
{ {
continue ITERATE; continue ITERATE;
@ -64,16 +67,29 @@ public class ExecuteThread<E extends NetClient> implements Runnable
} }
} }
_packetHandler.handle(client, new ReadablePacket(data)); _packetHandler.handle(client, new ReadablePacket(data));
_idle = false;
}
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -15,12 +15,15 @@ public class NetClient
{ {
protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName());
private final Queue<byte[]> _receivedData = new ConcurrentLinkedQueue<>();
private final Queue<WritablePacket> _sendPacketQueue = new ConcurrentLinkedQueue<>();
private String _ip; private String _ip;
private Socket _socket; private Socket _socket;
private InputStream _inputStream; private InputStream _inputStream;
private OutputStream _outputStream; private OutputStream _outputStream;
private NetConfig _netConfig; private NetConfig _netConfig;
private Queue<byte[]> _packetData;
private byte[] _pendingData; private byte[] _pendingData;
private int _pendingPacketSize; private int _pendingPacketSize;
@ -33,7 +36,6 @@ public class NetClient
{ {
_socket = socket; _socket = socket;
_netConfig = netConfig; _netConfig = netConfig;
_packetData = new ConcurrentLinkedQueue<>();
_ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1
try try
@ -82,10 +84,8 @@ public class NetClient
} }
} }
if (_packetData != null) _receivedData.clear();
{ _sendPacketQueue.clear();
_packetData.clear();
}
if (_pendingData != null) if (_pendingData != null)
{ {
@ -97,10 +97,10 @@ public class NetClient
* Add packet data to the queue. * Add packet data to the queue.
* @param data * @param data
*/ */
public void addPacketData(byte[] data) public void addReceivedData(byte[] data)
{ {
// Check packet flooding. // Check packet flooding.
final int size = _packetData.size(); final int size = _receivedData.size();
if (size >= _netConfig.getPacketQueueLimit()) if (size >= _netConfig.getPacketQueueLimit())
{ {
if (_netConfig.isPacketFloodDisconnect()) if (_netConfig.isPacketFloodDisconnect())
@ -127,15 +127,15 @@ public class NetClient
} }
// Add to queue. // Add to queue.
_packetData.add(data); _receivedData.add(data);
} }
/** /**
* @return the pending packet data. * @return the pending received data.
*/ */
public Queue<byte[]> getPacketData() public Queue<byte[]> getReceivedData()
{ {
return _packetData; return _receivedData;
} }
/** /**
@ -172,34 +172,21 @@ public class NetClient
_pendingPacketSize = pendingPacketSize; _pendingPacketSize = pendingPacketSize;
} }
/**
* @return the writable packet queue waiting to be sent.
*/
public Queue<WritablePacket> getSendPacketQueue()
{
return _sendPacketQueue;
}
/** /**
* Sends a packet over the network using the default encryption. * Sends a packet over the network using the default encryption.
* @param packet The packet to send. * @param packet The packet to send.
*/ */
public void sendPacket(WritablePacket packet) public void sendPacket(WritablePacket packet)
{ {
if ((_socket == null) || !_socket.isConnected()) _sendPacketQueue.add(packet);
{
return;
}
final byte[] sendableBytes = packet.getSendableBytes(getEncryption());
if (sendableBytes == null)
{
return;
}
try
{
synchronized (this)
{
_outputStream.write(sendableBytes);
_outputStream.flush();
}
}
catch (Exception ignored)
{
}
} }
/** /**

View File

@ -7,6 +7,7 @@ package org.l2jmobius.commons.network;
public class NetConfig public class NetConfig
{ {
private int _readPoolSize = 100; private int _readPoolSize = 100;
private int _sendPoolSize = 25;
private int _executePoolSize = 50; private int _executePoolSize = 50;
private int _packetQueueLimit = 80; private int _packetQueueLimit = 80;
private boolean _packetFloodDisconnect = false; private boolean _packetFloodDisconnect = false;
@ -32,6 +33,23 @@ public class NetConfig
_readPoolSize = clientPoolSize; _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. * @return the NetClient pool size for executing client packets.
*/ */

View File

@ -20,6 +20,7 @@ public class NetServer<E extends NetClient>
protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName());
protected final List<Set<E>> _clientReadPools = new LinkedList<>(); protected final List<Set<E>> _clientReadPools = new LinkedList<>();
protected final List<Set<E>> _clientSendPools = new LinkedList<>();
protected final List<Set<E>> _clientExecutePools = new LinkedList<>(); protected final List<Set<E>> _clientExecutePools = new LinkedList<>();
protected final NetConfig _netConfig = new NetConfig(); protected final NetConfig _netConfig = new NetConfig();
protected final String _hostname; protected final String _hostname;
@ -155,6 +156,35 @@ public class NetServer<E extends NetClient>
_clientReadPools.add(newReadPool); _clientReadPools.add(newReadPool);
} }
// Add to send pool.
// Find a pool that is not full.
boolean sendPoolFound = false;
SEND_POOLS: for (Set<E> 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<E> 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. // Add to execute pool.
// Find a pool that is not full. // Find a pool that is not full.

View File

@ -14,6 +14,7 @@ public class ReadThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer.
private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer.
private boolean _idle;
public ReadThread(Set<E> pool) public ReadThread(Set<E> pool)
{ {
@ -28,6 +29,8 @@ public class ReadThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -80,14 +83,16 @@ public class ReadThread<E extends NetClient> implements Runnable
// Read was complete. // Read was complete.
if (currentSize >= pendingPacketSize) if (currentSize >= pendingPacketSize)
{ {
// Add packet data to client. // Add received data to client.
client.addPacketData(mergedData); client.addReceivedData(mergedData);
client.setPendingData(null); client.setPendingData(null);
} }
else else
{ {
client.setPendingData(mergedData); client.setPendingData(mergedData);
} }
_idle = false;
} }
} }
continue ITERATE; continue ITERATE;
@ -188,10 +193,12 @@ public class ReadThread<E extends NetClient> implements Runnable
client.setPendingData(pendindBytes); client.setPendingData(pendindBytes);
client.setPendingPacketSize(packetSize); 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<E extends NetClient> implements Runnable
onDisconnection(client); onDisconnection(client);
} }
} }
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -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 <E> extends NetClient
*/
public class SendThread<E extends NetClient> 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<E> _pool;
private boolean _idle;
public SendThread(Set<E> 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<WritablePacket> 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)
{
}
}
}
}
}

View File

@ -223,6 +223,14 @@ public abstract class WritablePacket
// Overridden by server implementation. // Overridden by server implementation.
} }
/**
* Method that runs after packet is sent.
*/
public void run()
{
// Overridden by server implementation.
}
/** /**
* @return <b>byte[]</b> of the sendable packet data, including a size header. * @return <b>byte[]</b> of the sendable packet data, including a size header.
*/ */

View File

@ -464,6 +464,7 @@ public class GameServer
final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); 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().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE);
server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT);
server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT);

View File

@ -20,8 +20,6 @@ import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -66,7 +64,6 @@ public class GameClient extends NetClient
{ {
protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting");
private final Queue<ServerPacket> _pendingPackets = new ConcurrentLinkedQueue<>();
private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final FloodProtectors _floodProtectors = new FloodProtectors(this);
private final ReentrantLock _playerLock = new ReentrantLock(); private final ReentrantLock _playerLock = new ReentrantLock();
private ConnectionState _connectionState = ConnectionState.CONNECTED; private ConnectionState _connectionState = ConnectionState.CONNECTED;
@ -218,16 +215,11 @@ public class GameClient extends NetClient
} }
} }
// Keep the order of packets if sent by multiple threads. // Used by packet run() method.
_pendingPackets.add(packet); packet.setPlayer(_player);
synchronized (_pendingPackets)
{ // Send the packet data.
// Send the packet data. super.sendPacket(packet);
super.sendPacket(packet);
// Run packet implementation.
packet.run(_player);
}
} }
public void sendPacket(SystemMessageId systemMessageId) public void sendPacket(SystemMessageId systemMessageId)

View File

@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.clearHtmlActions(getScope()); player.clearHtmlActions(getScope());
} }
if (_disabledValidation) if (_disabledValidation)
{ {
return; return;

View File

@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.broadcastSnoop(_chatType, _senderName, _text); player.broadcastSnoop(_chatType, _senderName, _text);

View File

@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket
super(initialSize); super(initialSize);
} }
private Player _player;
/** /**
* Method that runs after packet is sent. * @return the Player
* @param 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) protected void writeOptionalInt(int value)

View File

@ -136,6 +136,7 @@ public class LoginServer
final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(2000); server.getNetConfig().setReadPoolSize(2000);
server.getNetConfig().setSendPoolSize(2000);
server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setExecutePoolSize(2000);
server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketQueueLimit(10);
server.getNetConfig().setPacketFloodDisconnect(true); server.getNetConfig().setPacketFloodDisconnect(true);

View File

@ -36,6 +36,11 @@ GameserverPort = 7777
# Default: 100 # Default: 100
ClientReadPoolSize = 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. # Client pool size for executing client packets.
# Each pool is executed on a separate thread. # Each pool is executed on a separate thread.
# Default: 50 # Default: 50

View File

@ -767,6 +767,7 @@ public class Config
public static int LOGIN_BLOCK_AFTER_BAN; public static int LOGIN_BLOCK_AFTER_BAN;
public static String GAMESERVER_HOSTNAME; public static String GAMESERVER_HOSTNAME;
public static int CLIENT_READ_POOL_SIZE; public static int CLIENT_READ_POOL_SIZE;
public static int CLIENT_SEND_POOL_SIZE;
public static int CLIENT_EXECUTE_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE;
public static int PACKET_QUEUE_LIMIT; public static int PACKET_QUEUE_LIMIT;
public static boolean PACKET_FLOOD_DISCONNECT; public static boolean PACKET_FLOOD_DISCONNECT;
@ -1374,6 +1375,7 @@ public class Config
GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014);
GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1");
CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100);
CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25);
CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50);
PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80);
PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false);

View File

@ -16,6 +16,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final PacketHandlerInterface<E> _packetHandler; private final PacketHandlerInterface<E> _packetHandler;
private boolean _idle;
public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler) public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler)
{ {
@ -31,6 +32,8 @@ public class ExecuteThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -40,7 +43,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
continue ITERATE; continue ITERATE;
} }
final byte[] data = client.getPacketData().poll(); final byte[] data = client.getReceivedData().poll();
if (data == null) if (data == null)
{ {
continue ITERATE; continue ITERATE;
@ -64,16 +67,29 @@ public class ExecuteThread<E extends NetClient> implements Runnable
} }
} }
_packetHandler.handle(client, new ReadablePacket(data)); _packetHandler.handle(client, new ReadablePacket(data));
_idle = false;
}
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -15,12 +15,15 @@ public class NetClient
{ {
protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName());
private final Queue<byte[]> _receivedData = new ConcurrentLinkedQueue<>();
private final Queue<WritablePacket> _sendPacketQueue = new ConcurrentLinkedQueue<>();
private String _ip; private String _ip;
private Socket _socket; private Socket _socket;
private InputStream _inputStream; private InputStream _inputStream;
private OutputStream _outputStream; private OutputStream _outputStream;
private NetConfig _netConfig; private NetConfig _netConfig;
private Queue<byte[]> _packetData;
private byte[] _pendingData; private byte[] _pendingData;
private int _pendingPacketSize; private int _pendingPacketSize;
@ -33,7 +36,6 @@ public class NetClient
{ {
_socket = socket; _socket = socket;
_netConfig = netConfig; _netConfig = netConfig;
_packetData = new ConcurrentLinkedQueue<>();
_ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1
try try
@ -82,10 +84,8 @@ public class NetClient
} }
} }
if (_packetData != null) _receivedData.clear();
{ _sendPacketQueue.clear();
_packetData.clear();
}
if (_pendingData != null) if (_pendingData != null)
{ {
@ -97,10 +97,10 @@ public class NetClient
* Add packet data to the queue. * Add packet data to the queue.
* @param data * @param data
*/ */
public void addPacketData(byte[] data) public void addReceivedData(byte[] data)
{ {
// Check packet flooding. // Check packet flooding.
final int size = _packetData.size(); final int size = _receivedData.size();
if (size >= _netConfig.getPacketQueueLimit()) if (size >= _netConfig.getPacketQueueLimit())
{ {
if (_netConfig.isPacketFloodDisconnect()) if (_netConfig.isPacketFloodDisconnect())
@ -127,15 +127,15 @@ public class NetClient
} }
// Add to queue. // Add to queue.
_packetData.add(data); _receivedData.add(data);
} }
/** /**
* @return the pending packet data. * @return the pending received data.
*/ */
public Queue<byte[]> getPacketData() public Queue<byte[]> getReceivedData()
{ {
return _packetData; return _receivedData;
} }
/** /**
@ -172,34 +172,21 @@ public class NetClient
_pendingPacketSize = pendingPacketSize; _pendingPacketSize = pendingPacketSize;
} }
/**
* @return the writable packet queue waiting to be sent.
*/
public Queue<WritablePacket> getSendPacketQueue()
{
return _sendPacketQueue;
}
/** /**
* Sends a packet over the network using the default encryption. * Sends a packet over the network using the default encryption.
* @param packet The packet to send. * @param packet The packet to send.
*/ */
public void sendPacket(WritablePacket packet) public void sendPacket(WritablePacket packet)
{ {
if ((_socket == null) || !_socket.isConnected()) _sendPacketQueue.add(packet);
{
return;
}
final byte[] sendableBytes = packet.getSendableBytes(getEncryption());
if (sendableBytes == null)
{
return;
}
try
{
synchronized (this)
{
_outputStream.write(sendableBytes);
_outputStream.flush();
}
}
catch (Exception ignored)
{
}
} }
/** /**

View File

@ -7,6 +7,7 @@ package org.l2jmobius.commons.network;
public class NetConfig public class NetConfig
{ {
private int _readPoolSize = 100; private int _readPoolSize = 100;
private int _sendPoolSize = 25;
private int _executePoolSize = 50; private int _executePoolSize = 50;
private int _packetQueueLimit = 80; private int _packetQueueLimit = 80;
private boolean _packetFloodDisconnect = false; private boolean _packetFloodDisconnect = false;
@ -32,6 +33,23 @@ public class NetConfig
_readPoolSize = clientPoolSize; _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. * @return the NetClient pool size for executing client packets.
*/ */

View File

@ -20,6 +20,7 @@ public class NetServer<E extends NetClient>
protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName());
protected final List<Set<E>> _clientReadPools = new LinkedList<>(); protected final List<Set<E>> _clientReadPools = new LinkedList<>();
protected final List<Set<E>> _clientSendPools = new LinkedList<>();
protected final List<Set<E>> _clientExecutePools = new LinkedList<>(); protected final List<Set<E>> _clientExecutePools = new LinkedList<>();
protected final NetConfig _netConfig = new NetConfig(); protected final NetConfig _netConfig = new NetConfig();
protected final String _hostname; protected final String _hostname;
@ -155,6 +156,35 @@ public class NetServer<E extends NetClient>
_clientReadPools.add(newReadPool); _clientReadPools.add(newReadPool);
} }
// Add to send pool.
// Find a pool that is not full.
boolean sendPoolFound = false;
SEND_POOLS: for (Set<E> 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<E> 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. // Add to execute pool.
// Find a pool that is not full. // Find a pool that is not full.

View File

@ -14,6 +14,7 @@ public class ReadThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer.
private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer.
private boolean _idle;
public ReadThread(Set<E> pool) public ReadThread(Set<E> pool)
{ {
@ -28,6 +29,8 @@ public class ReadThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -80,14 +83,16 @@ public class ReadThread<E extends NetClient> implements Runnable
// Read was complete. // Read was complete.
if (currentSize >= pendingPacketSize) if (currentSize >= pendingPacketSize)
{ {
// Add packet data to client. // Add received data to client.
client.addPacketData(mergedData); client.addReceivedData(mergedData);
client.setPendingData(null); client.setPendingData(null);
} }
else else
{ {
client.setPendingData(mergedData); client.setPendingData(mergedData);
} }
_idle = false;
} }
} }
continue ITERATE; continue ITERATE;
@ -188,10 +193,12 @@ public class ReadThread<E extends NetClient> implements Runnable
client.setPendingData(pendindBytes); client.setPendingData(pendindBytes);
client.setPendingPacketSize(packetSize); 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<E extends NetClient> implements Runnable
onDisconnection(client); onDisconnection(client);
} }
} }
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -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 <E> extends NetClient
*/
public class SendThread<E extends NetClient> 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<E> _pool;
private boolean _idle;
public SendThread(Set<E> 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<WritablePacket> 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)
{
}
}
}
}
}

View File

@ -223,6 +223,14 @@ public abstract class WritablePacket
// Overridden by server implementation. // Overridden by server implementation.
} }
/**
* Method that runs after packet is sent.
*/
public void run()
{
// Overridden by server implementation.
}
/** /**
* @return <b>byte[]</b> of the sendable packet data, including a size header. * @return <b>byte[]</b> of the sendable packet data, including a size header.
*/ */

View File

@ -464,6 +464,7 @@ public class GameServer
final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); 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().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE);
server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT);
server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT);

View File

@ -20,8 +20,6 @@ import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -66,7 +64,6 @@ public class GameClient extends NetClient
{ {
protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting");
private final Queue<ServerPacket> _pendingPackets = new ConcurrentLinkedQueue<>();
private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final FloodProtectors _floodProtectors = new FloodProtectors(this);
private final ReentrantLock _playerLock = new ReentrantLock(); private final ReentrantLock _playerLock = new ReentrantLock();
private ConnectionState _connectionState = ConnectionState.CONNECTED; private ConnectionState _connectionState = ConnectionState.CONNECTED;
@ -218,16 +215,11 @@ public class GameClient extends NetClient
} }
} }
// Keep the order of packets if sent by multiple threads. // Used by packet run() method.
_pendingPackets.add(packet); packet.setPlayer(_player);
synchronized (_pendingPackets)
{ // Send the packet data.
// Send the packet data. super.sendPacket(packet);
super.sendPacket(packet);
// Run packet implementation.
packet.run(_player);
}
} }
public void sendPacket(SystemMessageId systemMessageId) public void sendPacket(SystemMessageId systemMessageId)

View File

@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.clearHtmlActions(getScope()); player.clearHtmlActions(getScope());
} }
if (_disabledValidation) if (_disabledValidation)
{ {
return; return;

View File

@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.broadcastSnoop(_chatType, _senderName, _text); player.broadcastSnoop(_chatType, _senderName, _text);

View File

@ -112,12 +112,22 @@ public abstract class ServerPacket extends WritablePacket
super(initialSize); super(initialSize);
} }
private Player _player;
/** /**
* Method that runs after packet is sent. * @return the Player
* @param 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) protected void writeOptionalInt(int value)

View File

@ -136,6 +136,7 @@ public class LoginServer
final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(2000); server.getNetConfig().setReadPoolSize(2000);
server.getNetConfig().setSendPoolSize(2000);
server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setExecutePoolSize(2000);
server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketQueueLimit(10);
server.getNetConfig().setPacketFloodDisconnect(true); server.getNetConfig().setPacketFloodDisconnect(true);

View File

@ -36,6 +36,11 @@ GameserverPort = 7777
# Default: 100 # Default: 100
ClientReadPoolSize = 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. # Client pool size for executing client packets.
# Each pool is executed on a separate thread. # Each pool is executed on a separate thread.
# Default: 50 # Default: 50

View File

@ -766,6 +766,7 @@ public class Config
public static int LOGIN_BLOCK_AFTER_BAN; public static int LOGIN_BLOCK_AFTER_BAN;
public static String GAMESERVER_HOSTNAME; public static String GAMESERVER_HOSTNAME;
public static int CLIENT_READ_POOL_SIZE; public static int CLIENT_READ_POOL_SIZE;
public static int CLIENT_SEND_POOL_SIZE;
public static int CLIENT_EXECUTE_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE;
public static int PACKET_QUEUE_LIMIT; public static int PACKET_QUEUE_LIMIT;
public static boolean PACKET_FLOOD_DISCONNECT; public static boolean PACKET_FLOOD_DISCONNECT;
@ -1373,6 +1374,7 @@ public class Config
GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014);
GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1");
CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100);
CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25);
CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50);
PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80);
PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false);

View File

@ -16,6 +16,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final PacketHandlerInterface<E> _packetHandler; private final PacketHandlerInterface<E> _packetHandler;
private boolean _idle;
public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler) public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler)
{ {
@ -31,6 +32,8 @@ public class ExecuteThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -40,7 +43,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
continue ITERATE; continue ITERATE;
} }
final byte[] data = client.getPacketData().poll(); final byte[] data = client.getReceivedData().poll();
if (data == null) if (data == null)
{ {
continue ITERATE; continue ITERATE;
@ -64,16 +67,29 @@ public class ExecuteThread<E extends NetClient> implements Runnable
} }
} }
_packetHandler.handle(client, new ReadablePacket(data)); _packetHandler.handle(client, new ReadablePacket(data));
_idle = false;
}
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -15,12 +15,15 @@ public class NetClient
{ {
protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName());
private final Queue<byte[]> _receivedData = new ConcurrentLinkedQueue<>();
private final Queue<WritablePacket> _sendPacketQueue = new ConcurrentLinkedQueue<>();
private String _ip; private String _ip;
private Socket _socket; private Socket _socket;
private InputStream _inputStream; private InputStream _inputStream;
private OutputStream _outputStream; private OutputStream _outputStream;
private NetConfig _netConfig; private NetConfig _netConfig;
private Queue<byte[]> _packetData;
private byte[] _pendingData; private byte[] _pendingData;
private int _pendingPacketSize; private int _pendingPacketSize;
@ -33,7 +36,6 @@ public class NetClient
{ {
_socket = socket; _socket = socket;
_netConfig = netConfig; _netConfig = netConfig;
_packetData = new ConcurrentLinkedQueue<>();
_ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1
try try
@ -82,10 +84,8 @@ public class NetClient
} }
} }
if (_packetData != null) _receivedData.clear();
{ _sendPacketQueue.clear();
_packetData.clear();
}
if (_pendingData != null) if (_pendingData != null)
{ {
@ -97,10 +97,10 @@ public class NetClient
* Add packet data to the queue. * Add packet data to the queue.
* @param data * @param data
*/ */
public void addPacketData(byte[] data) public void addReceivedData(byte[] data)
{ {
// Check packet flooding. // Check packet flooding.
final int size = _packetData.size(); final int size = _receivedData.size();
if (size >= _netConfig.getPacketQueueLimit()) if (size >= _netConfig.getPacketQueueLimit())
{ {
if (_netConfig.isPacketFloodDisconnect()) if (_netConfig.isPacketFloodDisconnect())
@ -127,15 +127,15 @@ public class NetClient
} }
// Add to queue. // Add to queue.
_packetData.add(data); _receivedData.add(data);
} }
/** /**
* @return the pending packet data. * @return the pending received data.
*/ */
public Queue<byte[]> getPacketData() public Queue<byte[]> getReceivedData()
{ {
return _packetData; return _receivedData;
} }
/** /**
@ -172,34 +172,21 @@ public class NetClient
_pendingPacketSize = pendingPacketSize; _pendingPacketSize = pendingPacketSize;
} }
/**
* @return the writable packet queue waiting to be sent.
*/
public Queue<WritablePacket> getSendPacketQueue()
{
return _sendPacketQueue;
}
/** /**
* Sends a packet over the network using the default encryption. * Sends a packet over the network using the default encryption.
* @param packet The packet to send. * @param packet The packet to send.
*/ */
public void sendPacket(WritablePacket packet) public void sendPacket(WritablePacket packet)
{ {
if ((_socket == null) || !_socket.isConnected()) _sendPacketQueue.add(packet);
{
return;
}
final byte[] sendableBytes = packet.getSendableBytes(getEncryption());
if (sendableBytes == null)
{
return;
}
try
{
synchronized (this)
{
_outputStream.write(sendableBytes);
_outputStream.flush();
}
}
catch (Exception ignored)
{
}
} }
/** /**

View File

@ -7,6 +7,7 @@ package org.l2jmobius.commons.network;
public class NetConfig public class NetConfig
{ {
private int _readPoolSize = 100; private int _readPoolSize = 100;
private int _sendPoolSize = 25;
private int _executePoolSize = 50; private int _executePoolSize = 50;
private int _packetQueueLimit = 80; private int _packetQueueLimit = 80;
private boolean _packetFloodDisconnect = false; private boolean _packetFloodDisconnect = false;
@ -32,6 +33,23 @@ public class NetConfig
_readPoolSize = clientPoolSize; _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. * @return the NetClient pool size for executing client packets.
*/ */

View File

@ -20,6 +20,7 @@ public class NetServer<E extends NetClient>
protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName());
protected final List<Set<E>> _clientReadPools = new LinkedList<>(); protected final List<Set<E>> _clientReadPools = new LinkedList<>();
protected final List<Set<E>> _clientSendPools = new LinkedList<>();
protected final List<Set<E>> _clientExecutePools = new LinkedList<>(); protected final List<Set<E>> _clientExecutePools = new LinkedList<>();
protected final NetConfig _netConfig = new NetConfig(); protected final NetConfig _netConfig = new NetConfig();
protected final String _hostname; protected final String _hostname;
@ -155,6 +156,35 @@ public class NetServer<E extends NetClient>
_clientReadPools.add(newReadPool); _clientReadPools.add(newReadPool);
} }
// Add to send pool.
// Find a pool that is not full.
boolean sendPoolFound = false;
SEND_POOLS: for (Set<E> 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<E> 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. // Add to execute pool.
// Find a pool that is not full. // Find a pool that is not full.

View File

@ -14,6 +14,7 @@ public class ReadThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer.
private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer.
private boolean _idle;
public ReadThread(Set<E> pool) public ReadThread(Set<E> pool)
{ {
@ -28,6 +29,8 @@ public class ReadThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -80,14 +83,16 @@ public class ReadThread<E extends NetClient> implements Runnable
// Read was complete. // Read was complete.
if (currentSize >= pendingPacketSize) if (currentSize >= pendingPacketSize)
{ {
// Add packet data to client. // Add received data to client.
client.addPacketData(mergedData); client.addReceivedData(mergedData);
client.setPendingData(null); client.setPendingData(null);
} }
else else
{ {
client.setPendingData(mergedData); client.setPendingData(mergedData);
} }
_idle = false;
} }
} }
continue ITERATE; continue ITERATE;
@ -188,10 +193,12 @@ public class ReadThread<E extends NetClient> implements Runnable
client.setPendingData(pendindBytes); client.setPendingData(pendindBytes);
client.setPendingPacketSize(packetSize); 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<E extends NetClient> implements Runnable
onDisconnection(client); onDisconnection(client);
} }
} }
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -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 <E> extends NetClient
*/
public class SendThread<E extends NetClient> 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<E> _pool;
private boolean _idle;
public SendThread(Set<E> 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<WritablePacket> 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)
{
}
}
}
}
}

View File

@ -223,6 +223,14 @@ public abstract class WritablePacket
// Overridden by server implementation. // Overridden by server implementation.
} }
/**
* Method that runs after packet is sent.
*/
public void run()
{
// Overridden by server implementation.
}
/** /**
* @return <b>byte[]</b> of the sendable packet data, including a size header. * @return <b>byte[]</b> of the sendable packet data, including a size header.
*/ */

View File

@ -468,6 +468,7 @@ public class GameServer
final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); 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().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE);
server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT);
server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT);

View File

@ -20,8 +20,6 @@ import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -66,7 +64,6 @@ public class GameClient extends NetClient
{ {
protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting");
private final Queue<ServerPacket> _pendingPackets = new ConcurrentLinkedQueue<>();
private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final FloodProtectors _floodProtectors = new FloodProtectors(this);
private final ReentrantLock _playerLock = new ReentrantLock(); private final ReentrantLock _playerLock = new ReentrantLock();
private ConnectionState _connectionState = ConnectionState.CONNECTED; private ConnectionState _connectionState = ConnectionState.CONNECTED;
@ -218,16 +215,11 @@ public class GameClient extends NetClient
} }
} }
// Keep the order of packets if sent by multiple threads. // Used by packet run() method.
_pendingPackets.add(packet); packet.setPlayer(_player);
synchronized (_pendingPackets)
{ // Send the packet data.
// Send the packet data. super.sendPacket(packet);
super.sendPacket(packet);
// Run packet implementation.
packet.run(_player);
}
} }
public void sendPacket(SystemMessageId systemMessageId) public void sendPacket(SystemMessageId systemMessageId)

View File

@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.clearHtmlActions(getScope()); player.clearHtmlActions(getScope());
} }
if (_disabledValidation) if (_disabledValidation)
{ {
return; return;

View File

@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.broadcastSnoop(_chatType, _senderName, _text); player.broadcastSnoop(_chatType, _senderName, _text);

View File

@ -117,12 +117,22 @@ public abstract class ServerPacket extends WritablePacket
super(initialSize); super(initialSize);
} }
private Player _player;
/** /**
* Method that runs after packet is sent. * @return the Player
* @param 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) protected void writeOptionalInt(int value)

View File

@ -136,6 +136,7 @@ public class LoginServer
final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(2000); server.getNetConfig().setReadPoolSize(2000);
server.getNetConfig().setSendPoolSize(2000);
server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setExecutePoolSize(2000);
server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketQueueLimit(10);
server.getNetConfig().setPacketFloodDisconnect(true); server.getNetConfig().setPacketFloodDisconnect(true);

View File

@ -36,6 +36,11 @@ GameserverPort = 7777
# Default: 100 # Default: 100
ClientReadPoolSize = 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. # Client pool size for executing client packets.
# Each pool is executed on a separate thread. # Each pool is executed on a separate thread.
# Default: 50 # Default: 50

View File

@ -766,6 +766,7 @@ public class Config
public static int LOGIN_BLOCK_AFTER_BAN; public static int LOGIN_BLOCK_AFTER_BAN;
public static String GAMESERVER_HOSTNAME; public static String GAMESERVER_HOSTNAME;
public static int CLIENT_READ_POOL_SIZE; public static int CLIENT_READ_POOL_SIZE;
public static int CLIENT_SEND_POOL_SIZE;
public static int CLIENT_EXECUTE_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE;
public static int PACKET_QUEUE_LIMIT; public static int PACKET_QUEUE_LIMIT;
public static boolean PACKET_FLOOD_DISCONNECT; public static boolean PACKET_FLOOD_DISCONNECT;
@ -1380,6 +1381,7 @@ public class Config
GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014);
GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1");
CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100);
CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25);
CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50);
PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80);
PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false);

View File

@ -16,6 +16,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final PacketHandlerInterface<E> _packetHandler; private final PacketHandlerInterface<E> _packetHandler;
private boolean _idle;
public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler) public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler)
{ {
@ -31,6 +32,8 @@ public class ExecuteThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -40,7 +43,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
continue ITERATE; continue ITERATE;
} }
final byte[] data = client.getPacketData().poll(); final byte[] data = client.getReceivedData().poll();
if (data == null) if (data == null)
{ {
continue ITERATE; continue ITERATE;
@ -64,16 +67,29 @@ public class ExecuteThread<E extends NetClient> implements Runnable
} }
} }
_packetHandler.handle(client, new ReadablePacket(data)); _packetHandler.handle(client, new ReadablePacket(data));
_idle = false;
}
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -15,12 +15,15 @@ public class NetClient
{ {
protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName());
private final Queue<byte[]> _receivedData = new ConcurrentLinkedQueue<>();
private final Queue<WritablePacket> _sendPacketQueue = new ConcurrentLinkedQueue<>();
private String _ip; private String _ip;
private Socket _socket; private Socket _socket;
private InputStream _inputStream; private InputStream _inputStream;
private OutputStream _outputStream; private OutputStream _outputStream;
private NetConfig _netConfig; private NetConfig _netConfig;
private Queue<byte[]> _packetData;
private byte[] _pendingData; private byte[] _pendingData;
private int _pendingPacketSize; private int _pendingPacketSize;
@ -33,7 +36,6 @@ public class NetClient
{ {
_socket = socket; _socket = socket;
_netConfig = netConfig; _netConfig = netConfig;
_packetData = new ConcurrentLinkedQueue<>();
_ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1
try try
@ -82,10 +84,8 @@ public class NetClient
} }
} }
if (_packetData != null) _receivedData.clear();
{ _sendPacketQueue.clear();
_packetData.clear();
}
if (_pendingData != null) if (_pendingData != null)
{ {
@ -97,10 +97,10 @@ public class NetClient
* Add packet data to the queue. * Add packet data to the queue.
* @param data * @param data
*/ */
public void addPacketData(byte[] data) public void addReceivedData(byte[] data)
{ {
// Check packet flooding. // Check packet flooding.
final int size = _packetData.size(); final int size = _receivedData.size();
if (size >= _netConfig.getPacketQueueLimit()) if (size >= _netConfig.getPacketQueueLimit())
{ {
if (_netConfig.isPacketFloodDisconnect()) if (_netConfig.isPacketFloodDisconnect())
@ -127,15 +127,15 @@ public class NetClient
} }
// Add to queue. // Add to queue.
_packetData.add(data); _receivedData.add(data);
} }
/** /**
* @return the pending packet data. * @return the pending received data.
*/ */
public Queue<byte[]> getPacketData() public Queue<byte[]> getReceivedData()
{ {
return _packetData; return _receivedData;
} }
/** /**
@ -172,34 +172,21 @@ public class NetClient
_pendingPacketSize = pendingPacketSize; _pendingPacketSize = pendingPacketSize;
} }
/**
* @return the writable packet queue waiting to be sent.
*/
public Queue<WritablePacket> getSendPacketQueue()
{
return _sendPacketQueue;
}
/** /**
* Sends a packet over the network using the default encryption. * Sends a packet over the network using the default encryption.
* @param packet The packet to send. * @param packet The packet to send.
*/ */
public void sendPacket(WritablePacket packet) public void sendPacket(WritablePacket packet)
{ {
if ((_socket == null) || !_socket.isConnected()) _sendPacketQueue.add(packet);
{
return;
}
final byte[] sendableBytes = packet.getSendableBytes(getEncryption());
if (sendableBytes == null)
{
return;
}
try
{
synchronized (this)
{
_outputStream.write(sendableBytes);
_outputStream.flush();
}
}
catch (Exception ignored)
{
}
} }
/** /**

View File

@ -7,6 +7,7 @@ package org.l2jmobius.commons.network;
public class NetConfig public class NetConfig
{ {
private int _readPoolSize = 100; private int _readPoolSize = 100;
private int _sendPoolSize = 25;
private int _executePoolSize = 50; private int _executePoolSize = 50;
private int _packetQueueLimit = 80; private int _packetQueueLimit = 80;
private boolean _packetFloodDisconnect = false; private boolean _packetFloodDisconnect = false;
@ -32,6 +33,23 @@ public class NetConfig
_readPoolSize = clientPoolSize; _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. * @return the NetClient pool size for executing client packets.
*/ */

View File

@ -20,6 +20,7 @@ public class NetServer<E extends NetClient>
protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName());
protected final List<Set<E>> _clientReadPools = new LinkedList<>(); protected final List<Set<E>> _clientReadPools = new LinkedList<>();
protected final List<Set<E>> _clientSendPools = new LinkedList<>();
protected final List<Set<E>> _clientExecutePools = new LinkedList<>(); protected final List<Set<E>> _clientExecutePools = new LinkedList<>();
protected final NetConfig _netConfig = new NetConfig(); protected final NetConfig _netConfig = new NetConfig();
protected final String _hostname; protected final String _hostname;
@ -155,6 +156,35 @@ public class NetServer<E extends NetClient>
_clientReadPools.add(newReadPool); _clientReadPools.add(newReadPool);
} }
// Add to send pool.
// Find a pool that is not full.
boolean sendPoolFound = false;
SEND_POOLS: for (Set<E> 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<E> 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. // Add to execute pool.
// Find a pool that is not full. // Find a pool that is not full.

View File

@ -14,6 +14,7 @@ public class ReadThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer.
private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer.
private boolean _idle;
public ReadThread(Set<E> pool) public ReadThread(Set<E> pool)
{ {
@ -28,6 +29,8 @@ public class ReadThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -80,14 +83,16 @@ public class ReadThread<E extends NetClient> implements Runnable
// Read was complete. // Read was complete.
if (currentSize >= pendingPacketSize) if (currentSize >= pendingPacketSize)
{ {
// Add packet data to client. // Add received data to client.
client.addPacketData(mergedData); client.addReceivedData(mergedData);
client.setPendingData(null); client.setPendingData(null);
} }
else else
{ {
client.setPendingData(mergedData); client.setPendingData(mergedData);
} }
_idle = false;
} }
} }
continue ITERATE; continue ITERATE;
@ -188,10 +193,12 @@ public class ReadThread<E extends NetClient> implements Runnable
client.setPendingData(pendindBytes); client.setPendingData(pendindBytes);
client.setPendingPacketSize(packetSize); 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<E extends NetClient> implements Runnable
onDisconnection(client); onDisconnection(client);
} }
} }
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -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 <E> extends NetClient
*/
public class SendThread<E extends NetClient> 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<E> _pool;
private boolean _idle;
public SendThread(Set<E> 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<WritablePacket> 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)
{
}
}
}
}
}

View File

@ -223,6 +223,14 @@ public abstract class WritablePacket
// Overridden by server implementation. // Overridden by server implementation.
} }
/**
* Method that runs after packet is sent.
*/
public void run()
{
// Overridden by server implementation.
}
/** /**
* @return <b>byte[]</b> of the sendable packet data, including a size header. * @return <b>byte[]</b> of the sendable packet data, including a size header.
*/ */

View File

@ -468,6 +468,7 @@ public class GameServer
final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); 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().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE);
server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT);
server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT);

View File

@ -20,8 +20,6 @@ import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -66,7 +64,6 @@ public class GameClient extends NetClient
{ {
protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting"); protected static final Logger LOGGER_ACCOUNTING = Logger.getLogger("accounting");
private final Queue<ServerPacket> _pendingPackets = new ConcurrentLinkedQueue<>();
private final FloodProtectors _floodProtectors = new FloodProtectors(this); private final FloodProtectors _floodProtectors = new FloodProtectors(this);
private final ReentrantLock _playerLock = new ReentrantLock(); private final ReentrantLock _playerLock = new ReentrantLock();
private ConnectionState _connectionState = ConnectionState.CONNECTED; private ConnectionState _connectionState = ConnectionState.CONNECTED;
@ -218,16 +215,11 @@ public class GameClient extends NetClient
} }
} }
// Keep the order of packets if sent by multiple threads. // Used by packet run() method.
_pendingPackets.add(packet); packet.setPlayer(_player);
synchronized (_pendingPackets)
{ // Send the packet data.
// Send the packet data. super.sendPacket(packet);
super.sendPacket(packet);
// Run packet implementation.
packet.run(_player);
}
} }
public void sendPacket(SystemMessageId systemMessageId) public void sendPacket(SystemMessageId systemMessageId)

View File

@ -131,12 +131,14 @@ public abstract class AbstractHtmlPacket extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.clearHtmlActions(getScope()); player.clearHtmlActions(getScope());
} }
if (_disabledValidation) if (_disabledValidation)
{ {
return; return;

View File

@ -162,8 +162,9 @@ public class CreatureSay extends ServerPacket
} }
@Override @Override
public void run(Player player) public void run()
{ {
final Player player = getPlayer();
if (player != null) if (player != null)
{ {
player.broadcastSnoop(_chatType, _senderName, _text); player.broadcastSnoop(_chatType, _senderName, _text);

View File

@ -139,12 +139,22 @@ public abstract class ServerPacket extends WritablePacket
super(initialSize); super(initialSize);
} }
private Player _player;
/** /**
* Method that runs after packet is sent. * @return the Player
* @param 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) protected void writeOptionalInt(int value)

View File

@ -136,6 +136,7 @@ public class LoginServer
final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new); final NetServer<LoginClient> server = new NetServer<>(Config.LOGIN_BIND_ADDRESS, Config.PORT_LOGIN, new LoginPacketHandler(), LoginClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(2000); server.getNetConfig().setReadPoolSize(2000);
server.getNetConfig().setSendPoolSize(2000);
server.getNetConfig().setExecutePoolSize(2000); server.getNetConfig().setExecutePoolSize(2000);
server.getNetConfig().setPacketQueueLimit(10); server.getNetConfig().setPacketQueueLimit(10);
server.getNetConfig().setPacketFloodDisconnect(true); server.getNetConfig().setPacketFloodDisconnect(true);

View File

@ -36,6 +36,11 @@ GameserverPort = 7777
# Default: 100 # Default: 100
ClientReadPoolSize = 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. # Client pool size for executing client packets.
# Each pool is executed on a separate thread. # Each pool is executed on a separate thread.
# Default: 50 # Default: 50

View File

@ -767,6 +767,7 @@ public class Config
public static int LOGIN_BLOCK_AFTER_BAN; public static int LOGIN_BLOCK_AFTER_BAN;
public static String GAMESERVER_HOSTNAME; public static String GAMESERVER_HOSTNAME;
public static int CLIENT_READ_POOL_SIZE; public static int CLIENT_READ_POOL_SIZE;
public static int CLIENT_SEND_POOL_SIZE;
public static int CLIENT_EXECUTE_POOL_SIZE; public static int CLIENT_EXECUTE_POOL_SIZE;
public static int PACKET_QUEUE_LIMIT; public static int PACKET_QUEUE_LIMIT;
public static boolean PACKET_FLOOD_DISCONNECT; public static boolean PACKET_FLOOD_DISCONNECT;
@ -1401,6 +1402,7 @@ public class Config
GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014); GAME_SERVER_LOGIN_PORT = serverConfig.getInt("LoginPort", 9014);
GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1"); GAME_SERVER_LOGIN_HOST = serverConfig.getString("LoginHost", "127.0.0.1");
CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100); CLIENT_READ_POOL_SIZE = serverConfig.getInt("ClientReadPoolSize", 100);
CLIENT_SEND_POOL_SIZE = serverConfig.getInt("ClientSendPoolSize", 25);
CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50); CLIENT_EXECUTE_POOL_SIZE = serverConfig.getInt("ClientExecutePoolSize", 50);
PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80); PACKET_QUEUE_LIMIT = serverConfig.getInt("PacketQueueLimit", 80);
PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false); PACKET_FLOOD_DISCONNECT = serverConfig.getBoolean("PacketFloodDisconnect", false);

View File

@ -16,6 +16,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final PacketHandlerInterface<E> _packetHandler; private final PacketHandlerInterface<E> _packetHandler;
private boolean _idle;
public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler) public ExecuteThread(Set<E> pool, PacketHandlerInterface<E> packetHandler)
{ {
@ -31,6 +32,8 @@ public class ExecuteThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -40,7 +43,7 @@ public class ExecuteThread<E extends NetClient> implements Runnable
continue ITERATE; continue ITERATE;
} }
final byte[] data = client.getPacketData().poll(); final byte[] data = client.getReceivedData().poll();
if (data == null) if (data == null)
{ {
continue ITERATE; continue ITERATE;
@ -64,16 +67,29 @@ public class ExecuteThread<E extends NetClient> implements Runnable
} }
} }
_packetHandler.handle(client, new ReadablePacket(data)); _packetHandler.handle(client, new ReadablePacket(data));
_idle = false;
}
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -15,12 +15,15 @@ public class NetClient
{ {
protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetClient.class.getName());
private final Queue<byte[]> _receivedData = new ConcurrentLinkedQueue<>();
private final Queue<WritablePacket> _sendPacketQueue = new ConcurrentLinkedQueue<>();
private String _ip; private String _ip;
private Socket _socket; private Socket _socket;
private InputStream _inputStream; private InputStream _inputStream;
private OutputStream _outputStream; private OutputStream _outputStream;
private NetConfig _netConfig; private NetConfig _netConfig;
private Queue<byte[]> _packetData;
private byte[] _pendingData; private byte[] _pendingData;
private int _pendingPacketSize; private int _pendingPacketSize;
@ -33,7 +36,6 @@ public class NetClient
{ {
_socket = socket; _socket = socket;
_netConfig = netConfig; _netConfig = netConfig;
_packetData = new ConcurrentLinkedQueue<>();
_ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1 _ip = socket.getInetAddress().toString().substring(1); // Trim out /127.0.0.1
try try
@ -82,10 +84,8 @@ public class NetClient
} }
} }
if (_packetData != null) _receivedData.clear();
{ _sendPacketQueue.clear();
_packetData.clear();
}
if (_pendingData != null) if (_pendingData != null)
{ {
@ -97,10 +97,10 @@ public class NetClient
* Add packet data to the queue. * Add packet data to the queue.
* @param data * @param data
*/ */
public void addPacketData(byte[] data) public void addReceivedData(byte[] data)
{ {
// Check packet flooding. // Check packet flooding.
final int size = _packetData.size(); final int size = _receivedData.size();
if (size >= _netConfig.getPacketQueueLimit()) if (size >= _netConfig.getPacketQueueLimit())
{ {
if (_netConfig.isPacketFloodDisconnect()) if (_netConfig.isPacketFloodDisconnect())
@ -127,15 +127,15 @@ public class NetClient
} }
// Add to queue. // Add to queue.
_packetData.add(data); _receivedData.add(data);
} }
/** /**
* @return the pending packet data. * @return the pending received data.
*/ */
public Queue<byte[]> getPacketData() public Queue<byte[]> getReceivedData()
{ {
return _packetData; return _receivedData;
} }
/** /**
@ -172,34 +172,21 @@ public class NetClient
_pendingPacketSize = pendingPacketSize; _pendingPacketSize = pendingPacketSize;
} }
/**
* @return the writable packet queue waiting to be sent.
*/
public Queue<WritablePacket> getSendPacketQueue()
{
return _sendPacketQueue;
}
/** /**
* Sends a packet over the network using the default encryption. * Sends a packet over the network using the default encryption.
* @param packet The packet to send. * @param packet The packet to send.
*/ */
public void sendPacket(WritablePacket packet) public void sendPacket(WritablePacket packet)
{ {
if ((_socket == null) || !_socket.isConnected()) _sendPacketQueue.add(packet);
{
return;
}
final byte[] sendableBytes = packet.getSendableBytes(getEncryption());
if (sendableBytes == null)
{
return;
}
try
{
synchronized (this)
{
_outputStream.write(sendableBytes);
_outputStream.flush();
}
}
catch (Exception ignored)
{
}
} }
/** /**

View File

@ -7,6 +7,7 @@ package org.l2jmobius.commons.network;
public class NetConfig public class NetConfig
{ {
private int _readPoolSize = 100; private int _readPoolSize = 100;
private int _sendPoolSize = 25;
private int _executePoolSize = 50; private int _executePoolSize = 50;
private int _packetQueueLimit = 80; private int _packetQueueLimit = 80;
private boolean _packetFloodDisconnect = false; private boolean _packetFloodDisconnect = false;
@ -32,6 +33,23 @@ public class NetConfig
_readPoolSize = clientPoolSize; _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. * @return the NetClient pool size for executing client packets.
*/ */

View File

@ -20,6 +20,7 @@ public class NetServer<E extends NetClient>
protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName()); protected static final Logger LOGGER = Logger.getLogger(NetServer.class.getName());
protected final List<Set<E>> _clientReadPools = new LinkedList<>(); protected final List<Set<E>> _clientReadPools = new LinkedList<>();
protected final List<Set<E>> _clientSendPools = new LinkedList<>();
protected final List<Set<E>> _clientExecutePools = new LinkedList<>(); protected final List<Set<E>> _clientExecutePools = new LinkedList<>();
protected final NetConfig _netConfig = new NetConfig(); protected final NetConfig _netConfig = new NetConfig();
protected final String _hostname; protected final String _hostname;
@ -155,6 +156,35 @@ public class NetServer<E extends NetClient>
_clientReadPools.add(newReadPool); _clientReadPools.add(newReadPool);
} }
// Add to send pool.
// Find a pool that is not full.
boolean sendPoolFound = false;
SEND_POOLS: for (Set<E> 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<E> 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. // Add to execute pool.
// Find a pool that is not full. // Find a pool that is not full.

View File

@ -14,6 +14,7 @@ public class ReadThread<E extends NetClient> implements Runnable
private final Set<E> _pool; private final Set<E> _pool;
private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer. private final byte[] _sizeBuffer = new byte[2]; // Reusable size buffer.
private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer. private final byte[] _pendingSizeBuffer = new byte[1]; // Reusable pending size buffer.
private boolean _idle;
public ReadThread(Set<E> pool) public ReadThread(Set<E> pool)
{ {
@ -28,6 +29,8 @@ public class ReadThread<E extends NetClient> implements Runnable
// No need to iterate when pool is empty. // No need to iterate when pool is empty.
if (!_pool.isEmpty()) if (!_pool.isEmpty())
{ {
_idle = true;
// Iterate client pool. // Iterate client pool.
ITERATE: for (E client : _pool) ITERATE: for (E client : _pool)
{ {
@ -80,14 +83,16 @@ public class ReadThread<E extends NetClient> implements Runnable
// Read was complete. // Read was complete.
if (currentSize >= pendingPacketSize) if (currentSize >= pendingPacketSize)
{ {
// Add packet data to client. // Add received data to client.
client.addPacketData(mergedData); client.addReceivedData(mergedData);
client.setPendingData(null); client.setPendingData(null);
} }
else else
{ {
client.setPendingData(mergedData); client.setPendingData(mergedData);
} }
_idle = false;
} }
} }
continue ITERATE; continue ITERATE;
@ -188,10 +193,12 @@ public class ReadThread<E extends NetClient> implements Runnable
client.setPendingData(pendindBytes); client.setPendingData(pendindBytes);
client.setPendingPacketSize(packetSize); 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<E extends NetClient> implements Runnable
onDisconnection(client); onDisconnection(client);
} }
} }
// 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(1);
}
catch (Exception ignored)
{ {
// Prevent high CPU caused by repeatedly looping.
try
{
Thread.sleep(1000);
}
catch (Exception ignored)
{
}
} }
} }
} }

View File

@ -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 <E> extends NetClient
*/
public class SendThread<E extends NetClient> 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<E> _pool;
private boolean _idle;
public SendThread(Set<E> 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<WritablePacket> 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)
{
}
}
}
}
}

View File

@ -223,6 +223,14 @@ public abstract class WritablePacket
// Overridden by server implementation. // Overridden by server implementation.
} }
/**
* Method that runs after packet is sent.
*/
public void run()
{
// Overridden by server implementation.
}
/** /**
* @return <b>byte[]</b> of the sendable packet data, including a size header. * @return <b>byte[]</b> of the sendable packet data, including a size header.
*/ */

View File

@ -470,6 +470,7 @@ public class GameServer
final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new); final NetServer<GameClient> server = new NetServer<>(Config.GAMESERVER_HOSTNAME, Config.PORT_GAME, new PacketHandler(), GameClient::new);
server.setName(getClass().getSimpleName()); server.setName(getClass().getSimpleName());
server.getNetConfig().setReadPoolSize(Config.CLIENT_READ_POOL_SIZE); 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().setExecutePoolSize(Config.CLIENT_EXECUTE_POOL_SIZE);
server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT); server.getNetConfig().setPacketQueueLimit(Config.PACKET_QUEUE_LIMIT);
server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT); server.getNetConfig().setPacketFloodDisconnect(Config.PACKET_FLOOD_DISCONNECT);

Some files were not shown because too many files have changed in this diff Show More