From 834ece264521cdfe48f69176cea3ec139d57084d Mon Sep 17 00:00:00 2001 From: Deon George Date: Fri, 7 May 2021 22:07:26 +1000 Subject: [PATCH] Added SocketServer and SocketClient --- app/Classes/Sock/SocketClient.php | 145 +++++++++++++++++++++++++++ app/Classes/Sock/SocketException.php | 27 +++++ app/Classes/Sock/SocketServer.php | 114 +++++++++++++++++++++ 3 files changed, 286 insertions(+) create mode 100644 app/Classes/Sock/SocketClient.php create mode 100644 app/Classes/Sock/SocketException.php create mode 100644 app/Classes/Sock/SocketServer.php diff --git a/app/Classes/Sock/SocketClient.php b/app/Classes/Sock/SocketClient.php new file mode 100644 index 0000000..34a5686 --- /dev/null +++ b/app/Classes/Sock/SocketClient.php @@ -0,0 +1,145 @@ +address,$this->port); + Log::info(sprintf('Connection from [%s] on port [%d]',$this->address,$this->port),['m'=>__METHOD__]); + + $this->connection = $connection; + } + + /** + * @param int $timeout + * @return int + */ + 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 + * @return static + */ + public static function create(string $address,int $port): self + { + Log::debug(sprintf('Creating connection to [%s:%d]',$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); + } + + /** + * Return the client's address + * + * @return string + */ + public function getAddress(): string + { + return $this->address; + } + + /** + * Return the port in use + * + * @return int + */ + public function getPort(): int + { + return $this->port; + } + + /** + * @param int $timeout + * @return int + * @note use socketSelect() + * @todo Node used by bink yet? + * @todo to test + */ + public function hasData(int $timeout): int + { + $read = [$this->connection]; + $write = $except = NULL; + + //$rc = socket_select($read,$write,$except,$timeout); + //return $rc; + return $this->socketSelect($read,NULL,NULL,$timeout); + } + + /** + * Send data to the client + * + * @param $message + * @param int $timeout + * @param null $length + * @return false|int + */ + 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); + } + + private function socketSelect(?array $read,?array $write,?array $except,int $timeout): int + { + return socket_select($read,$write,$except,$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 false|int|string|null + */ + public function read(int $timeout,int $len=1024) + { + Log::debug(sprintf('+ Start [%d]',$len),['m'=>__METHOD__]); + + if ($timeout AND (! $rc = $this->hasData($timeout))) + return $rc; + + if (($buf=socket_read($this->connection,$len,PHP_BINARY_READ)) === FALSE) { + return NULL; + } + + Log::debug(sprintf(' - Read [%d]',strlen($buf)),['m'=>__METHOD__]); + + // For single character reads, we'll return the ASCII value of the buf + return ($len == 1 and (ord($buf) != 0)) ? ord($buf) : $buf; + } +} diff --git a/app/Classes/Sock/SocketException.php b/app/Classes/Sock/SocketException.php new file mode 100644 index 0000000..d883dc9 --- /dev/null +++ b/app/Classes/Sock/SocketException.php @@ -0,0 +1,27 @@ + 'Can\'t create socket: "%s"', + self::CANT_BIND_SOCKET => 'Can\'t bind socket: "%s"', + self::CANT_LISTEN => 'Can\'t listen: "%s"', + self::CANT_ACCEPT => 'Can\'t accept connections: "%s"', + self::CANT_CONNECT => 'Can\'t connect: "%s"', + ]; + + public function __construct(int $code,string $params=NULL) { + $message = $params + ? call_user_func_array('sprintf',[$this->messages[$code],$params]) + : $this->messages[$code]; + + parent::__construct($message,$code); + } +} diff --git a/app/Classes/Sock/SocketServer.php b/app/Classes/Sock/SocketServer.php new file mode 100644 index 0000000..80f389d --- /dev/null +++ b/app/Classes/Sock/SocketServer.php @@ -0,0 +1,114 @@ +bind = $bind; + $this->port = $port; + + $this->_init(); + } + + /** + * Bind to our Socket + * + * @throws SocketException + */ + private function _bindSocket(): void + { + if (socket_bind($this->server,$this->bind,$this->port) === FALSE) + throw new SocketException(SocketException::CANT_BIND_SOCKET,socket_strerror(socket_last_error($this->server))); + } + + /** + * Create our Socket + * + * @throws SocketException + */ + private function _createSocket(): void + { + /** + * Check dependencies + */ + if (! extension_loaded('sockets')) + throw new SocketException(SocketException::CANT_ACCEPT,'Missing sockets extension'); + + if (! extension_loaded('pcntl')) + throw new SocketException(SocketException::CANT_ACCEPT,'Missing pcntl extension'); + + $this->server = socket_create(AF_INET,SOCK_STREAM,SOL_TCP); + + if ($this->server === FALSE) + throw new SocketException(SocketException::CANT_CREATE_SOCKET,socket_strerror(socket_last_error())); + + socket_set_option($this->server,SOL_SOCKET,SO_REUSEADDR,1); + } + + /** + * Setup Socket and Bind + * + * @throws SocketException + */ + private function _init(): void + { + $this->_createSocket(); + $this->_bindSocket(); + } + + /** + * Our main loop where we listen for connections + * + * @throws SocketException + */ + public function listen() + { + if (! $this->handler) + throw new SocketException(SocketException::CANT_LISTEN,'Handler not set.'); + + if (socket_listen($this->server,$this->backlog) === FALSE) + throw new SocketException(SocketException::CANT_LISTEN,socket_strerror(socket_last_error($this->server))); + + Log::info(sprintf('Listening on [%s:%d]',$this->bind,$this->port),['m'=>__METHOD__]); + + $this->loop(); + socket_close($this->server); + + Log::info(sprintf('CLosed [%s:%d]',$this->bind,$this->port),['m'=>__METHOD__]); + } + + /** + * Manage and execute incoming connecitons + * + * @throws SocketException + */ + private function loop() + { + while (TRUE) { + if (($accept = socket_accept($this->server)) === FALSE) + throw new SocketException(SocketException::CANT_ACCEPT,socket_strerror(socket_last_error($this->server))); + + $this->handler[0]->{$this->handler[1]}(new SocketClient($accept)); + } + } + + /** + * Set our connection handler Class and Method + * + * @param array $handler + */ + public function setConnectionHandler(array $handler): void + { + $this->handler = $handler; + } +} \ No newline at end of file