SocketServer.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. <?php
  2. namespace React\Socket;
  3. use Evenement\EventEmitter;
  4. use React\EventLoop\LoopInterface;
  5. final class SocketServer extends EventEmitter implements ServerInterface
  6. {
  7. private $server;
  8. /**
  9. * The `SocketServer` class is the main class in this package that implements the `ServerInterface` and
  10. * allows you to accept incoming streaming connections, such as plaintext TCP/IP or secure TLS connection streams.
  11. *
  12. * ```php
  13. * $socket = new React\Socket\SocketServer('127.0.0.1:0');
  14. * $socket = new React\Socket\SocketServer('127.0.0.1:8000');
  15. * $socket = new React\Socket\SocketServer('127.0.0.1:8000', $context);
  16. * ```
  17. *
  18. * This class takes an optional `LoopInterface|null $loop` parameter that can be used to
  19. * pass the event loop instance to use for this object. You can use a `null` value
  20. * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
  21. * This value SHOULD NOT be given unless you're sure you want to explicitly use a
  22. * given event loop instance.
  23. *
  24. * @param string $uri
  25. * @param array $context
  26. * @param ?LoopInterface $loop
  27. * @throws \InvalidArgumentException if the listening address is invalid
  28. * @throws \RuntimeException if listening on this address fails (already in use etc.)
  29. */
  30. public function __construct($uri, array $context = array(), $loop = null)
  31. {
  32. if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
  33. throw new \InvalidArgumentException('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface');
  34. }
  35. // apply default options if not explicitly given
  36. $context += array(
  37. 'tcp' => array(),
  38. 'tls' => array(),
  39. 'unix' => array()
  40. );
  41. $scheme = 'tcp';
  42. $pos = \strpos($uri, '://');
  43. if ($pos !== false) {
  44. $scheme = \substr($uri, 0, $pos);
  45. }
  46. if ($scheme === 'unix') {
  47. $server = new UnixServer($uri, $loop, $context['unix']);
  48. } elseif ($scheme === 'php') {
  49. $server = new FdServer($uri, $loop);
  50. } else {
  51. if (preg_match('#^(?:\w+://)?\d+$#', $uri)) {
  52. throw new \InvalidArgumentException(
  53. 'Invalid URI given (EINVAL)',
  54. \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
  55. );
  56. }
  57. $server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']);
  58. if ($scheme === 'tls') {
  59. $server = new SecureServer($server, $loop, $context['tls']);
  60. }
  61. }
  62. $this->server = $server;
  63. $that = $this;
  64. $server->on('connection', function (ConnectionInterface $conn) use ($that) {
  65. $that->emit('connection', array($conn));
  66. });
  67. $server->on('error', function (\Exception $error) use ($that) {
  68. $that->emit('error', array($error));
  69. });
  70. }
  71. public function getAddress()
  72. {
  73. return $this->server->getAddress();
  74. }
  75. public function pause()
  76. {
  77. $this->server->pause();
  78. }
  79. public function resume()
  80. {
  81. $this->server->resume();
  82. }
  83. public function close()
  84. {
  85. $this->server->close();
  86. }
  87. /**
  88. * [internal] Internal helper method to accept new connection from given server socket
  89. *
  90. * @param resource $socket server socket to accept connection from
  91. * @return resource new client socket if any
  92. * @throws \RuntimeException if accepting fails
  93. * @internal
  94. */
  95. public static function accept($socket)
  96. {
  97. $errno = 0;
  98. $errstr = '';
  99. \set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
  100. // Match errstr from PHP's warning message.
  101. // stream_socket_accept(): accept failed: Connection timed out
  102. $errstr = \preg_replace('#.*: #', '', $error);
  103. $errno = SocketServer::errno($errstr);
  104. });
  105. $newSocket = \stream_socket_accept($socket, 0);
  106. \restore_error_handler();
  107. if (false === $newSocket) {
  108. throw new \RuntimeException(
  109. 'Unable to accept new connection: ' . $errstr . self::errconst($errno),
  110. $errno
  111. );
  112. }
  113. return $newSocket;
  114. }
  115. /**
  116. * [Internal] Returns errno value for given errstr
  117. *
  118. * The errno and errstr values describes the type of error that has been
  119. * encountered. This method tries to look up the given errstr and find a
  120. * matching errno value which can be useful to provide more context to error
  121. * messages. It goes through the list of known errno constants when either
  122. * `ext-sockets`, `ext-posix` or `ext-pcntl` is available to find an errno
  123. * matching the given errstr.
  124. *
  125. * @param string $errstr
  126. * @return int errno value (e.g. value of `SOCKET_ECONNREFUSED`) or 0 if not found
  127. * @internal
  128. * @copyright Copyright (c) 2023 Christian Lück, taken from https://github.com/clue/errno with permission
  129. * @codeCoverageIgnore
  130. */
  131. public static function errno($errstr)
  132. {
  133. // PHP defines the required `strerror()` function through either `ext-sockets`, `ext-posix` or `ext-pcntl`
  134. $strerror = \function_exists('socket_strerror') ? 'socket_strerror' : (\function_exists('posix_strerror') ? 'posix_strerror' : (\function_exists('pcntl_strerror') ? 'pcntl_strerror' : null));
  135. if ($strerror !== null) {
  136. assert(\is_string($strerror) && \is_callable($strerror));
  137. // PHP defines most useful errno constants like `ECONNREFUSED` through constants in `ext-sockets` like `SOCKET_ECONNREFUSED`
  138. // PHP also defines a hand full of errno constants like `EMFILE` through constants in `ext-pcntl` like `PCNTL_EMFILE`
  139. // go through list of all defined constants like `SOCKET_E*` and `PCNTL_E*` and see if they match the given `$errstr`
  140. foreach (\get_defined_constants(false) as $name => $value) {
  141. if (\is_int($value) && (\strpos($name, 'SOCKET_E') === 0 || \strpos($name, 'PCNTL_E') === 0) && $strerror($value) === $errstr) {
  142. return $value;
  143. }
  144. }
  145. // if we reach this, no matching errno constant could be found (unlikely when `ext-sockets` is available)
  146. // go through list of all possible errno values from 1 to `MAX_ERRNO` and see if they match the given `$errstr`
  147. for ($errno = 1, $max = \defined('MAX_ERRNO') ? \MAX_ERRNO : 4095; $errno <= $max; ++$errno) {
  148. if ($strerror($errno) === $errstr) {
  149. return $errno;
  150. }
  151. }
  152. }
  153. // if we reach this, no matching errno value could be found (unlikely when either `ext-sockets`, `ext-posix` or `ext-pcntl` is available)
  154. return 0;
  155. }
  156. /**
  157. * [Internal] Returns errno constant name for given errno value
  158. *
  159. * The errno value describes the type of error that has been encountered.
  160. * This method tries to look up the given errno value and find a matching
  161. * errno constant name which can be useful to provide more context and more
  162. * descriptive error messages. It goes through the list of known errno
  163. * constants when either `ext-sockets` or `ext-pcntl` is available to find
  164. * the matching errno constant name.
  165. *
  166. * Because this method is used to append more context to error messages, the
  167. * constant name will be prefixed with a space and put between parenthesis
  168. * when found.
  169. *
  170. * @param int $errno
  171. * @return string e.g. ` (ECONNREFUSED)` or empty string if no matching const for the given errno could be found
  172. * @internal
  173. * @copyright Copyright (c) 2023 Christian Lück, taken from https://github.com/clue/errno with permission
  174. * @codeCoverageIgnore
  175. */
  176. public static function errconst($errno)
  177. {
  178. // PHP defines most useful errno constants like `ECONNREFUSED` through constants in `ext-sockets` like `SOCKET_ECONNREFUSED`
  179. // PHP also defines a hand full of errno constants like `EMFILE` through constants in `ext-pcntl` like `PCNTL_EMFILE`
  180. // go through list of all defined constants like `SOCKET_E*` and `PCNTL_E*` and see if they match the given `$errno`
  181. foreach (\get_defined_constants(false) as $name => $value) {
  182. if ($value === $errno && (\strpos($name, 'SOCKET_E') === 0 || \strpos($name, 'PCNTL_E') === 0)) {
  183. return ' (' . \substr($name, \strpos($name, '_') + 1) . ')';
  184. }
  185. }
  186. // if we reach this, no matching errno constant could be found (unlikely when `ext-sockets` is available)
  187. return '';
  188. }
  189. }