48    private const RAKLIB_TPS = 100;
 
   49    private const RAKLIB_TIME_PER_TICK = 1 / self::RAKLIB_TPS;
 
   50    private const BLOCK_MESSAGE_SUPPRESSION_THRESHOLD = 2;
 
   51    private const PACKET_ERROR_SUPPRESSION_THRESHOLD = 2;
 
   53    protected int $receiveBytes = 0;
 
   54    protected int $sendBytes = 0;
 
   57    protected array $sessionsByAddress = [];
 
   59    protected array $sessions = [];
 
   63    protected string $name = 
"";
 
   65    protected int $packetLimit = 200;
 
   67    protected bool $shutdown = 
false;
 
   69    protected int $ticks = 0;
 
   72    protected array $block = [];
 
   74    protected array $ipSec = [];
 
   76    private int $blockedSinceLastUpdate = 0;
 
   78    private int $packetErrorsSinceLastUpdate = 0;
 
   81    protected array $rawPacketFilters = [];
 
   83    public bool $portChecking = 
false;
 
   85    protected int $nextSessionId = 0;
 
   92        protected int $serverId,
 
   95        protected int $maxMtuSize,
 
  100        private int $recvMaxSplitParts = ServerSession::DEFAULT_MAX_SPLIT_PART_COUNT,
 
  101        private int $recvMaxConcurrentSplits = ServerSession::DEFAULT_MAX_CONCURRENT_SPLIT_COUNT,
 
  102        private int $blockMessageSuppressionThreshold = self::BLOCK_MESSAGE_SUPPRESSION_THRESHOLD,
 
  103        private int $packetErrorSuppressionThreshold = self::PACKET_ERROR_SUPPRESSION_THRESHOLD,
 
  104        private bool $blockIpOnPacketErrors = 
