clrghouz/app/Classes/Sock/SocketClient.php

434 lines
10 KiB
PHP

<?php
namespace App\Classes\Sock;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
/**
* Class SocketClient
*
* @package App\Classes\Sock
* @property int cps
* @property int speed
*/
final class SocketClient {
private const LOGKEY = 'SC-';
// For deep debugging
private bool $DEBUG = FALSE;
private \Socket $connection;
private string $address_local = '';
private int $port_local = 0;
private string $address_remote = '';
private int $port_remote = 0;
// Our session state
private array $session = [];
private const OK = 0;
private const EOF = -1;
private const TIMEOUT = -2;
private const RCDO = -3;
private const GCOUNT = -4;
private const ERROR = -5;
private const TTY_SUCCESS = self::OK;
private const TTY_TIMEOUT = self::TIMEOUT;
private const TTY_HANGUP = self::RCDO;
private const TTY_ERROR = self::ERROR;
public const TCP_SPEED = 115200;
// Buffer for sending
private const TX_BUF_SIZE = (0x8100);
private int $tx_ptr = 0;
private int $tx_free = self::TX_BUF_SIZE;
private int $tty_status = 0;
private string $tx_buf = '';
// Buffer for receiving
private const RX_BUF_SIZE = (0x8100);
private int $rx_ptr = 0;
private int $rx_left = 0;
private string $rx_buf = '';
public function __construct (\Socket $connection) {
socket_getsockname($connection,$this->address_local,$this->port_local);
socket_getpeername($connection,$this->address_remote,$this->port_remote);
Log::info(sprintf('%s:+ Connection from [%s] on port [%d]',self::LOGKEY,$this->address_remote,$this->port_remote));
$this->connection = $connection;
}
public function __get($key) {
switch ($key) {
case 'address_remote':
case 'port_remote':
return $this->{$key};
case 'cps':
case 'speed':
return Arr::get($this->session,$key);
default:
throw new \Exception(sprintf('%s:! Unknown key [%s]:',self::LOGKEY,$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]:',self::LOGKEY,$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)',self::LOGKEY,$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]',self::LOGKEY,$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]',self::LOGKEY,$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]',self::LOGKEY,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]',self::LOGKEY,$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]',self::LOGKEY,$restsize));
$rc = $this->send(substr($this->tx_buf,$tx_ptr,$restsize),0);
Log::debug(sprintf('%s: - Sent [%d] (%s)',self::LOGKEY,$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]',self::LOGKEY,$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]',self::LOGKEY,$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);
}
/**
* @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)',self::LOGKEY,$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]',self::LOGKEY,$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.
// @todo We throw an errorexception when the socket is closed by the remote I think.
if (($rc == 0) && is_nulL($buf) && ($this->hasData(0) > 0)) {
try {
socket_recv($this->connection,$buf, $len,MSG_OOB);
} catch (\Exception $e) {
throw new SocketException($x=socket_last_error($this->connection),socket_strerror($x));
}
}
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] - rx_left[%d], rx_ptr[%d]',self::LOGKEY,$timeout,$this->rx_left,$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',self::LOGKEY));
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',self::LOGKEY,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)',self::LOGKEY,$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);
}
}