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