true 
  106        if($maxMtuSize < Session::MIN_MTU_SIZE){
 
  107            throw new \InvalidArgumentException(
"MTU size must be at least " . Session::MIN_MTU_SIZE . 
", got $maxMtuSize");
 
  109        $this->socket->setBlocking(
false);
 
 
  114    public function getPort() : int{
 
  115        return $this->socket->getBindAddress()->getPort();
 
  118    public function getMaxMtuSize() : int{
 
  119        return $this->maxMtuSize;
 
  122    public function getLogger() : \
Logger{
 
  123        return $this->logger;
 
  126    public function tickProcessor() : void{
 
  127        $start = microtime(true);
 
  134            $stream = !$this->shutdown;
 
  135            for($i = 0; $i < 100 && $stream && !$this->shutdown; ++$i){ 
 
  136                $stream = $this->eventSource->process($this);
 
  140            for($i = 0; $i < 100 && $socket; ++$i){
 
  141                $socket = $this->receivePacket();
 
  143        }
while($stream || $socket);
 
  147        $time = microtime(
true) - $start;
 
  148        if($time < self::RAKLIB_TIME_PER_TICK){
 
  149            @time_sleep_until(microtime(
true) + self::RAKLIB_TIME_PER_TICK - $time);
 
  157        $this->shutdown = true;
 
  159        while($this->eventSource->process($this)){
 
  165        foreach($this->sessions as $session){
 
  166            $session->initiateDisconnect(DisconnectReason::SERVER_SHUTDOWN);
 
  169        while(count($this->sessions) > 0){
 
  170            $this->tickProcessor();
 
  173        $this->socket->close();
 
  174        $this->logger->debug(
"Graceful shutdown complete");
 
 
  177    private function tick() : void{
 
  178        $time = microtime(true);
 
  179        foreach($this->sessions as $session){
 
  180            $session->update($time);
 
  181            if($session->isFullyDisconnected()){
 
  182                $this->removeSessionInternal($session);
 
  188        if(!$this->shutdown and ($this->ticks % self::RAKLIB_TPS) === 0){
 
  189            if($this->sendBytes > 0 or $this->receiveBytes > 0){
 
  190                $this->eventListener->onBandwidthStatsUpdate($this->sendBytes, $this->receiveBytes);
 
  191                $this->sendBytes = 0;
 
  192                $this->receiveBytes = 0;
 
  195            $packetErrorsWithoutMessage = $this->packetErrorsSinceLastUpdate - $this->packetErrorSuppressionThreshold;
 
  196            if($packetErrorsWithoutMessage > 0){
 
  197                $this->logger->warning(
"$packetErrorsWithoutMessage suppressed packet errors - RakLib may be under attack");
 
  199            $this->packetErrorsSinceLastUpdate = 0;
 
  201            $ipsBlockedWithoutMessage = $this->blockedSinceLastUpdate - $this->blockMessageSuppressionThreshold;
 
  202            if($ipsBlockedWithoutMessage > 0){
 
  203                $this->logger->warning(
"$ipsBlockedWithoutMessage more IP addresses were blocked - RakLib may be under attack");
 
  205            $this->blockedSinceLastUpdate = 0;
 
  207            if(count($this->block) > 0){
 
  210                foreach($this->block as $address => $timeout){
 
  211                    if($timeout <= $now){
 
  212                        unset($this->block[$address]);
 
  224    private function receivePacket() : bool{
 
  226            $buffer = $this->socket->readPacket($addressIp, $addressPort);
 
  227        }
catch(SocketException $e){
 
  228            $error = $e->getCode();
 
  229            if($error === SOCKET_ECONNRESET){ 
 
  233            $this->logger->debug($e->getMessage());
 
  236        if($buffer === 
null){
 
  239        assert($addressIp !== 
null, 
"Can't be null if we got a buffer");
 
  240        assert($addressPort !== 
null, 
"Can't be null if we got a buffer");
 
  242        $len = strlen($buffer);
 
  244        $this->receiveBytes += $len;
 
  245        if(isset($this->block[$addressIp])){
 
  249        if(isset($this->ipSec[$addressIp])){
 
  250            if(++$this->ipSec[$addressIp] >= $this->packetLimit){
 
  251                $this->blockAddress($addressIp);
 
  255            $this->ipSec[$addressIp] = 1;
 
  262        $address = 
new InternetAddress($addressIp, $addressPort, $this->socket->getBindAddress()->getVersion());
 
  264            $session = $this->getSessionByAddress($address);
 
  265            if($session !== 
null){
 
  266                $header = ord($buffer[0]);
 
  267                if(($header & Datagram::BITFLAG_VALID) !== 0){
 
  268                    if(($header & Datagram::BITFLAG_ACK) !== 0){
 
  270                    }elseif(($header & Datagram::BITFLAG_NAK) !== 0){
 
  271                        $packet = 
new NACK();
 
  273                        $packet = 
new Datagram();
 
  275                    $packet->decode(
new PacketSerializer($buffer));
 
  277                        $session->handlePacket($packet);
 
  278                    }
catch(PacketHandlingException $e){
 
  279                        $session->getLogger()->error(
"Error receiving packet: " . $e->getMessage());
 
  280                        $session->forciblyDisconnect($e->getDisconnectReason());
 
  283                }elseif($session->isConnected()){
 
  286                    $this->logger->debug(
"Ignored unconnected packet from $address due to session already opened (0x" . bin2hex($buffer[0]) . 
")");
 
  291            if(!$this->shutdown){
 
  292                if(!($handled = $this->unconnectedMessageHandler->handleRaw($buffer, $address))){
 
  293                    foreach($this->rawPacketFilters as $pattern){
 
  294                        if(preg_match($pattern, $buffer) > 0){
 
  296                            $this->eventListener->onRawPacketReceive($address->getIp(), $address->getPort(), $buffer);
 
  303                    $this->logger->debug(
"Ignored packet from $address due to no session opened (0x" . bin2hex($buffer[0]) . 
")");
 
  306        }
catch(BinaryDataException $e){
 
  307            if($this->packetErrorsSinceLastUpdate < $this->packetErrorSuppressionThreshold){
 
  308                $logFn = 
function() use ($address, $e, $buffer) : 
void{
 
  309                    $this->logger->debug(
"Packet from $address (" . strlen($buffer) . 
" bytes): 0x" . bin2hex($buffer));
 
  310                    $this->logger->debug(get_class($e) . 
": " . $e->getMessage() . 
" in " . $e->getFile() . 
" on line " . $e->getLine());
 
  311                    foreach($this->traceCleaner->getTrace(0, $e->getTrace()) as $line){
 
  312                        $this->logger->debug($line);
 
  314                    $this->logger->error(
"Bad packet from $address: " . $e->getMessage());
 
  317                    $this->logger->buffer($logFn);
 
  322            $this->packetErrorsSinceLastUpdate++;
 
  323            if($this->blockIpOnPacketErrors){
 
  324                $this->blockAddress($address->getIp(), 5);
 
  331    public function sendPacket(Packet $packet, InternetAddress $address) : void{
 
  332        $out = new PacketSerializer(); 
 
  333        $packet->encode($out);
 
  335            $this->sendBytes += $this->socket->writePacket($out->getBuffer(), $address->getIp(), $address->getPort());
 
  336        }
catch(SocketException $e){
 
  337            $this->logger->debug($e->getMessage());
 
  341    public function getEventListener() : ServerEventListener{
 
  342        return $this->eventListener;
 
  345    public function sendEncapsulated(
int $sessionId, EncapsulatedPacket $packet, 
bool $immediate = 
false) : void{
 
  346        $session = $this->sessions[$sessionId] ?? null;
 
  347        if($session !== 
null and $session->isConnected()){
 
  348            $session->addEncapsulatedToQueue($packet, $immediate);
 
  352    public function sendRaw(
string $address, 
int $port, 
string $payload) : void{
 
  354            $this->socket->writePacket($payload, $address, $port);
 
  355        }
catch(SocketException $e){
 
  356            $this->logger->debug($e->getMessage());
 
  360    public function closeSession(
int $sessionId) : void{
 
  361        if(isset($this->sessions[$sessionId])){
 
  362            $this->sessions[$sessionId]->initiateDisconnect(DisconnectReason::SERVER_DISCONNECT);
 
  366    public function setName(
string $name) : void{
 
  370    public function setPortCheck(
bool $value) : void{
 
  371        $this->portChecking = $value;
 
  374    public function setPacketsPerTickLimit(
int $limit) : void{
 
  375        $this->packetLimit = $limit;
 
  378    public function blockAddress(
string $address, 
int $timeout = 300) : void{
 
  379        $final = time() + $timeout;
 
  380        if(!isset($this->block[$address]) or $timeout === -1){
 
  382                $final = PHP_INT_MAX;
 
  384            if($this->blockedSinceLastUpdate < $this->blockMessageSuppressionThreshold){
 
  387                $this->logger->notice(
"Blocked $address" . ($timeout === -1 ? 
" forever" : 
" for $timeout seconds"));
 
  389            $this->block[$address] = $final;
 
  390            $this->blockedSinceLastUpdate++;
 
  391        }elseif($this->block[$address] < $final){
 
  392            $this->block[$address] = $final;
 
  396    public function unblockAddress(
string $address) : void{
 
  397        unset($this->block[$address]);
 
  398        $this->logger->debug(
"Unblocked $address");
 
  401    public function addRawPacketFilter(
string $regex) : void{
 
  402        $this->rawPacketFilters[] = $regex;
 
  405    public function getSessionByAddress(InternetAddress $address) : ?ServerSession{
 
  406        return $this->sessionsByAddress[$address->toString()] ?? null;
 
  409    public function sessionExists(InternetAddress $address) : bool{
 
  410        return isset($this->sessionsByAddress[$address->toString()]);
 
  413    public function createSession(InternetAddress $address, 
int $clientId, 
int $mtuSize) : ServerSession{
 
  414        $existingSession = $this->sessionsByAddress[$address->toString()] ?? null;
 
  415        if($existingSession !== 
null){
 
  416            $existingSession->forciblyDisconnect(DisconnectReason::CLIENT_RECONNECT);
 
  417            $this->removeSessionInternal($existingSession);
 
  420        $this->checkSessions();
 
  422        while(isset($this->sessions[$this->nextSessionId])){
 
  423            $this->nextSessionId++;
 
  424            $this->nextSessionId &= 0x7fffffff; 
 
  427        $session = 
new ServerSession($this, $this->logger, clone $address, $clientId, $mtuSize, $this->nextSessionId, $this->recvMaxSplitParts, $this->recvMaxConcurrentSplits);
 
  428        $this->sessionsByAddress[$address->toString()] = $session;
 
  429        $this->sessions[$this->nextSessionId] = $session;
 
  430        $this->logger->debug(
"Created session for $address with MTU size $mtuSize");
 
  435    private function removeSessionInternal(ServerSession $session) : void{
 
  436        unset($this->sessionsByAddress[$session->getAddress()->toString()], $this->sessions[$session->getInternalId()]);
 
  439    public function openSession(ServerSession $session) : void{
 
  440        $address = $session->getAddress();
 
  441        $this->eventListener->onClientConnect($session->getInternalId(), $address->getIp(), $address->getPort(), $session->getID());
 
  444    private function checkSessions() : void{
 
  445        if(count($this->sessions) > 4096){
 
  446            foreach($this->sessions as $sessionId => $session){
 
  447                if($session->isTemporary()){
 
  448                    $this->removeSessionInternal($session);
 
  449                    if(count($this->sessions) <= 4096){
 
  457    public function getName() : string{
 
  461    public function getID() : int{
 
  462        return $this->serverId;