PocketMine-MP 5.18.2 git-00e39821f06a4b6d728d35053c2621dbb19369ff
Session.php
1<?php
2
3/*
4 * This file is part of RakLib.
5 * Copyright (C) 2014-2022 PocketMine Team <https://github.com/pmmp/RakLib>
6 *
7 * RakLib is not affiliated with Jenkins Software LLC nor RakNet.
8 *
9 * RakLib is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
13 */
14
15declare(strict_types=1);
16
17namespace raklib\generic;
18
33use function hrtime;
34use function intdiv;
35use function microtime;
36use function ord;
37use const PHP_INT_MAX;
38
39abstract class Session{
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;
45
46 public const MIN_MTU_SIZE = 400;
47
48 private \Logger $logger;
49
50 protected InternetAddress $address;
51
52 protected int $state = self::STATE_CONNECTING;
53
54 private int $id;
55
56 private float $lastUpdate;
57 private float $disconnectionTime = 0;
58
59 private bool $isActive = false;
60
61 private float $lastPingTime = -1;
62
63 private int $lastPingMeasure = 1;
64
65 private ReceiveReliabilityLayer $recvLayer;
66
67 private SendReliabilityLayer $sendLayer;
68
73 public function __construct(
74 \Logger $logger,
75 InternetAddress $address,
76 int $clientId,
77 int $mtuSize,
78 int $recvMaxSplitParts = PHP_INT_MAX,
79 int $recvMaxConcurrentSplits = PHP_INT_MAX
80 ){
81 if($mtuSize < self::MIN_MTU_SIZE){
82 throw new \InvalidArgumentException("MTU size must be at least " . self::MIN_MTU_SIZE . ", got $mtuSize");
83 }
84 $this->logger = new \PrefixedLogger($logger, "Session: " . $address->toString());
85 $this->address = $address;
86 $this->id = $clientId;
87
88 $this->lastUpdate = microtime(true);
89
90 $this->recvLayer = new ReceiveReliabilityLayer(
91 $this->logger,
92 function(EncapsulatedPacket $pk) : void{
93 $this->handleEncapsulatedPacketRoute($pk);
94 },
95 function(AcknowledgePacket $pk) : void{
96 $this->sendPacket($pk);
97 },
98 $recvMaxSplitParts,
99 $recvMaxConcurrentSplits
100 );
101 $this->sendLayer = new SendReliabilityLayer(
102 $mtuSize,
103 function(Datagram $datagram) : void{
104 $this->sendPacket($datagram);
105 },
106 function(int $identifierACK) : void{
107 $this->onPacketAck($identifierACK);
108 }
109 );
110 }
111
115 abstract protected function sendPacket(Packet $packet) : void;
116
120 abstract protected function onPacketAck(int $identifierACK) : void;
121
130 abstract protected function onDisconnect(int $reason) : void;
131
136 abstract protected function handleRakNetConnectionPacket(string $packet) : void;
137
143 abstract protected function onPacketReceive(string $packet) : void;
144
148 abstract protected function onPingMeasure(int $pingMS) : void;
149
154 protected function getRakNetTimeMS() : int{
155 return intdiv(hrtime(true), 1_000_000);
156 }
157
158 public function getAddress() : InternetAddress{
159 return $this->address;
160 }
161
162 public function getID() : int{
163 return $this->id;
164 }
165
166 public function getState() : int{
167 return $this->state;
168 }
169
170 public function isTemporary() : bool{
171 return $this->state === self::STATE_CONNECTING;
172 }
173
174 public function isConnected() : bool{
175 return
176 $this->state !== self::STATE_DISCONNECT_PENDING and
177 $this->state !== self::STATE_DISCONNECT_NOTIFIED and
178 $this->state !== self::STATE_DISCONNECTED;
179 }
180
181 public function update(float $time) : void{
182 if(!$this->isActive and ($this->lastUpdate + 10) < $time){
183 $this->forciblyDisconnect(DisconnectReason::PEER_TIMEOUT);
184
185 return;
186 }
187
188 if($this->state === self::STATE_DISCONNECT_PENDING || $this->state === self::STATE_DISCONNECT_NOTIFIED){
189 //by this point we already told the event listener that the session is closing, so we don't need to do it again
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");
195 }else{
196 $this->state = self::STATE_DISCONNECTED;
197 $this->logger->debug("Client cleanly disconnected, marking session for destruction");
198 return;
199 }
200 }elseif($this->disconnectionTime + 10 < $time){
201 $this->state = self::STATE_DISCONNECTED;
202 $this->logger->debug("Timeout during graceful disconnect, forcibly closing session");
203 return;
204 }
205 }
206
207 $this->isActive = false;
208
209 $this->recvLayer->update();
210 $this->sendLayer->update();
211
212 if($this->lastPingTime + 5 < $time){
213 $this->sendPing();
214 $this->lastPingTime = $time;
215 }
216 }
217
218 protected function queueConnectedPacket(ConnectedPacket $packet, int $reliability, int $orderChannel, bool $immediate = false) : void{
219 $out = new PacketSerializer(); //TODO: reuse streams to reduce allocations
220 $packet->encode($out);
221
222 $encapsulated = new EncapsulatedPacket();
223 $encapsulated->reliability = $reliability;
224 $encapsulated->orderChannel = $orderChannel;
225 $encapsulated->buffer = $out->getBuffer();
226
227 $this->sendLayer->addEncapsulatedToQueue($encapsulated, $immediate);
228 }
229
230 public function addEncapsulatedToQueue(EncapsulatedPacket $packet, bool $immediate) : void{
231 $this->sendLayer->addEncapsulatedToQueue($packet, $immediate);
232 }
233
234 protected function sendPing(int $reliability = PacketReliability::UNRELIABLE) : void{
235 $this->queueConnectedPacket(ConnectedPing::create($this->getRakNetTimeMS()), $reliability, 0, true);
236 }
237
238 private function handleEncapsulatedPacketRoute(EncapsulatedPacket $packet) : void{
239 $id = ord($packet->buffer[0]);
240 if($id < MessageIdentifiers::ID_USER_PACKET_ENUM){ //internal data packet
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));
255
256 $this->handlePong($dataPacket->sendPingTime, $dataPacket->sendPongTime);
257 }
258 }elseif($this->state === self::STATE_CONNECTED){
259 $this->onPacketReceive($packet->buffer);
260 }else{
261 //$this->logger->notice("Received packet before connection: " . bin2hex($packet->buffer));
262 }
263 }
264
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");
272 }else{
273 $this->lastPingMeasure = $currentTime - $sendPingTime;
274 $this->onPingMeasure($this->lastPingMeasure);
275 }
276 }
277
278 public function handlePacket(Packet $packet) : void{
279 $this->isActive = true;
280 $this->lastUpdate = microtime(true);
281
282 if($packet instanceof Datagram){ //In reality, ALL of these packets are datagrams.
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);
288 }
289 }
290
299 public function initiateDisconnect(int $reason) : void{
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) . "\"");
305 }
306 }
307
316 public function forciblyDisconnect(int $reason) : void{
317 $this->state = self::STATE_DISCONNECTED;
318 $this->onDisconnect($reason);
319 $this->logger->debug("Forcibly disconnecting session due to " . DisconnectReason::toString($reason));
320 }
321
322 private function handleRemoteDisconnect() : void{
323 //the client will expect an ACK for this; make sure it gets sent, because after forcible termination
324 //there won't be any session ticks to update it
325 $this->recvLayer->update();
326
327 if($this->isConnected()){
328 //the client might have disconnected after the server sent a disconnect notification, but before the client
329 //received it - in this case, we don't want to notify the event handler twice
330 $this->onDisconnect(DisconnectReason::CLIENT_DISCONNECT);
331 }
332 $this->state = self::STATE_DISCONNECTED;
333 $this->logger->debug("Terminating session due to client disconnect");
334 }
335
339 public function isFullyDisconnected() : bool{
340 return $this->state === self::STATE_DISCONNECTED;
341 }
342}
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)
Definition: Session.php:73
forciblyDisconnect(int $reason)
Definition: Session.php:316
onPingMeasure(int $pingMS)
initiateDisconnect(int $reason)
Definition: Session.php:299
onPacketReceive(string $packet)