PocketMine-MP 5.27.2 git-d86943fa8c6384be3e2c1901ebf94f584b27e784
Loading...
Searching...
No Matches
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 getLogger() : \Logger{
159 return $this->logger;
160 }
161
162 public function getAddress() : InternetAddress{
163 return $this->address;
164 }
165
166 public function getID() : int{
167 return $this->id;
168 }
169
170 public function getState() : int{
171 return $this->state;
172 }
173
174 public function isTemporary() : bool{
175 return $this->state === self::STATE_CONNECTING;
176 }
177
178 public function isConnected() : bool{
179 return
180 $this->state !== self::STATE_DISCONNECT_PENDING and
181 $this->state !== self::STATE_DISCONNECT_NOTIFIED and
182 $this->state !== self::STATE_DISCONNECTED;
183 }
184
185 public function update(float $time) : void{
186 if(!$this->isActive and ($this->lastUpdate + 10) < $time){
187 $this->forciblyDisconnect(DisconnectReason::PEER_TIMEOUT);
188
189 return;
190 }
191
192 if($this->state === self::STATE_DISCONNECT_PENDING || $this->state === self::STATE_DISCONNECT_NOTIFIED){
193 //by this point we already told the event listener that the session is closing, so we don't need to do it again
194 if(!$this->sendLayer->needsUpdate() and !$this->recvLayer->needsUpdate()){
195 if($this->state === self::STATE_DISCONNECT_PENDING){
196 $this->queueConnectedPacket(new DisconnectionNotification(), PacketReliability::RELIABLE_ORDERED, 0, true);
197 $this->state = self::STATE_DISCONNECT_NOTIFIED;
198 $this->logger->debug("All pending traffic flushed, sent disconnect notification");
199 }else{
200 $this->state = self::STATE_DISCONNECTED;
201 $this->logger->debug("Client cleanly disconnected, marking session for destruction");
202 return;
203 }
204 }elseif($this->disconnectionTime + 10 < $time){
205 $this->state = self::STATE_DISCONNECTED;
206 $this->logger->debug("Timeout during graceful disconnect, forcibly closing session");
207 return;
208 }
209 }
210
211 $this->isActive = false;
212
213 $this->recvLayer->update();
214 $this->sendLayer->update();
215
216 if($this->lastPingTime + 5 < $time){
217 $this->sendPing();
218 $this->lastPingTime = $time;
219 }
220 }
221
222 protected function queueConnectedPacket(ConnectedPacket $packet, int $reliability, int $orderChannel, bool $immediate = false) : void{
223 $out = new PacketSerializer(); //TODO: reuse streams to reduce allocations
224 $packet->encode($out);
225
226 $encapsulated = new EncapsulatedPacket();
227 $encapsulated->reliability = $reliability;
228 $encapsulated->orderChannel = $orderChannel;
229 $encapsulated->buffer = $out->getBuffer();
230
231 $this->sendLayer->addEncapsulatedToQueue($encapsulated, $immediate);
232 }
233
234 public function addEncapsulatedToQueue(EncapsulatedPacket $packet, bool $immediate) : void{
235 $this->sendLayer->addEncapsulatedToQueue($packet, $immediate);
236 }
237
238 protected function sendPing(int $reliability = PacketReliability::UNRELIABLE) : void{
239 $this->queueConnectedPacket(ConnectedPing::create($this->getRakNetTimeMS()), $reliability, 0, true);
240 }
241
242 private function handleEncapsulatedPacketRoute(EncapsulatedPacket $packet) : void{
243 $id = ord($packet->buffer[0]);
244 if($id < MessageIdentifiers::ID_USER_PACKET_ENUM){ //internal data packet
245 if($this->state === self::STATE_CONNECTING){
246 $this->handleRakNetConnectionPacket($packet->buffer);
247 }elseif($id === MessageIdentifiers::ID_DISCONNECTION_NOTIFICATION){
248 $this->handleRemoteDisconnect();
249 }elseif($id === MessageIdentifiers::ID_CONNECTED_PING){
250 $dataPacket = new ConnectedPing();
251 $dataPacket->decode(new PacketSerializer($packet->buffer));
252 $this->queueConnectedPacket(ConnectedPong::create(
253 $dataPacket->sendPingTime,
254 $this->getRakNetTimeMS()
255 ), PacketReliability::UNRELIABLE, 0);
256 }elseif($id === MessageIdentifiers::ID_CONNECTED_PONG){
257 $dataPacket = new ConnectedPong();
258 $dataPacket->decode(new PacketSerializer($packet->buffer));
259
260 $this->handlePong($dataPacket->sendPingTime, $dataPacket->sendPongTime);
261 }
262 }elseif($this->state === self::STATE_CONNECTED){
263 $this->onPacketReceive($packet->buffer);
264 }else{
265 //$this->logger->notice("Received packet before connection: " . bin2hex($packet->buffer));
266 }
267 }
268
272 private function handlePong(int $sendPingTime, int $sendPongTime) : void{
273 if($sendPingTime < 0){
274 $this->logger->debug("Received invalid pong: timestamp overflow");
275 }else{
276 $currentTime = $this->getRakNetTimeMS();
277 if($currentTime < $sendPingTime){
278 $this->logger->debug("Received invalid pong: timestamp is in the future by " . ($sendPingTime - $currentTime) . " ms");
279 }else{
280 $this->lastPingMeasure = $currentTime - $sendPingTime;
281 $this->onPingMeasure($this->lastPingMeasure);
282 }
283 }
284 }
285
289 public function handlePacket(Packet $packet) : void{
290 $this->isActive = true;
291 $this->lastUpdate = microtime(true);
292
293 if($packet instanceof Datagram){ //In reality, ALL of these packets are datagrams.
294 $this->recvLayer->onDatagram($packet);
295 }elseif($packet instanceof ACK){
296 $this->sendLayer->onACK($packet);
297 }elseif($packet instanceof NACK){
298 $this->sendLayer->onNACK($packet);
299 }
300 }
301
310 public function initiateDisconnect(int $reason) : void{
311 if($this->isConnected()){
312 $this->state = self::STATE_DISCONNECT_PENDING;
313 $this->disconnectionTime = microtime(true);
314 $this->onDisconnect($reason);
315 $this->logger->debug("Requesting graceful disconnect because \"" . DisconnectReason::toString($reason) . "\"");
316 }
317 }
318
327 public function forciblyDisconnect(int $reason) : void{
328 $this->state = self::STATE_DISCONNECTED;
329 $this->onDisconnect($reason);
330 $this->logger->debug("Forcibly disconnecting session due to " . DisconnectReason::toString($reason));
331 }
332
333 private function handleRemoteDisconnect() : void{
334 //the client will expect an ACK for this; make sure it gets sent, because after forcible termination
335 //there won't be any session ticks to update it
336 $this->recvLayer->update();
337
338 if($this->isConnected()){
339 //the client might have disconnected after the server sent a disconnect notification, but before the client
340 //received it - in this case, we don't want to notify the event handler twice
341 $this->onDisconnect(DisconnectReason::CLIENT_DISCONNECT);
342 }
343 $this->state = self::STATE_DISCONNECTED;
344 $this->logger->debug("Terminating session due to client disconnect");
345 }
346
350 public function isFullyDisconnected() : bool{
351 return $this->state === self::STATE_DISCONNECTED;
352 }
353}
handleRakNetConnectionPacket(string $packet)
onDisconnect(int $reason)
sendPacket(Packet $packet)
handlePacket(Packet $packet)
Definition Session.php:289
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:327
onPingMeasure(int $pingMS)
initiateDisconnect(int $reason)
Definition Session.php:310
onPacketReceive(string $packet)