address,$this->port); Log::info(sprintf('%s: + Connection from [%s] on port [%d]',__METHOD__,$this->address,$this->port)); $this->connection = $connection; } public function __get($key) { switch ($key) { case 'cps': case 'speed': return Arr::get($this->session,$key); default: throw new \Exception(sprintf('%s: Unknown key [%s]:',__METHOD__,$key)); } } public function __set($key,$value) { switch ($key) { case 'cps': case 'speed': return $this->session[$key] = $value; default: throw new \Exception(sprintf('%s: Unknown key [%s]:',__METHOD__,$key)); } } /** * We'll add to our transmit buffer and if doesnt have space, we'll empty it first * * @param string $data * @return void * @throws \Exception */ public function buffer_add(string $data): void { if ($this->DEBUG) Log::debug(sprintf('%s: + Start [%s] (%d)',__METHOD__,$data,strlen($data))); //$rc = self::OK; //$tx_ptr = self::TX_BUF_SIZE-$this->tx_free; $ptr = 0; $num_bytes = strlen($data); $this->tty_status = self::TTY_SUCCESS; while ($num_bytes) { if ($this->DEBUG) Log::debug(sprintf('%s: - Num Bytes [%d]: TX Free [%d]',__METHOD__,$num_bytes,$this->tx_free)); if ($num_bytes > $this->tx_free) { do { $this->buffer_flush(5); if ($this->tty_status == self::TTY_SUCCESS) { $n = min($this->tx_free,$num_bytes); $this->tx_buf = substr($data,$ptr,$n); $this->tx_free -= $n; $num_bytes -= $n; $ptr += $n; } } while ($this->tty_status != self::TTY_SUCCESS); } else { if ($this->DEBUG) Log::debug(sprintf('%s: - Remaining data to send [%d]',__METHOD__,$num_bytes)); $this->tx_buf .= substr($data,$ptr,$num_bytes); $this->tx_free -= $num_bytes; $num_bytes = 0; } } if ($this->DEBUG) Log::debug(sprintf('%s: = End [%s]',__METHOD__,strlen($this->tx_buf))); } /** * Clear our TX buffer */ public function buffer_clear(): void { $this->tx_buf = ''; $this->tx_free = self::TX_BUF_SIZE; } /** * Empty our TX buffer * * @param int $timeout * @return int * @throws \Exception */ public function buffer_flush(int $timeout): int { if ($this->DEBUG) Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$timeout)); $rc = self::OK; $tx_ptr = 0; $restsize = self::TX_BUF_SIZE-$this->tx_free; $tm = $this->timer_set($timeout); while (self::TX_BUF_SIZE != $this->tx_free) { $tv = $this->timer_rest($tm); if ($rc = $this->canSend($tv)>0) { if ($this->DEBUG) Log::debug(sprintf('%s: - Sending [%d]',__METHOD__,$restsize)); $rc = $this->send(substr($this->tx_buf,$tx_ptr,$restsize),0); Log::debug(sprintf('%s: - Sent [%d] (%s)',__METHOD__,$rc,Str::limit($this->tx_buf,15))); if ($rc == $restsize) { $this->tx_buf = ''; $tx_ptr = 0; $this->tx_free += $rc; $this->buffer_clear(); } else if ($rc > 0) { $tx_ptr += $rc; $restsize -= $rc; } } else { return $rc; } // @todo Enable a delay for slow clients //sleep(1); if ($this->timer_expired($tm)) return self::ERROR; } if ($this->DEBUG) Log::debug(sprintf('%s: = End [%d]',__METHOD__,$rc)); return $rc; } /** * @param int $timeout * @return int * @throws \Exception */ public function canSend(int $timeout): int { $write = [$this->connection]; return $this->socketSelect(NULL,$write,NULL,$timeout); } /** * Close the connection with the client */ public function close(): void { socket_shutdown($this->connection); socket_close($this->connection); } /** * Create a client socket * @param string $address * @param int $port * @param int $speed * @return static * @throws SocketException */ public static function create(string $address,int $port,int $speed=self::TCP_SPEED): self { Log::debug(sprintf('%s: + Creating connection to [%s:%d]',__METHOD__,$address,$port)); $address = gethostbyname($address); /* Create a TCP/IP socket. */ $socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP); if ($socket === FALSE) throw new SocketException(SocketException::CANT_CREATE_SOCKET,socket_strerror(socket_last_error($socket))); $result = socket_connect($socket,$address,$port); if ($result === FALSE) throw new SocketException(SocketException::CANT_CONNECT,socket_strerror(socket_last_error($socket))); return new self($socket,$speed); } /** * Return the client's address * * @return string * @todo change to __get() */ public function getAddress(): string { return $this->address; } /** * Return the port in use * * @return int * @todo change to __get() */ public function getPort(): int { return $this->port; } /** * @param int $timeout * @return int * @note use socketSelect() * @throws \Exception */ public function hasData(int $timeout): int { $read = [$this->connection]; return $this->rx_left ?: $this->socketSelect($read,NULL,NULL,$timeout); } /** * Read data from the socket. * If we only want 1 character, we'll return the ASCII value of the data received * * @param int $timeout * @param int $len * @return int|string * @throws SocketException */ public function read(int $timeout,int $len=1024) { if ($this->DEBUG) Log::debug(sprintf('%s: + Start [%d] (%d)',__METHOD__,$len,$timeout)); if ($timeout AND ($this->hasData($timeout) === 0)) return ''; $buf = ''; $rc = socket_recv($this->connection,$buf, $len,MSG_DONTWAIT); if ($this->DEBUG) Log::debug(sprintf('%s: - Read [%d]',__METHOD__,$rc)); if ($rc === FALSE) throw new SocketException($x=socket_last_error($this->connection),socket_strerror($x)); // If our buffer is null, see if we have any out of band data. if (($rc == 0) && is_nulL($buf) && ($this->hasData(0) > 0)) socket_recv($this->connection,$buf, $len,MSG_OOB); return is_null($buf) ? '' : $buf; } /** * Read a character from the remote. * We'll buffer everything received * * @param int $timeout * @return int * @throws SocketException */ public function read_ch(int $timeout): int { if ($this->DEBUG) Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$timeout),['rx_left'=>$this->rx_left,'rx_ptr'=>$this->rx_ptr]); // If our buffer is empty, we'll try and read from the remote if ($this->rx_left == 0) { if ($this->hasData($timeout) > 0) { try { if (! strlen($this->rx_buf = $this->read(0,self::RX_BUF_SIZE))) { if ($this->DEBUG) Log::debug(sprintf('%s: - Nothing read',__METHOD__)); return self::TTY_TIMEOUT; } } catch (\Exception $e) { return ($e->getCode() == 11) ? self::TTY_TIMEOUT : self::ERROR; } if ($this->DEBUG) Log::info(sprintf('%s: - Read [%d] bytes',__METHOD__,strlen($this->rx_buf))); $this->rx_ptr = 0; $this->rx_left = strlen($this->rx_buf); } else { return self::TTY_TIMEOUT; } } $rc = ord(substr($this->rx_buf,$this->rx_ptr,1)); $this->rx_left--; $this->rx_ptr++; if ($this->DEBUG) Log::debug(sprintf('%s: = Return [%x] (%c)',__METHOD__,$rc,$rc)); return $rc; } public function rx_purge(): void { $this->rx_ptr = $this->rx_left = 0; $this->rx_buf = ''; } /** * Send data to the client * * @param $message * @param int $timeout * @param null $length * @return false|int * @throws \Exception */ public function send($message,int $timeout,$length=NULL) { if ($timeout AND (! $rc = $this->canSend($timeout))) return $rc; if (is_null($length)) $length = strlen($message); return socket_write($this->connection,$message,$length); } /** * Wait for data on a socket * * @param array|null $read * @param array|null $write * @param array|null $except * @param int $timeout * @return int * @throws \Exception */ private function socketSelect(?array $read,?array $write,?array $except,int $timeout): int { $rc = socket_select($read,$write,$except,$timeout); if ($rc === FALSE) throw new \Exception('Socket Error: '.socket_strerror(socket_last_error())); if ($this->DEBUG) Log::debug(sprintf('Socket select returned [%d] (%d)',$rc,$timeout),['read'=>$read,'write'=>$write,'except'=>$except]); return $rc; } public function timer_expired(int $timer): int { return (time()>=$timer); } public function timer_rest(int $timer): int { return (($timer)-time()); } public function timer_set(int $expire): int { return (time()+$expire); } /** * See if we there is data waiting to collect, or if we can send * * @param bool $read * @param bool $write * @param int $timeout * @return int * @throws \Exception */ public function ttySelect(bool $read,bool $write, int $timeout): int { $read = $read ? [$this->connection] : NULL; $write = $write ? [$this->connection] : NULL; return $this->socketSelect($read,$write,NULL,$timeout); } }