Skip to content

Commit fd9a1ab

Browse files
committed
Internal FdServer implementation to listen on file descriptors (FDs)
1 parent d92d0ec commit fd9a1ab

File tree

2 files changed

+525
-0
lines changed

2 files changed

+525
-0
lines changed

src/FdServer.php

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php
2+
3+
namespace React\Socket;
4+
5+
use Evenement\EventEmitter;
6+
use React\EventLoop\Loop;
7+
use React\EventLoop\LoopInterface;
8+
9+
/**
10+
* [Internal] The `FdServer` class implements the `ServerInterface` and
11+
* is responsible for accepting connections from an existing file descriptor.
12+
*
13+
* ```php
14+
* $socket = new React\Socket\FdServer(3);
15+
* ```
16+
*
17+
* Whenever a client connects, it will emit a `connection` event with a connection
18+
* instance implementing `ConnectionInterface`:
19+
*
20+
* ```php
21+
* $socket->on('connection', function (ConnectionInterface $connection) {
22+
* echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL;
23+
* $connection->write('hello there!' . PHP_EOL);
24+
* …
25+
* });
26+
* ```
27+
*
28+
* See also the `ServerInterface` for more details.
29+
*
30+
* @see ServerInterface
31+
* @see ConnectionInterface
32+
* @internal
33+
*/
34+
final class FdServer extends EventEmitter implements ServerInterface
35+
{
36+
private $master;
37+
private $loop;
38+
private $listening = false;
39+
40+
/**
41+
* Creates a socket server and starts listening on the given file descriptor
42+
*
43+
* This starts accepting new incoming connections on the given file descriptor.
44+
* See also the `connection event` documented in the `ServerInterface`
45+
* for more details.
46+
*
47+
* ```php
48+
* $socket = new React\Socket\FdServer(3);
49+
* ```
50+
*
51+
* If the given FD is invalid or out of range, it will throw an `InvalidArgumentException`:
52+
*
53+
* ```php
54+
* // throws InvalidArgumentException
55+
* $socket = new React\Socket\FdServer(-1);
56+
* ```
57+
*
58+
* If the given FD appears to be valid, but listening on it fails (such as
59+
* if the FD does not exist or does not refer to a socket server), it will
60+
* throw a `RuntimeException`:
61+
*
62+
* ```php
63+
* // throws RuntimeException because FD does not reference a socket server
64+
* $socket = new React\Socket\FdServer(0, $loop);
65+
* ```
66+
*
67+
* Note that these error conditions may vary depending on your system and/or
68+
* configuration.
69+
* See the exception message and code for more details about the actual error
70+
* condition.
71+
*
72+
* @param int $fd
73+
* @param ?LoopInterface $loop
74+
* @throws \InvalidArgumentException if the listening address is invalid
75+
* @throws \RuntimeException if listening on this address fails (already in use etc.)
76+
*/
77+
public function __construct($fd, LoopInterface $loop = null)
78+
{
79+
if (!\is_int($fd) || $fd < 0 || $fd >= \PHP_INT_MAX) {
80+
throw new \InvalidArgumentException('Invalid FD number given');
81+
}
82+
83+
$this->loop = $loop ?: Loop::get();
84+
85+
$this->master = @\fopen('php://fd/' . $fd, 'r+');
86+
if (false === $this->master) {
87+
// Match errstr from PHP's warning message.
88+
// fopen(php://fd/3): Failed to open stream: Error duping file descriptor 3; possibly it doesn't exist: [9]: Bad file descriptor
89+
$error = \error_get_last();
90+
\preg_match('/\[(\d+)\]: (.*)/', $error['message'], $m);
91+
$errno = isset($m[1]) ? (int) $m[1] : 0;
92+
$errstr = isset($m[2]) ? $m[2] : $error['message'];
93+
94+
throw new \RuntimeException('Failed to listen on FD ' . $fd . ': ' . $errstr, $errno);
95+
}
96+
97+
$meta = \stream_get_meta_data($this->master);
98+
if (!isset($meta['stream_type']) || $meta['stream_type'] !== 'tcp_socket') {
99+
\fclose($this->master);
100+
101+
$errno = \defined('SOCKET_ENOTSOCK') ? \SOCKET_ENOTSOCK : 88;
102+
$errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Not a socket';
103+
104+
throw new \RuntimeException('Failed to listen on FD ' . $fd . ': ' . $errstr, $errno);
105+
}
106+
107+
// Socket should not have a peer address if this is a listening socket.
108+
// Looks like this work-around is the closest we can get because PHP doesn't expose SO_ACCEPTCONN even with ext-sockets.
109+
if (\stream_socket_get_name($this->master, true) !== false) {
110+
\fclose($this->master);
111+
112+
$errno = \defined('SOCKET_EISCONN') ? \SOCKET_EISCONN : 106;
113+
$errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Socket is connected';
114+
115+
throw new \RuntimeException('Failed to listen on FD ' . $fd . ': ' . $errstr, $errno);
116+
}
117+
118+
\stream_set_blocking($this->master, false);
119+
120+
$this->resume();
121+
}
122+
123+
public function getAddress()
124+
{
125+
if (!\is_resource($this->master)) {
126+
return null;
127+
}
128+
129+
$address = \stream_socket_get_name($this->master, false);
130+
131+
// check if this is an IPv6 address which includes multiple colons but no square brackets
132+
$pos = \strrpos($address, ':');
133+
if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') {
134+
$address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore
135+
}
136+
137+
return 'tcp://' . $address;
138+
}
139+
140+
public function pause()
141+
{
142+
if (!$this->listening) {
143+
return;
144+
}
145+
146+
$this->loop->removeReadStream($this->master);
147+
$this->listening = false;
148+
}
149+
150+
public function resume()
151+
{
152+
if ($this->listening || !\is_resource($this->master)) {
153+
return;
154+
}
155+
156+
$that = $this;
157+
$this->loop->addReadStream($this->master, function ($master) use ($that) {
158+
try {
159+
$newSocket = SocketServer::accept($master);
160+
} catch (\RuntimeException $e) {
161+
$that->emit('error', array($e));
162+
return;
163+
}
164+
$that->handleConnection($newSocket);
165+
});
166+
$this->listening = true;
167+
}
168+
169+
public function close()
170+
{
171+
if (!\is_resource($this->master)) {
172+
return;
173+
}
174+
175+
$this->pause();
176+
\fclose($this->master);
177+
$this->removeAllListeners();
178+
}
179+
180+
/** @internal */
181+
public function handleConnection($socket)
182+
{
183+
$this->emit('connection', array(
184+
new Connection($socket, $this->loop)
185+
));
186+
}
187+
}

0 commit comments

Comments
 (0)