Inseparable set of network changes - these all need each other to work

- Separated player handling and creation from network interfaces
- Rewire disconnects to make them not be recursive
- Batching now uses sessions instead of players
- Fixed DisconnectPacket getting sent to players who disconnect of their own accord
This commit is contained in:
Dylan K. Taylor 2018-07-21 20:03:05 +01:00
parent a86d3fe071
commit 85105ed066
10 changed files with 160 additions and 88 deletions

View File

@ -125,7 +125,6 @@ use pocketmine\network\mcpe\protocol\types\PlayerPermissions;
use pocketmine\network\mcpe\protocol\UpdateAttributesPacket; use pocketmine\network\mcpe\protocol\UpdateAttributesPacket;
use pocketmine\network\mcpe\protocol\UpdateBlockPacket; use pocketmine\network\mcpe\protocol\UpdateBlockPacket;
use pocketmine\network\mcpe\VerifyLoginTask; use pocketmine\network\mcpe\VerifyLoginTask;
use pocketmine\network\NetworkInterface;
use pocketmine\permission\PermissibleBase; use pocketmine\permission\PermissibleBase;
use pocketmine\permission\PermissionAttachment; use pocketmine\permission\PermissionAttachment;
use pocketmine\permission\PermissionAttachmentInfo; use pocketmine\permission\PermissionAttachmentInfo;
@ -657,13 +656,14 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
} }
/** /**
* @param NetworkInterface $interface * @param Server $server
* @param string $ip * @param NetworkSession $session
* @param int $port
*/ */
public function __construct(NetworkInterface $interface, string $ip, int $port){ public function __construct(Server $server, NetworkSession $session){
$this->server = $server;
$this->networkSession = $session;
$this->perm = new PermissibleBase($this); $this->perm = new PermissibleBase($this);
$this->server = Server::getInstance();
$this->loaderId = Level::generateChunkLoaderId($this); $this->loaderId = Level::generateChunkLoaderId($this);
$this->chunksPerTick = (int) $this->server->getProperty("chunk-sending.per-tick", 4); $this->chunksPerTick = (int) $this->server->getProperty("chunk-sending.per-tick", 4);
$this->spawnThreshold = (int) (($this->server->getProperty("chunk-sending.spawn-radius", 4) ** 2) * M_PI); $this->spawnThreshold = (int) (($this->server->getProperty("chunk-sending.spawn-radius", 4) ** 2) * M_PI);
@ -671,8 +671,6 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
$this->creationTime = microtime(true); $this->creationTime = microtime(true);
$this->allowMovementCheats = (bool) $this->server->getProperty("player.anti-cheat.allow-movement-cheats", false); $this->allowMovementCheats = (bool) $this->server->getProperty("player.anti-cheat.allow-movement-cheats", false);
$this->networkSession = new NetworkSession($this->server, $this, $interface, $ip, $port);
} }
/** /**
@ -906,7 +904,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
$this->usedChunks[Level::chunkHash($x, $z)] = true; $this->usedChunks[Level::chunkHash($x, $z)] = true;
$this->chunkLoadCount++; $this->chunkLoadCount++;
$this->networkSession->getInterface()->putPacket($this, $payload); $this->networkSession->getInterface()->putPacket($this->networkSession, $payload);
if($this->spawned){ if($this->spawned){
foreach($this->level->getChunkEntities($x, $z) as $entity){ foreach($this->level->getChunkEntities($x, $z) as $entity){
@ -2938,7 +2936,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
try{ try{
$ip = $this->networkSession->getIp(); $ip = $this->networkSession->getIp();
$port = $this->networkSession->getPort(); $port = $this->networkSession->getPort();
$this->networkSession->serverDisconnect($reason, $notify); $this->networkSession->onPlayerDestroyed($reason, $notify);
$this->networkSession = null; $this->networkSession = null;
$this->server->getPluginManager()->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_USERS, $this); $this->server->getPluginManager()->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_USERS, $this);

View File

@ -71,6 +71,7 @@ use pocketmine\nbt\tag\StringTag;
use pocketmine\network\AdvancedNetworkInterface; use pocketmine\network\AdvancedNetworkInterface;
use pocketmine\network\mcpe\CompressBatchedTask; use pocketmine\network\mcpe\CompressBatchedTask;
use pocketmine\network\mcpe\NetworkCompression; use pocketmine\network\mcpe\NetworkCompression;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\PacketStream; use pocketmine\network\mcpe\PacketStream;
use pocketmine\network\mcpe\protocol\DataPacket; use pocketmine\network\mcpe\protocol\DataPacket;
use pocketmine\network\mcpe\protocol\PlayerListPacket; use pocketmine\network\mcpe\protocol\PlayerListPacket;
@ -1860,7 +1861,13 @@ class Server{
} }
Timings::$playerNetworkTimer->startTiming(); Timings::$playerNetworkTimer->startTiming();
$targets = array_filter($players, function(Player $player) : bool{ return $player->isConnected(); }); /** @var NetworkSession[] $targets */
$targets = [];
foreach($players as $player){
if($player->isConnected()){
$targets[] = $player->getNetworkSession();
}
}
if(!empty($targets)){ if(!empty($targets)){
$stream = new PacketStream(); $stream = new PacketStream();
@ -1887,13 +1894,14 @@ class Server{
} }
/** /**
* @param string $payload * @param string $payload
* @param Player[] $players * @param NetworkSession[] $sessions
* @param bool $immediate * @param bool $immediate
*/ */
public function broadcastPacketsCallback(string $payload, array $players, bool $immediate = false){ public function broadcastPacketsCallback(string $payload, array $sessions, bool $immediate = false){
foreach($players as $i){ /** @var NetworkSession $session */
$i->getNetworkSession()->getInterface()->putPacket($i, $payload, $immediate); foreach($sessions as $session){
$session->getInterface()->putPacket($session, $payload, $immediate);
} }
} }

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\event\player; namespace pocketmine\event\player;
use pocketmine\event\Event; use pocketmine\event\Event;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\NetworkInterface; use pocketmine\network\NetworkInterface;
use pocketmine\Player; use pocketmine\Player;
@ -31,48 +32,49 @@ use pocketmine\Player;
* Allows the creation of players overriding the base Player class * Allows the creation of players overriding the base Player class
*/ */
class PlayerCreationEvent extends Event{ class PlayerCreationEvent extends Event{
/** @var NetworkInterface */
private $interface; /** @var NetworkSession */
/** @var string */ private $session;
private $address;
/** @var int */
private $port;
/** @var Player::class */ /** @var Player::class */
private $baseClass = Player::class; private $baseClass = Player::class;
/** @var Player::class */ /** @var Player::class */
private $playerClass = Player::class; private $playerClass = Player::class;
/** /**
* @param NetworkInterface $interface * @param NetworkSession $session
* @param string $address
* @param int $port
*/ */
public function __construct(NetworkInterface $interface, string $address, int $port){ public function __construct(NetworkSession $session){
$this->interface = $interface; $this->session = $session;
$this->address = $address;
$this->port = $port;
} }
/** /**
* @return NetworkInterface * @return NetworkInterface
*/ */
public function getInterface() : NetworkInterface{ public function getInterface() : NetworkInterface{
return $this->interface; return $this->session->getInterface();
}
/**
* @return NetworkSession
*/
public function getNetworkSession() : NetworkSession{
return $this->session;
} }
/** /**
* @return string * @return string
*/ */
public function getAddress() : string{ public function getAddress() : string{
return $this->address; return $this->session->getIp();
} }
/** /**
* @return int * @return int
*/ */
public function getPort() : int{ public function getPort() : int{
return $this->port; return $this->session->getPort();
} }
/** /**

View File

@ -26,7 +26,7 @@ declare(strict_types=1);
*/ */
namespace pocketmine\network; namespace pocketmine\network;
use pocketmine\Player; use pocketmine\network\mcpe\NetworkSession;
/** /**
* Network interfaces are transport layers which can be used to transmit packets between the server and clients. * Network interfaces are transport layers which can be used to transmit packets between the server and clients.
@ -41,19 +41,19 @@ interface NetworkInterface{
/** /**
* Sends a DataPacket to the interface, returns an unique identifier for the packet if $needACK is true * Sends a DataPacket to the interface, returns an unique identifier for the packet if $needACK is true
* *
* @param Player $player * @param NetworkSession $session
* @param string $payload * @param string $payload
* @param bool $immediate * @param bool $immediate
*/ */
public function putPacket(Player $player, string $payload, bool $immediate = true) : void; public function putPacket(NetworkSession $session, string $payload, bool $immediate = true) : void;
/** /**
* Terminates the connection * Terminates the connection
* *
* @param Player $player * @param NetworkSession $session
* @param string $reason * @param string $reason
*/ */
public function close(Player $player, string $reason = "unknown reason") : void; public function close(NetworkSession $session, string $reason = "unknown reason") : void;
/** /**
* @param string $name * @param string $name

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe; namespace pocketmine\network\mcpe;
use pocketmine\Player;
use pocketmine\scheduler\AsyncTask; use pocketmine\scheduler\AsyncTask;
use pocketmine\Server; use pocketmine\Server;
@ -33,9 +32,9 @@ class CompressBatchedTask extends AsyncTask{
private $data; private $data;
/** /**
* @param PacketStream $stream * @param PacketStream $stream
* @param Player[] $targets * @param NetworkSession[] $targets
* @param int $compressionLevel * @param int $compressionLevel
*/ */
public function __construct(PacketStream $stream, array $targets, int $compressionLevel){ public function __construct(PacketStream $stream, array $targets, int $compressionLevel){
$this->data = $stream->buffer; $this->data = $stream->buffer;
@ -48,7 +47,7 @@ class CompressBatchedTask extends AsyncTask{
} }
public function onCompletion(Server $server) : void{ public function onCompletion(Server $server) : void{
/** @var Player[] $targets */ /** @var NetworkSession[] $targets */
$targets = $this->fetchLocal(); $targets = $this->fetchLocal();
$server->broadcastPacketsCallback($this->getResult(), $targets); $server->broadcastPacketsCallback($this->getResult(), $targets);

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe; namespace pocketmine\network\mcpe;
use pocketmine\event\player\PlayerCreationEvent;
use pocketmine\event\server\DataPacketReceiveEvent; use pocketmine\event\server\DataPacketReceiveEvent;
use pocketmine\event\server\DataPacketSendEvent; use pocketmine\event\server\DataPacketSendEvent;
use pocketmine\network\mcpe\handler\DeathSessionHandler; use pocketmine\network\mcpe\handler\DeathSessionHandler;
@ -59,15 +60,36 @@ class NetworkSession{
/** @var SessionHandler */ /** @var SessionHandler */
private $handler; private $handler;
public function __construct(Server $server, Player $player, NetworkInterface $interface, string $ip, int $port){ /** @var bool */
$this->server = $server; private $connected = true;
$this->player = $player;
$this->interface = $interface;
public function __construct(Server $server, NetworkInterface $interface, string $ip, int $port){
$this->server = $server;
$this->interface = $interface;
$this->ip = $ip; $this->ip = $ip;
$this->port = $port; $this->port = $port;
$this->setHandler(new LoginSessionHandler($player, $this)); //TODO: this should happen later in the login sequence
$this->createPlayer();
$this->setHandler(new LoginSessionHandler($this->player, $this));
}
protected function createPlayer() : void{
$this->server->getPluginManager()->callEvent($ev = new PlayerCreationEvent($this));
$class = $ev->getPlayerClass();
/**
* @var Player $player
* @see Player::__construct()
*/
$this->player = new $class($this->server, $this);
$this->server->addPlayer($this->player);
}
public function isConnected() : bool{
return $this->connected;
} }
public function getInterface() : NetworkInterface{ public function getInterface() : NetworkInterface{
@ -116,10 +138,14 @@ class NetworkSession{
} }
public function handleEncoded(string $payload) : void{ public function handleEncoded(string $payload) : void{
if(!$this->connected){
return;
}
//TODO: decryption if enabled //TODO: decryption if enabled
$stream = new PacketStream(NetworkCompression::decompress($payload)); $stream = new PacketStream(NetworkCompression::decompress($payload));
while(!$stream->feof() and $this->player->isConnected()){ while(!$stream->feof() and $this->connected){
$this->handleDataPacket(PacketPool::getPacket($stream->getString())); $this->handleDataPacket(PacketPool::getPacket($stream->getString()));
} }
} }
@ -160,14 +186,61 @@ class NetworkSession{
} }
} }
public function serverDisconnect(string $reason, bool $notify = true) : void{ /**
* Disconnects the session, destroying the associated player (if it exists).
*
* @param string $reason
* @param bool $notify
*/
public function disconnect(string $reason, bool $notify = true) : void{
if($this->connected){
$this->connected = false;
$this->player->close($this->player->getLeaveMessage(), $reason);
$this->doServerDisconnect($reason, $notify);
}
}
/**
* Called by the Player when it is closed (for example due to getting kicked).
*
* @param string $reason
* @param bool $notify
*/
public function onPlayerDestroyed(string $reason, bool $notify = true) : void{
if($this->connected){
$this->connected = false;
$this->doServerDisconnect($reason, $notify);
}
}
/**
* Internal helper function used to handle server disconnections.
*
* @param string $reason
* @param bool $notify
*/
private function doServerDisconnect(string $reason, bool $notify = true) : void{
if($notify){ if($notify){
$pk = new DisconnectPacket(); $pk = new DisconnectPacket();
$pk->message = $reason; $pk->message = $reason;
$pk->hideDisconnectionScreen = $reason === ""; $pk->hideDisconnectionScreen = $reason === "";
$this->sendDataPacket($pk, true); $this->sendDataPacket($pk, true);
} }
$this->interface->close($this->player, $notify ? $reason : "");
$this->interface->close($this, $notify ? $reason : "");
}
/**
* Called by the network interface to close the session when the client disconnects without server input, for
* example in a timeout condition or voluntary client disconnect.
*
* @param string $reason
*/
public function onClientDisconnect(string $reason) : void{
if($this->connected){
$this->connected = false;
$this->player->close($this->player->getLeaveMessage(), $reason);
}
} }
//TODO: onEnableEncryption() step //TODO: onEnableEncryption() step

View File

@ -23,11 +23,9 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe; namespace pocketmine\network\mcpe;
use pocketmine\event\player\PlayerCreationEvent;
use pocketmine\network\AdvancedNetworkInterface; use pocketmine\network\AdvancedNetworkInterface;
use pocketmine\network\mcpe\protocol\ProtocolInfo; use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\Network; use pocketmine\network\Network;
use pocketmine\Player;
use pocketmine\Server; use pocketmine\Server;
use pocketmine\snooze\SleeperNotifier; use pocketmine\snooze\SleeperNotifier;
use raklib\protocol\EncapsulatedPacket; use raklib\protocol\EncapsulatedPacket;
@ -56,8 +54,8 @@ class RakLibInterface implements ServerInstance, AdvancedNetworkInterface{
/** @var RakLibServer */ /** @var RakLibServer */
private $rakLib; private $rakLib;
/** @var Player[] */ /** @var NetworkSession[] */
private $players = []; private $sessions = [];
/** @var string[] */ /** @var string[] */
private $identifiers = []; private $identifiers = [];
@ -104,17 +102,17 @@ class RakLibInterface implements ServerInstance, AdvancedNetworkInterface{
} }
public function closeSession(string $identifier, string $reason) : void{ public function closeSession(string $identifier, string $reason) : void{
if(isset($this->players[$identifier])){ if(isset($this->sessions[$identifier])){
$player = $this->players[$identifier]; $session = $this->sessions[$identifier];
unset($this->identifiers[spl_object_hash($player)]); unset($this->identifiers[spl_object_hash($session)]);
unset($this->players[$identifier]); unset($this->sessions[$identifier]);
$player->close($player->getLeaveMessage(), $reason); $session->onClientDisconnect($reason);
} }
} }
public function close(Player $player, string $reason = "unknown reason") : void{ public function close(NetworkSession $session, string $reason = "unknown reason") : void{
if(isset($this->identifiers[$h = spl_object_hash($player)])){ if(isset($this->identifiers[$h = spl_object_hash($session)])){
unset($this->players[$this->identifiers[$h]]); unset($this->sessions[$this->identifiers[$h]]);
$this->interface->closeSession($this->identifiers[$h], $reason); $this->interface->closeSession($this->identifiers[$h], $reason);
unset($this->identifiers[$h]); unset($this->identifiers[$h]);
} }
@ -131,23 +129,18 @@ class RakLibInterface implements ServerInstance, AdvancedNetworkInterface{
} }
public function openSession(string $identifier, string $address, int $port, int $clientID) : void{ public function openSession(string $identifier, string $address, int $port, int $clientID) : void{
$ev = new PlayerCreationEvent($this, $address, $port); $session = new NetworkSession($this->server, $this, $address, $port);
$this->server->getPluginManager()->callEvent($ev); $this->sessions[$identifier] = $session;
$class = $ev->getPlayerClass(); $this->identifiers[spl_object_hash($session)] = $identifier;
$player = new $class($this, $ev->getAddress(), $ev->getPort());
$this->players[$identifier] = $player;
$this->identifiers[spl_object_hash($player)] = $identifier;
$this->server->addPlayer($player);
} }
public function handleEncapsulated(string $identifier, EncapsulatedPacket $packet, int $flags) : void{ public function handleEncapsulated(string $identifier, EncapsulatedPacket $packet, int $flags) : void{
if(isset($this->players[$identifier])){ if(isset($this->sessions[$identifier])){
//get this now for blocking in case the player was closed before the exception was raised //get this now for blocking in case the player was closed before the exception was raised
$address = $this->players[$identifier]->getNetworkSession()->getIp(); $address = $this->sessions[$identifier]->getIp();
try{ try{
if($packet->buffer !== "" and $packet->buffer{0} === self::MCPE_RAKNET_PACKET_ID){ //Batch if($packet->buffer !== "" and $packet->buffer{0} === self::MCPE_RAKNET_PACKET_ID){ //Batch
$this->players[$identifier]->getNetworkSession()->handleEncoded(substr($packet->buffer, 1)); $this->sessions[$identifier]->handleEncoded(substr($packet->buffer, 1));
} }
}catch(\Throwable $e){ }catch(\Throwable $e){
$logger = $this->server->getLogger(); $logger = $this->server->getLogger();
@ -208,8 +201,8 @@ class RakLibInterface implements ServerInstance, AdvancedNetworkInterface{
} }
} }
public function putPacket(Player $player, string $payload, bool $immediate = true) : void{ public function putPacket(NetworkSession $session, string $payload, bool $immediate = true) : void{
if(isset($this->identifiers[$h = spl_object_hash($player)])){ if(isset($this->identifiers[$h = spl_object_hash($session)])){
$identifier = $this->identifiers[$h]; $identifier = $this->identifiers[$h];
$pk = new EncapsulatedPacket(); $pk = new EncapsulatedPacket();
@ -222,8 +215,8 @@ class RakLibInterface implements ServerInstance, AdvancedNetworkInterface{
} }
public function updatePing(string $identifier, int $pingMS) : void{ public function updatePing(string $identifier, int $pingMS) : void{
if(isset($this->players[$identifier])){ if(isset($this->sessions[$identifier])){
$this->players[$identifier]->getNetworkSession()->updatePing($pingMS); $this->sessions[$identifier]->updatePing($pingMS);
} }
} }
} }

View File

@ -53,8 +53,7 @@ class LoginSessionHandler extends SessionHandler{
$this->session->sendDataPacket($pk, true); $this->session->sendDataPacket($pk, true);
//This pocketmine disconnect message will only be seen by the console (PlayStatusPacket causes the messages to be shown for the client) //This pocketmine disconnect message will only be seen by the console (PlayStatusPacket causes the messages to be shown for the client)
$this->player->close( $this->session->disconnect(
"",
$this->player->getServer()->getLanguage()->translateString("pocketmine.disconnect.incompatibleProtocol", [$packet->protocol]), $this->player->getServer()->getLanguage()->translateString("pocketmine.disconnect.incompatibleProtocol", [$packet->protocol]),
false false
); );
@ -63,13 +62,13 @@ class LoginSessionHandler extends SessionHandler{
} }
if(!Player::isValidUserName($packet->username)){ if(!Player::isValidUserName($packet->username)){
$this->player->close("", "disconnectionScreen.invalidName"); $this->session->disconnect("disconnectionScreen.invalidName");
return true; return true;
} }
if($packet->skin === null or !$packet->skin->isValid()){ if($packet->skin === null or !$packet->skin->isValid()){
$this->player->close("", "disconnectionScreen.invalidSkin"); $this->session->disconnect("disconnectionScreen.invalidSkin");
return true; return true;
} }

View File

@ -86,7 +86,7 @@ class PreSpawnSessionHandler extends SessionHandler{
$this->player->sendAllInventories(); $this->player->sendAllInventories();
$this->player->getInventory()->sendCreativeContents(); $this->player->getInventory()->sendCreativeContents();
$this->player->getInventory()->sendHeldItem($this->player); $this->player->getInventory()->sendHeldItem($this->player);
$this->session->getInterface()->putPacket($this->player, $this->server->getCraftingManager()->getCraftingDataPacket()); $this->session->getInterface()->putPacket($this->session, $this->server->getCraftingManager()->getCraftingDataPacket());
$this->server->sendFullPlayerListData($this->player); $this->server->sendFullPlayerListData($this->player);
} }

View File

@ -66,14 +66,14 @@ class ResourcePacksSessionHandler extends SessionHandler{
private function disconnectWithError(string $error) : void{ private function disconnectWithError(string $error) : void{
$this->player->getServer()->getLogger()->error("Error while downloading resource packs for " . $this->player->getName() . ": " . $error); $this->player->getServer()->getLogger()->error("Error while downloading resource packs for " . $this->player->getName() . ": " . $error);
$this->player->close("", "disconnectionScreen.resourcePack", true); $this->session->disconnect("disconnectionScreen.resourcePack");
} }
public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{ public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
switch($packet->status){ switch($packet->status){
case ResourcePackClientResponsePacket::STATUS_REFUSED: case ResourcePackClientResponsePacket::STATUS_REFUSED:
//TODO: add lang strings for this //TODO: add lang strings for this
$this->player->close("", "You must accept resource packs to join this server.", true); $this->session->disconnect("You must accept resource packs to join this server.", true);
break; break;
case ResourcePackClientResponsePacket::STATUS_SEND_PACKS: case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
foreach($packet->packIds as $uuid){ foreach($packet->packIds as $uuid){