123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562 |
- <?php
- namespace Socket\Raw;
- /**
- * Simple and lightweight OOP wrapper for the low-level sockets extension (ext-sockets)
- *
- * @author clue
- * @link https://github.com/clue/php-socket-raw
- */
- class Socket
- {
- /**
- * reference to actual socket resource
- *
- * @var \Socket|resource
- */
- private $resource;
- /**
- * instanciate socket wrapper for given socket resource
- *
- * should usually not be called manually, see Factory
- *
- * @param \Socket|resource $resource
- * @see Factory as the preferred (and simplest) way to construct socket instances
- */
- public function __construct($resource)
- {
- $this->resource = $resource;
- }
- /**
- * get actual socket resource
- *
- * @return \Socket|resource returns the socket resource (a `Socket` object as of PHP 8)
- */
- public function getResource()
- {
- return $this->resource;
- }
- /**
- * accept an incomming connection on this listening socket
- *
- * @return \Socket\Raw\Socket new connected socket used for communication
- * @throws Exception on error, if this is not a listening socket or there's no connection pending
- * @throws \Error PHP 8 only: throws \Error when socket is invalid
- * @see self::selectRead() to check if this listening socket can accept()
- * @see Factory::createServer() to create a listening socket
- * @see self::listen() has to be called first
- * @uses socket_accept()
- */
- public function accept()
- {
- $resource = @socket_accept($this->resource);
- if ($resource === false) {
- throw Exception::createFromGlobalSocketOperation();
- }
- return new Socket($resource);
- }
- /**
- * binds a name/address/path to this socket
- *
- * has to be called before issuing connect() or listen()
- *
- * @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
- * @return self $this (chainable)
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @uses socket_bind()
- */
- public function bind($address)
- {
- $ret = @socket_bind($this->resource, $this->unformatAddress($address, $port), $port);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $this;
- }
- /**
- * close this socket
- *
- * ATTENTION: make sure to NOT re-use this socket instance after closing it!
- * its socket resource remains closed and most further operations will fail!
- *
- * @return self $this (chainable)
- * @throws \Error PHP 8 only: throws \Error when socket is invalid
- * @see self::shutdown() should be called before closing socket
- * @uses socket_close()
- */
- public function close()
- {
- socket_close($this->resource);
- return $this;
- }
- /**
- * initiate a connection to given address
- *
- * @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
- * @return self $this (chainable)
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @uses socket_connect()
- */
- public function connect($address)
- {
- $ret = @socket_connect($this->resource, $this->unformatAddress($address, $port), $port);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $this;
- }
- /**
- * Initiates a new connection to given address, wait for up to $timeout seconds
- *
- * The given $timeout parameter is an upper bound, a maximum time to wait
- * for the connection to be either accepted or rejected.
- *
- * The resulting socket resource will be set to non-blocking mode,
- * regardless of its previous state and whether this method succedes or
- * if it fails. Make sure to reset with `setBlocking(true)` if you want to
- * continue using blocking calls.
- *
- * @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
- * @param float $timeout maximum time to wait (in seconds)
- * @return self $this (chainable)
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @uses self::setBlocking() to enable non-blocking mode
- * @uses self::connect() to initiate the connection
- * @uses self::selectWrite() to wait for the connection to complete
- * @uses self::assertAlive() to check connection state
- */
- public function connectTimeout($address, $timeout)
- {
- $this->setBlocking(false);
- try {
- // socket is non-blocking, so connect should emit EINPROGRESS
- $this->connect($address);
- // socket is already connected immediately?
- return $this;
- } catch (Exception $e) {
- // non-blocking connect() should be EINPROGRESS (or EWOULDBLOCK on Windows) => otherwise re-throw
- if ($e->getCode() !== SOCKET_EINPROGRESS && $e->getCode() !== SOCKET_EWOULDBLOCK) {
- throw $e;
- }
- // connection should be completed (or rejected) within timeout: socket becomes writable on success or error
- // Windows requires special care because it uses exceptfds for socket errors: https://github.com/reactphp/event-loop/issues/206
- $r = null;
- $w = array($this->resource);
- $e = DIRECTORY_SEPARATOR === '\\' ? $w : null;
- $ret = @socket_select($r, $w, $e, $timeout === null ? null : (int) $timeout, (int) (($timeout - floor($timeout)) * 1000000));
- if ($ret === false) {
- throw Exception::createFromGlobalSocketOperation('Failed to select socket for writing');
- } elseif ($ret === 0) {
- throw new Exception('Timed out while waiting for connection', SOCKET_ETIMEDOUT);
- }
- // confirm connection success (or fail if connected has been rejected)
- $this->assertAlive();
- return $this;
- }
- }
- /**
- * get socket option
- *
- * @param int $level
- * @param int $optname
- * @return mixed
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @uses socket_get_option()
- */
- public function getOption($level, $optname)
- {
- $value = @socket_get_option($this->resource, $level, $optname);
- if ($value === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $value;
- }
- /**
- * get remote side's address/path
- *
- * @return string
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket is invalid
- * @uses socket_getpeername()
- */
- public function getPeerName()
- {
- $ret = @socket_getpeername($this->resource, $address, $port);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $this->formatAddress($address, $port);
- }
- /**
- * get local side's address/path
- *
- * @return string
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket is invalid
- * @uses socket_getsockname()
- */
- public function getSockName()
- {
- $ret = @socket_getsockname($this->resource, $address, $port);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $this->formatAddress($address, $port);
- }
- /**
- * start listen for incoming connections
- *
- * @param int $backlog maximum number of incoming connections to be queued
- * @return self $this (chainable)
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @see self::bind() has to be called first to bind name to socket
- * @uses socket_listen()
- */
- public function listen($backlog = 0)
- {
- $ret = @socket_listen($this->resource, $backlog);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $this;
- }
- /**
- * read up to $length bytes from connect()ed / accept()ed socket
- *
- * The $type parameter specifies if this should use either binary safe reading
- * (PHP_BINARY_READ, the default) or stop at CR or LF characters (PHP_NORMAL_READ)
- *
- * @param int $length maximum length to read
- * @param int $type either of PHP_BINARY_READ (the default) or PHP_NORMAL_READ
- * @return string
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @see self::recv() if you need to pass flags
- * @uses socket_read()
- */
- public function read($length, $type = PHP_BINARY_READ)
- {
- $data = @socket_read($this->resource, $length, $type);
- if ($data === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $data;
- }
- /**
- * receive up to $length bytes from connect()ed / accept()ed socket
- *
- * @param int $length maximum length to read
- * @param int $flags
- * @return string
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @see self::read() if you do not need to pass $flags
- * @see self::recvFrom() if your socket is not connect()ed
- * @uses socket_recv()
- */
- public function recv($length, $flags)
- {
- $ret = @socket_recv($this->resource, $buffer, $length, $flags);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $buffer;
- }
- /**
- * receive up to $length bytes from socket
- *
- * @param int $length maximum length to read
- * @param int $flags
- * @param string $remote reference will be filled with remote/peer address/path
- * @return string
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @see self::recv() if your socket is connect()ed
- * @uses socket_recvfrom()
- */
- public function recvFrom($length, $flags, &$remote)
- {
- $ret = @socket_recvfrom($this->resource, $buffer, $length, $flags, $address, $port);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- $remote = $this->formatAddress($address, $port);
- return $buffer;
- }
- /**
- * check socket to see if a read/recv/revFrom will not block
- *
- * @param float|null $sec maximum time to wait (in seconds), 0 = immediate polling, null = no limit
- * @return boolean true = socket ready (read will not block), false = timeout expired, socket is not ready
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @uses socket_select()
- */
- public function selectRead($sec = 0)
- {
- $usec = $sec === null ? 0 : (int) (($sec - floor($sec)) * 1000000);
- $r = array($this->resource);
- $n = null;
- $ret = @socket_select($r, $n, $n, $sec === null ? null : (int) $sec, $usec);
- if ($ret === false) {
- throw Exception::createFromGlobalSocketOperation('Failed to select socket for reading');
- }
- return !!$ret;
- }
- /**
- * check socket to see if a write/send/sendTo will not block
- *
- * @param float|null $sec maximum time to wait (in seconds), 0 = immediate polling, null = no limit
- * @return boolean true = socket ready (write will not block), false = timeout expired, socket is not ready
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @uses socket_select()
- */
- public function selectWrite($sec = 0)
- {
- $usec = $sec === null ? 0 : (int) (($sec - floor($sec)) * 1000000);
- $w = array($this->resource);
- $n = null;
- $ret = @socket_select($n, $w, $n, $sec === null ? null : (int) $sec, $usec);
- if ($ret === false) {
- throw Exception::createFromGlobalSocketOperation('Failed to select socket for writing');
- }
- return !!$ret;
- }
- /**
- * send given $buffer to connect()ed / accept()ed socket
- *
- * @param string $buffer
- * @param int $flags
- * @return int number of bytes actually written (make sure to check against given buffer length!)
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @see self::write() if you do not need to pass $flags
- * @see self::sendTo() if your socket is not connect()ed
- * @uses socket_send()
- */
- public function send($buffer, $flags)
- {
- $ret = @socket_send($this->resource, $buffer, strlen($buffer), $flags);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $ret;
- }
- /**
- * send given $buffer to socket
- *
- * @param string $buffer
- * @param int $flags
- * @param string $remote remote/peer address/path
- * @return int number of bytes actually written
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @see self::send() if your socket is connect()ed
- * @uses socket_sendto()
- */
- public function sendTo($buffer, $flags, $remote)
- {
- $ret = @socket_sendto($this->resource, $buffer, strlen($buffer), $flags, $this->unformatAddress($remote, $port), $port);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $ret;
- }
- /**
- * enable/disable blocking/nonblocking mode (O_NONBLOCK flag)
- *
- * @param boolean $toggle
- * @return self $this (chainable)
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @uses socket_set_block()
- * @uses socket_set_nonblock()
- */
- public function setBlocking($toggle = true)
- {
- $ret = $toggle ? @socket_set_block($this->resource) : @socket_set_nonblock($this->resource);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $this;
- }
- /**
- * set socket option
- *
- * @param int $level
- * @param int $optname
- * @param mixed $optval
- * @return self $this (chainable)
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @see self::getOption()
- * @uses socket_set_option()
- */
- public function setOption($level, $optname, $optval)
- {
- $ret = @socket_set_option($this->resource, $level, $optname, $optval);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $this;
- }
- /**
- * shuts down socket for receiving, sending or both
- *
- * @param int $how 0 = shutdown reading, 1 = shutdown writing, 2 = shutdown reading and writing
- * @return self $this (chainable)
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @see self::close()
- * @uses socket_shutdown()
- */
- public function shutdown($how = 2)
- {
- $ret = @socket_shutdown($this->resource, $how);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $this;
- }
- /**
- * write $buffer to connect()ed / accept()ed socket
- *
- * @param string $buffer
- * @return int number of bytes actually written
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
- * @see self::send() if you need to pass flags
- * @uses socket_write()
- */
- public function write($buffer)
- {
- $ret = @socket_write($this->resource, $buffer);
- if ($ret === false) {
- throw Exception::createFromSocketResource($this->resource);
- }
- return $ret;
- }
- /**
- * get socket type as passed to socket_create()
- *
- * @return int usually either SOCK_STREAM or SOCK_DGRAM
- * @throws Exception on error
- * @throws \Error PHP 8 only: throws \Error when socket is invalid
- * @uses self::getOption()
- */
- public function getType()
- {
- return $this->getOption(SOL_SOCKET, SO_TYPE);
- }
- /**
- * assert that this socket is alive and its error code is 0
- *
- * This will fetch and reset the current socket error code from the
- * socket and options and will throw an Exception along with error
- * message and code if the code is not 0, i.e. if it does indicate
- * an error situation.
- *
- * Calling this method should not be needed in most cases and is
- * likely to not throw an Exception. Each socket operation like
- * connect(), send(), etc. will throw a dedicated Exception in case
- * of an error anyway.
- *
- * @return self $this (chainable)
- * @throws Exception if error code is not 0
- * @throws \Error PHP 8 only: throws \Error when socket is invalid
- * @uses self::getOption() to retrieve and clear current error code
- * @uses self::getErrorMessage() to translate error code to
- */
- public function assertAlive()
- {
- $code = $this->getOption(SOL_SOCKET, SO_ERROR);
- if ($code !== 0) {
- throw Exception::createFromCode($code, 'Socket error');
- }
- return $this;
- }
- /**
- * format given address/host/path and port
- *
- * @param string $address
- * @param int $port
- * @return string
- */
- protected function formatAddress($address, $port)
- {
- if ($port !== 0) {
- if (strpos($address, ':') !== false) {
- $address = '[' . $address . ']';
- }
- $address .= ':' . $port;
- }
- return $address;
- }
- /**
- * format given address by splitting it into returned address and port set by reference
- *
- * @param string $address
- * @param int $port
- * @return string address with port removed
- */
- protected function unformatAddress($address, &$port)
- {
- // [::1]:2 => ::1 2
- // test:2 => test 2
- // ::1 => ::1
- // test => test
- $colon = strrpos($address, ':');
- // there is a colon and this is the only colon or there's a closing IPv6 bracket right before it
- if ($colon !== false && (strpos($address, ':') === $colon || strpos($address, ']') === ($colon - 1))) {
- $port = (int)substr($address, $colon + 1);
- $address = substr($address, 0, $colon);
- // remove IPv6 square brackets
- if (substr($address, 0, 1) === '[') {
- $address = substr($address, 1, -1);
- }
- }
- return $address;
- }
- }
|