123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- <?php
- namespace React\Socket;
- use Evenement\EventEmitter;
- use React\EventLoop\Loop;
- use React\EventLoop\LoopInterface;
- use InvalidArgumentException;
- use RuntimeException;
- /**
- * The `TcpServer` class implements the `ServerInterface` and
- * is responsible for accepting plaintext TCP/IP connections.
- *
- * ```php
- * $server = new React\Socket\TcpServer(8080);
- * ```
- *
- * Whenever a client connects, it will emit a `connection` event with a connection
- * instance implementing `ConnectionInterface`:
- *
- * ```php
- * $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
- * echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL;
- * $connection->write('hello there!' . PHP_EOL);
- * …
- * });
- * ```
- *
- * See also the `ServerInterface` for more details.
- *
- * @see ServerInterface
- * @see ConnectionInterface
- */
- final class TcpServer extends EventEmitter implements ServerInterface
- {
- private $master;
- private $loop;
- private $listening = false;
- /**
- * Creates a plaintext TCP/IP socket server and starts listening on the given address
- *
- * This starts accepting new incoming connections on the given address.
- * See also the `connection event` documented in the `ServerInterface`
- * for more details.
- *
- * ```php
- * $server = new React\Socket\TcpServer(8080);
- * ```
- *
- * As above, the `$uri` parameter can consist of only a port, in which case the
- * server will default to listening on the localhost address `127.0.0.1`,
- * which means it will not be reachable from outside of this system.
- *
- * In order to use a random port assignment, you can use the port `0`:
- *
- * ```php
- * $server = new React\Socket\TcpServer(0);
- * $address = $server->getAddress();
- * ```
- *
- * In order to change the host the socket is listening on, you can provide an IP
- * address through the first parameter provided to the constructor, optionally
- * preceded by the `tcp://` scheme:
- *
- * ```php
- * $server = new React\Socket\TcpServer('192.168.0.1:8080');
- * ```
- *
- * If you want to listen on an IPv6 address, you MUST enclose the host in square
- * brackets:
- *
- * ```php
- * $server = new React\Socket\TcpServer('[::1]:8080');
- * ```
- *
- * If the given URI is invalid, does not contain a port, any other scheme or if it
- * contains a hostname, it will throw an `InvalidArgumentException`:
- *
- * ```php
- * // throws InvalidArgumentException due to missing port
- * $server = new React\Socket\TcpServer('127.0.0.1');
- * ```
- *
- * If the given URI appears to be valid, but listening on it fails (such as if port
- * is already in use or port below 1024 may require root access etc.), it will
- * throw a `RuntimeException`:
- *
- * ```php
- * $first = new React\Socket\TcpServer(8080);
- *
- * // throws RuntimeException because port is already in use
- * $second = new React\Socket\TcpServer(8080);
- * ```
- *
- * Note that these error conditions may vary depending on your system and/or
- * configuration.
- * See the exception message and code for more details about the actual error
- * condition.
- *
- * This class takes an optional `LoopInterface|null $loop` parameter that can be used to
- * pass the event loop instance to use for this object. You can use a `null` value
- * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
- * This value SHOULD NOT be given unless you're sure you want to explicitly use a
- * given event loop instance.
- *
- * Optionally, you can specify [socket context options](https://www.php.net/manual/en/context.socket.php)
- * for the underlying stream socket resource like this:
- *
- * ```php
- * $server = new React\Socket\TcpServer('[::1]:8080', null, array(
- * 'backlog' => 200,
- * 'so_reuseport' => true,
- * 'ipv6_v6only' => true
- * ));
- * ```
- *
- * Note that available [socket context options](https://www.php.net/manual/en/context.socket.php),
- * their defaults and effects of changing these may vary depending on your system
- * and/or PHP version.
- * Passing unknown context options has no effect.
- * The `backlog` context option defaults to `511` unless given explicitly.
- *
- * @param string|int $uri
- * @param ?LoopInterface $loop
- * @param array $context
- * @throws InvalidArgumentException if the listening address is invalid
- * @throws RuntimeException if listening on this address fails (already in use etc.)
- */
- public function __construct($uri, LoopInterface $loop = null, array $context = array())
- {
- $this->loop = $loop ?: Loop::get();
- // a single port has been given => assume localhost
- if ((string)(int)$uri === (string)$uri) {
- $uri = '127.0.0.1:' . $uri;
- }
- // assume default scheme if none has been given
- if (\strpos($uri, '://') === false) {
- $uri = 'tcp://' . $uri;
- }
- // parse_url() does not accept null ports (random port assignment) => manually remove
- if (\substr($uri, -2) === ':0') {
- $parts = \parse_url(\substr($uri, 0, -2));
- if ($parts) {
- $parts['port'] = 0;
- }
- } else {
- $parts = \parse_url($uri);
- }
- // ensure URI contains TCP scheme, host and port
- if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') {
- throw new \InvalidArgumentException(
- 'Invalid URI "' . $uri . '" given (EINVAL)',
- \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
- );
- }
- if (@\inet_pton(\trim($parts['host'], '[]')) === false) {
- throw new \InvalidArgumentException(
- 'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)',
- \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
- );
- }
- $this->master = @\stream_socket_server(
- $uri,
- $errno,
- $errstr,
- \STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN,
- \stream_context_create(array('socket' => $context + array('backlog' => 511)))
- );
- if (false === $this->master) {
- if ($errno === 0) {
- // PHP does not seem to report errno, so match errno from errstr
- // @link https://3v4l.org/3qOBl
- $errno = SocketServer::errno($errstr);
- }
- throw new \RuntimeException(
- 'Failed to listen on "' . $uri . '": ' . $errstr . SocketServer::errconst($errno),
- $errno
- );
- }
- \stream_set_blocking($this->master, false);
- $this->resume();
- }
- public function getAddress()
- {
- if (!\is_resource($this->master)) {
- return null;
- }
- $address = \stream_socket_get_name($this->master, false);
- // check if this is an IPv6 address which includes multiple colons but no square brackets
- $pos = \strrpos($address, ':');
- if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') {
- $address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore
- }
- return 'tcp://' . $address;
- }
- public function pause()
- {
- if (!$this->listening) {
- return;
- }
- $this->loop->removeReadStream($this->master);
- $this->listening = false;
- }
- public function resume()
- {
- if ($this->listening || !\is_resource($this->master)) {
- return;
- }
- $that = $this;
- $this->loop->addReadStream($this->master, function ($master) use ($that) {
- try {
- $newSocket = SocketServer::accept($master);
- } catch (\RuntimeException $e) {
- $that->emit('error', array($e));
- return;
- }
- $that->handleConnection($newSocket);
- });
- $this->listening = true;
- }
- public function close()
- {
- if (!\is_resource($this->master)) {
- return;
- }
- $this->pause();
- \fclose($this->master);
- $this->removeAllListeners();
- }
- /** @internal */
- public function handleConnection($socket)
- {
- $this->emit('connection', array(
- new Connection($socket, $this->loop)
- ));
- }
- }
|