15declare(strict_types=1);
17namespace raklib\generic;
35use
function microtime;
40 public const STATE_CONNECTING = 0;
41 public const STATE_CONNECTED = 1;
42 public const STATE_DISCONNECT_PENDING = 2;
43 public const STATE_DISCONNECT_NOTIFIED = 3;
44 public const STATE_DISCONNECTED = 4;
46 public const MIN_MTU_SIZE = 400;
48 private \Logger $logger;
52 protected int $state = self::STATE_CONNECTING;
56 private float $lastUpdate;
57 private float $disconnectionTime = 0;
59 private bool $isActive =
false;
61 private float $lastPingTime = -1;
63 private int $lastPingMeasure = 1;
78 int $recvMaxSplitParts = PHP_INT_MAX,
79 int $recvMaxConcurrentSplits = PHP_INT_MAX
81 if($mtuSize < self::MIN_MTU_SIZE){
82 throw new \InvalidArgumentException(
"MTU size must be at least " . self::MIN_MTU_SIZE .
", got $mtuSize");
84 $this->logger = new \PrefixedLogger($logger,
"Session: " . $address->toString());
85 $this->address = $address;
86 $this->
id = $clientId;
88 $this->lastUpdate = microtime(
true);
93 $this->handleEncapsulatedPacketRoute($pk);
99 $recvMaxConcurrentSplits
103 function(
Datagram $datagram) :
void{
106 function(
int $identifierACK) :
void{
120 abstract protected function onPacketAck(
int $identifierACK) : void;
155 return intdiv(hrtime(true), 1_000_000);
159 return $this->address;
162 public function getID() : int{
166 public function getState() : int{
170 public function isTemporary() : bool{
171 return $this->state === self::STATE_CONNECTING;
174 public function isConnected() : bool{
176 $this->state !== self::STATE_DISCONNECT_PENDING and
177 $this->state !== self::STATE_DISCONNECT_NOTIFIED and
178 $this->state !== self::STATE_DISCONNECTED;
181 public function update(
float $time) : void{
182 if(!$this->isActive and ($this->lastUpdate + 10) < $time){
183 $this->forciblyDisconnect(DisconnectReason::PEER_TIMEOUT);
188 if($this->state === self::STATE_DISCONNECT_PENDING || $this->state === self::STATE_DISCONNECT_NOTIFIED){
190 if(!$this->sendLayer->needsUpdate() and !$this->recvLayer->needsUpdate()){
191 if($this->state === self::STATE_DISCONNECT_PENDING){
192 $this->queueConnectedPacket(
new DisconnectionNotification(), PacketReliability::RELIABLE_ORDERED, 0,
true);
193 $this->state = self::STATE_DISCONNECT_NOTIFIED;
194 $this->logger->debug(
"All pending traffic flushed, sent disconnect notification");
196 $this->state = self::STATE_DISCONNECTED;
197 $this->logger->debug(
"Client cleanly disconnected, marking session for destruction");
200 }elseif($this->disconnectionTime + 10 < $time){
201 $this->state = self::STATE_DISCONNECTED;
202 $this->logger->debug(
"Timeout during graceful disconnect, forcibly closing session");
207 $this->isActive =
false;
209 $this->recvLayer->update();
210 $this->sendLayer->update();
212 if($this->lastPingTime + 5 < $time){
214 $this->lastPingTime = $time;
218 protected function queueConnectedPacket(ConnectedPacket $packet,
int $reliability,
int $orderChannel,
bool $immediate =
false) : void{
219 $out = new PacketSerializer();
220 $packet->encode($out);
222 $encapsulated =
new EncapsulatedPacket();
223 $encapsulated->reliability = $reliability;
224 $encapsulated->orderChannel = $orderChannel;
225 $encapsulated->buffer = $out->getBuffer();
227 $this->sendLayer->addEncapsulatedToQueue($encapsulated, $immediate);
230 public function addEncapsulatedToQueue(EncapsulatedPacket $packet,
bool $immediate) : void{
231 $this->sendLayer->addEncapsulatedToQueue($packet, $immediate);
234 protected function sendPing(
int $reliability = PacketReliability::UNRELIABLE) : void{
235 $this->queueConnectedPacket(ConnectedPing::create($this->getRakNetTimeMS()), $reliability, 0, true);
238 private function handleEncapsulatedPacketRoute(EncapsulatedPacket $packet) : void{
239 $id = ord($packet->buffer[0]);
240 if($id < MessageIdentifiers::ID_USER_PACKET_ENUM){
241 if($this->state === self::STATE_CONNECTING){
242 $this->handleRakNetConnectionPacket($packet->buffer);
243 }elseif($id === MessageIdentifiers::ID_DISCONNECTION_NOTIFICATION){
244 $this->handleRemoteDisconnect();
245 }elseif($id === MessageIdentifiers::ID_CONNECTED_PING){
246 $dataPacket =
new ConnectedPing();
247 $dataPacket->decode(
new PacketSerializer($packet->buffer));
248 $this->queueConnectedPacket(ConnectedPong::create(
249 $dataPacket->sendPingTime,
250 $this->getRakNetTimeMS()
251 ), PacketReliability::UNRELIABLE, 0);
252 }elseif($id === MessageIdentifiers::ID_CONNECTED_PONG){
253 $dataPacket =
new ConnectedPong();
254 $dataPacket->decode(
new PacketSerializer($packet->buffer));
256 $this->handlePong($dataPacket->sendPingTime, $dataPacket->sendPongTime);
258 }elseif($this->state === self::STATE_CONNECTED){
259 $this->onPacketReceive($packet->buffer);
268 private function handlePong(
int $sendPingTime,
int $sendPongTime) : void{
269 $currentTime = $this->getRakNetTimeMS();
270 if($currentTime < $sendPingTime){
271 $this->logger->debug(
"Received invalid pong: timestamp is in the future by " . ($sendPingTime - $currentTime) .
" ms");
273 $this->lastPingMeasure = $currentTime - $sendPingTime;
274 $this->onPingMeasure($this->lastPingMeasure);
278 public function handlePacket(Packet $packet) : void{
279 $this->isActive = true;
280 $this->lastUpdate = microtime(
true);
282 if($packet instanceof Datagram){
283 $this->recvLayer->onDatagram($packet);
284 }elseif($packet instanceof ACK){
285 $this->sendLayer->onACK($packet);
286 }elseif($packet instanceof NACK){
287 $this->sendLayer->onNACK($packet);
300 if($this->isConnected()){
301 $this->state = self::STATE_DISCONNECT_PENDING;
302 $this->disconnectionTime = microtime(
true);
303 $this->onDisconnect($reason);
304 $this->logger->debug(
"Requesting graceful disconnect because \"" . DisconnectReason::toString($reason) .
"\"");
317 $this->state = self::STATE_DISCONNECTED;
318 $this->onDisconnect($reason);
319 $this->logger->debug(
"Forcibly disconnecting session due to " . DisconnectReason::toString($reason));
322 private function handleRemoteDisconnect() : void{
325 $this->recvLayer->update();
327 if($this->isConnected()){
330 $this->onDisconnect(DisconnectReason::CLIENT_DISCONNECT);
332 $this->state = self::STATE_DISCONNECTED;
333 $this->logger->debug(
"Terminating session due to client disconnect");
340 return $this->state === self::STATE_DISCONNECTED;
handleRakNetConnectionPacket(string $packet)
onDisconnect(int $reason)
sendPacket(Packet $packet)
onPacketAck(int $identifierACK)
__construct(\Logger $logger, InternetAddress $address, int $clientId, int $mtuSize, int $recvMaxSplitParts=PHP_INT_MAX, int $recvMaxConcurrentSplits=PHP_INT_MAX)
forciblyDisconnect(int $reason)
onPingMeasure(int $pingMS)
initiateDisconnect(int $reason)
onPacketReceive(string $packet)