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