TcpServer.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. <?php
  2. namespace React\Socket;
  3. use Evenement\EventEmitter;
  4. use React\EventLoop\Loop;
  5. use React\EventLoop\LoopInterface;
  6. use InvalidArgumentException;
  7. use RuntimeException;
  8. /**
  9. * The `TcpServer` class implements the `ServerInterface` and
  10. * is responsible for accepting plaintext TCP/IP connections.
  11. *
  12. * ```php
  13. * $server = new React\Socket\TcpServer(8080);
  14. * ```
  15. *
  16. * Whenever a client connects, it will emit a `connection` event with a connection
  17. * instance implementing `ConnectionInterface`:
  18. *
  19. * ```php
  20. * $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
  21. * echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL;
  22. * $connection->write('hello there!' . PHP_EOL);
  23. * …
  24. * });
  25. * ```
  26. *
  27. * See also the `ServerInterface` for more details.
  28. *
  29. * @see ServerInterface
  30. * @see ConnectionInterface
  31. */
  32. final class TcpServer extends EventEmitter implements ServerInterface
  33. {
  34. private $master;
  35. private $loop;
  36. private $listening = false;
  37. /**
  38. * Creates a plaintext TCP/IP socket server and starts listening on the given address
  39. *
  40. * This starts accepting new incoming connections on the given address.
  41. * See also the `connection event` documented in the `ServerInterface`
  42. * for more details.
  43. *
  44. * ```php
  45. * $server = new React\Socket\TcpServer(8080);
  46. * ```
  47. *
  48. * As above, the `$uri` parameter can consist of only a port, in which case the
  49. * server will default to listening on the localhost address `127.0.0.1`,
  50. * which means it will not be reachable from outside of this system.
  51. *
  52. * In order to use a random port assignment, you can use the port `0`:
  53. *
  54. * ```php
  55. * $server = new React\Socket\TcpServer(0);
  56. * $address = $server->getAddress();
  57. * ```
  58. *
  59. * In order to change the host the socket is listening on, you can provide an IP
  60. * address through the first parameter provided to the constructor, optionally
  61. * preceded by the `tcp://` scheme:
  62. *
  63. * ```php
  64. * $server = new React\Socket\TcpServer('192.168.0.1:8080');
  65. * ```
  66. *
  67. * If you want to listen on an IPv6 address, you MUST enclose the host in square
  68. * brackets:
  69. *
  70. * ```php
  71. * $server = new React\Socket\TcpServer('[::1]:8080');
  72. * ```
  73. *
  74. * If the given URI is invalid, does not contain a port, any other scheme or if it
  75. * contains a hostname, it will throw an `InvalidArgumentException`:
  76. *
  77. * ```php
  78. * // throws InvalidArgumentException due to missing port
  79. * $server = new React\Socket\TcpServer('127.0.0.1');
  80. * ```
  81. *
  82. * If the given URI appears to be valid, but listening on it fails (such as if port
  83. * is already in use or port below 1024 may require root access etc.), it will
  84. * throw a `RuntimeException`:
  85. *
  86. * ```php
  87. * $first = new React\Socket\TcpServer(8080);
  88. *
  89. * // throws RuntimeException because port is already in use
  90. * $second = new React\Socket\TcpServer(8080);
  91. * ```
  92. *
  93. * Note that these error conditions may vary depending on your system and/or
  94. * configuration.
  95. * See the exception message and code for more details about the actual error
  96. * condition.
  97. *
  98. * This class takes an optional `LoopInterface|null $loop` parameter that can be used to
  99. * pass the event loop instance to use for this object. You can use a `null` value
  100. * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
  101. * This value SHOULD NOT be given unless you're sure you want to explicitly use a
  102. * given event loop instance.
  103. *
  104. * Optionally, you can specify [socket context options](https://www.php.net/manual/en/context.socket.php)
  105. * for the underlying stream socket resource like this:
  106. *
  107. * ```php
  108. * $server = new React\Socket\TcpServer('[::1]:8080', null, array(
  109. * 'backlog' => 200,
  110. * 'so_reuseport' => true,
  111. * 'ipv6_v6only' => true
  112. * ));
  113. * ```
  114. *
  115. * Note that available [socket context options](https://www.php.net/manual/en/context.socket.php),
  116. * their defaults and effects of changing these may vary depending on your system
  117. * and/or PHP version.
  118. * Passing unknown context options has no effect.
  119. * The `backlog` context option defaults to `511` unless given explicitly.
  120. *
  121. * @param string|int $uri
  122. * @param ?LoopInterface $loop
  123. * @param array $context
  124. * @throws InvalidArgumentException if the listening address is invalid
  125. * @throws RuntimeException if listening on this address fails (already in use etc.)
  126. */
  127. public function __construct($uri, LoopInterface $loop = null, array $context = array())
  128. {
  129. $this->loop = $loop ?: Loop::get();
  130. // a single port has been given => assume localhost
  131. if ((string)(int)$uri === (string)$uri) {
  132. $uri = '127.0.0.1:' . $uri;
  133. }
  134. // assume default scheme if none has been given
  135. if (\strpos($uri, '://') === false) {
  136. $uri = 'tcp://' . $uri;
  137. }
  138. // parse_url() does not accept null ports (random port assignment) => manually remove
  139. if (\substr($uri, -2) === ':0') {
  140. $parts = \parse_url(\substr($uri, 0, -2));
  141. if ($parts) {
  142. $parts['port'] = 0;
  143. }
  144. } else {
  145. $parts = \parse_url($uri);
  146. }
  147. // ensure URI contains TCP scheme, host and port
  148. if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') {
  149. throw new \InvalidArgumentException(
  150. 'Invalid URI "' . $uri . '" given (EINVAL)',
  151. \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
  152. );
  153. }
  154. if (@\inet_pton(\trim($parts['host'], '[]')) === false) {
  155. throw new \InvalidArgumentException(
  156. 'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)',
  157. \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
  158. );
  159. }
  160. $this->master = @\stream_socket_server(
  161. $uri,
  162. $errno,
  163. $errstr,
  164. \STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN,
  165. \stream_context_create(array('socket' => $context + array('backlog' => 511)))
  166. );
  167. if (false === $this->master) {
  168. if ($errno === 0) {
  169. // PHP does not seem to report errno, so match errno from errstr
  170. // @link https://3v4l.org/3qOBl
  171. $errno = SocketServer::errno($errstr);
  172. }
  173. throw new \RuntimeException(
  174. 'Failed to listen on "' . $uri . '": ' . $errstr . SocketServer::errconst($errno),
  175. $errno
  176. );
  177. }
  178. \stream_set_blocking($this->master, false);
  179. $this->resume();
  180. }
  181. public function getAddress()
  182. {
  183. if (!\is_resource($this->master)) {
  184. return null;
  185. }
  186. $address = \stream_socket_get_name($this->master, false);
  187. // check if this is an IPv6 address which includes multiple colons but no square brackets
  188. $pos = \strrpos($address, ':');
  189. if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') {
  190. $address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore
  191. }
  192. return 'tcp://' . $address;
  193. }
  194. public function pause()
  195. {
  196. if (!$this->listening) {
  197. return;
  198. }
  199. $this->loop->removeReadStream($this->master);
  200. $this->listening = false;
  201. }
  202. public function resume()
  203. {
  204. if ($this->listening || !\is_resource($this->master)) {
  205. return;
  206. }
  207. $that = $this;
  208. $this->loop->addReadStream($this->master, function ($master) use ($that) {
  209. try {
  210. $newSocket = SocketServer::accept($master);
  211. } catch (\RuntimeException $e) {
  212. $that->emit('error', array($e));
  213. return;
  214. }
  215. $that->handleConnection($newSocket);
  216. });
  217. $this->listening = true;
  218. }
  219. public function close()
  220. {
  221. if (!\is_resource($this->master)) {
  222. return;
  223. }
  224. $this->pause();
  225. \fclose($this->master);
  226. $this->removeAllListeners();
  227. }
  228. /** @internal */
  229. public function handleConnection($socket)
  230. {
  231. $this->emit('connection', array(
  232. new Connection($socket, $this->loop)
  233. ));
  234. }
  235. }