Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 626bf5b

Browse files
authored
Merge pull request #120 from andig/unix
Support Unix domain socket (UDS) server
2 parents fa0b187 + 9257632 commit 626bf5b

8 files changed

+524
-7
lines changed

README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ handle multiple concurrent connections without blocking.
3434
* [Advanced server usage](#advanced-server-usage)
3535
* [TcpServer](#tcpserver)
3636
* [SecureServer](#secureserver)
37+
* [UnixServer](#unixserver)
3738
* [LimitingServer](#limitingserver)
3839
* [getConnections()](#getconnections)
3940
* [Client usage](#client-usage)
@@ -255,7 +256,8 @@ If the address can not be determined or is unknown at this time (such as
255256
after the socket has been closed), it MAY return a `NULL` value instead.
256257

257258
Otherwise, it will return the full address (URI) as a string value, such
258-
as `tcp://127.0.0.1:8080`, `tcp://[::1]:80` or `tls://127.0.0.1:443`.
259+
as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`
260+
`unix://example.sock` or `unix:///path/to/example.sock`.
259261
Note that individual URI components are application specific and depend
260262
on the underlying transport protocol.
261263

@@ -342,6 +344,7 @@ Calling this method more than once on the same instance is a NO-OP.
342344
The `Server` class is the main class in this package that implements the
343345
[`ServerInterface`](#serverinterface) and allows you to accept incoming
344346
streaming connections, such as plaintext TCP/IP or secure TLS connection streams.
347+
Connections can also be accepted on Unix domain sockets.
345348

346349
```php
347350
$server = new Server(8080, $loop);
@@ -373,6 +376,13 @@ brackets:
373376
$server = new Server('[::1]:8080', $loop);
374377
```
375378

379+
To listen on a Unix domain socket (UDS) path, you MUST prefix the URI with the
380+
`unix://` scheme:
381+
382+
```php
383+
$server = new Server('unix:///tmp/server.sock', $loop);
384+
```
385+
376386
If the given URI is invalid, does not contain a port, any other scheme or if it
377387
contains a hostname, it will throw an `InvalidArgumentException`:
378388

@@ -648,6 +658,43 @@ If you use a custom `ServerInterface` and its `connection` event does not
648658
meet this requirement, the `SecureServer` will emit an `error` event and
649659
then close the underlying connection.
650660

661+
#### UnixServer
662+
663+
The `UnixServer` class implements the [`ServerInterface`](#serverinterface) and
664+
is responsible for accepting connections on Unix domain sockets (UDS).
665+
666+
```php
667+
$server = new UnixServer('/tmp/server.sock', $loop);
668+
```
669+
670+
As above, the `$uri` parameter can consist of only a socket path or socket path
671+
prefixed by the `unix://` scheme.
672+
673+
If the given URI appears to be valid, but listening on it fails (such as if the
674+
socket is already in use or the file not accessible etc.), it will throw a
675+
`RuntimeException`:
676+
677+
```php
678+
$first = new UnixServer('/tmp/same.sock', $loop);
679+
680+
// throws RuntimeException because socket is already in use
681+
$second = new UnixServer('/tmp/same.sock', $loop);
682+
```
683+
684+
Whenever a client connects, it will emit a `connection` event with a connection
685+
instance implementing [`ConnectionInterface`](#connectioninterface):
686+
687+
```php
688+
$server->on('connection', function (ConnectionInterface $connection) {
689+
echo 'New connection' . PHP_EOL;
690+
691+
$connection->write('hello there!' . PHP_EOL);
692+
693+
});
694+
```
695+
696+
See also the [`ServerInterface`](#serverinterface) for more details.
697+
651698
#### LimitingServer
652699

653700
The `LimitingServer` decorator wraps a given `ServerInterface` and is responsible

examples/01-echo.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
//
1111
// $ php examples/01-echo.php tls://127.0.0.1:8000 examples/localhost.pem
1212
// $ openssl s_client -connect localhost:8000
13+
//
14+
// You can also run a Unix domain socket (UDS) server like this:
15+
//
16+
// $ php examples/01-echo.php unix:///tmp/server.sock
17+
// $ nc -U /tmp/server.sock
1318

1419
use React\EventLoop\Factory;
1520
use React\Socket\Server;

examples/02-chat-server.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
//
1111
// $ php examples/02-chat-server.php tls://127.0.0.1:8000 examples/localhost.pem
1212
// $ openssl s_client -connect localhost:8000
13+
//
14+
// You can also run a Unix domain socket (UDS) server like this:
15+
//
16+
// $ php examples/02-chat-server.php unix:///tmp/server.sock
17+
// $ nc -U /tmp/server.sock
1318

1419
use React\EventLoop\Factory;
1520
use React\Socket\Server;

examples/03-benchmark.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,21 @@
66
//
77
// $ php examples/03-benchmark.php 8000
88
// $ telnet localhost 8000
9-
// $ echo hello world | nc -v localhost 8000
10-
// $ dd if=/dev/zero bs=1M count=1000 | nc -v localhost 8000
9+
// $ echo hello world | nc -N localhost 8000
10+
// $ dd if=/dev/zero bs=1M count=1000 | nc -N localhost 8000
1111
//
1212
// You can also run a secure TLS benchmarking server like this:
1313
//
1414
// $ php examples/03-benchmark.php tls://127.0.0.1:8000 examples/localhost.pem
1515
// $ openssl s_client -connect localhost:8000
1616
// $ echo hello world | openssl s_client -connect localhost:8000
1717
// $ dd if=/dev/zero bs=1M count=1000 | openssl s_client -connect localhost:8000
18+
//
19+
// You can also run a Unix domain socket (UDS) server benchmark like this:
20+
//
21+
// $ php examples/03-benchmark.php unix:///tmp/server.sock
22+
// $ nc -N -U /tmp/server.sock
23+
// $ dd if=/dev/zero bs=1M count=1000 | nc -N -U /tmp/server.sock
1824

1925
use React\EventLoop\Factory;
2026
use React\Socket\Server;

src/Server.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ final class Server extends EventEmitter implements ServerInterface
1212
public function __construct($uri, LoopInterface $loop, array $context = array())
1313
{
1414
// sanitize TCP context options if not properly wrapped
15-
if ($context && (!isset($context['tcp']) && !isset($context['tls']))) {
15+
if ($context && (!isset($context['tcp']) && !isset($context['tls']) && !isset($context['unix']))) {
1616
$context = array('tcp' => $context);
1717
}
1818

1919
// apply default options if not explicitly given
2020
$context += array(
2121
'tcp' => array(),
2222
'tls' => array(),
23+
'unix' => array()
2324
);
2425

2526
$scheme = 'tcp';
@@ -28,10 +29,14 @@ public function __construct($uri, LoopInterface $loop, array $context = array())
2829
$scheme = substr($uri, 0, $pos);
2930
}
3031

31-
$server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']);
32+
if ($scheme === 'unix') {
33+
$server = new UnixServer($uri, $loop, $context['unix']);
34+
} else {
35+
$server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']);
3236

33-
if ($scheme === 'tls') {
34-
$server = new SecureServer($server, $loop, $context['tls']);
37+
if ($scheme === 'tls') {
38+
$server = new SecureServer($server, $loop, $context['tls']);
39+
}
3540
}
3641

3742
$this->server = $server;

src/UnixServer.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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 `UnixServer` class implements the `ServerInterface` and
12+
* is responsible for accepting plaintext connections on unix domain sockets.
13+
*
14+
* ```php
15+
* $server = new UnixServer('unix:///tmp/app.sock', $loop);
16+
* ```
17+
*
18+
* See also the `ServerInterface` for more details.
19+
*
20+
* @see ServerInterface
21+
* @see ConnectionInterface
22+
*/
23+
final class UnixServer extends EventEmitter implements ServerInterface
24+
{
25+
private $master;
26+
private $loop;
27+
private $listening = false;
28+
29+
/**
30+
* Creates a plaintext socket server and starts listening on the given unix socket
31+
*
32+
* This starts accepting new incoming connections on the given address.
33+
* See also the `connection event` documented in the `ServerInterface`
34+
* for more details.
35+
*
36+
* ```php
37+
* $server = new UnixServer('unix:///tmp/app.sock', $loop);
38+
* ```
39+
*
40+
* @param string $path
41+
* @param LoopInterface $loop
42+
* @param array $context
43+
* @throws InvalidArgumentException if the listening address is invalid
44+
* @throws RuntimeException if listening on this address fails (already in use etc.)
45+
*/
46+
public function __construct($path, LoopInterface $loop, array $context = array())
47+
{
48+
$this->loop = $loop;
49+
50+
if (strpos($path, '://') === false) {
51+
$path = 'unix://' . $path;
52+
} elseif (substr($path, 0, 7) !== 'unix://') {
53+
throw new \InvalidArgumentException('Given URI "' . $path . '" is invalid');
54+
}
55+
56+
$this->master = @stream_socket_server(
57+
$path,
58+
$errno,
59+
$errstr,
60+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
61+
stream_context_create(array('socket' => $context))
62+
);
63+
if (false === $this->master) {
64+
throw new RuntimeException('Failed to listen on unix domain socket "' . $path . '": ' . $errstr, $errno);
65+
}
66+
stream_set_blocking($this->master, 0);
67+
68+
$this->resume();
69+
}
70+
71+
public function getAddress()
72+
{
73+
if (!is_resource($this->master)) {
74+
return null;
75+
}
76+
77+
return 'unix://' . stream_socket_get_name($this->master, false);
78+
}
79+
80+
public function pause()
81+
{
82+
if (!$this->listening) {
83+
return;
84+
}
85+
86+
$this->loop->removeReadStream($this->master);
87+
$this->listening = false;
88+
}
89+
90+
public function resume()
91+
{
92+
if ($this->listening || !is_resource($this->master)) {
93+
return;
94+
}
95+
96+
$that = $this;
97+
$this->loop->addReadStream($this->master, function ($master) use ($that) {
98+
$newSocket = @stream_socket_accept($master);
99+
if (false === $newSocket) {
100+
$that->emit('error', array(new \RuntimeException('Error accepting new connection')));
101+
102+
return;
103+
}
104+
$that->handleConnection($newSocket);
105+
});
106+
$this->listening = true;
107+
}
108+
109+
public function close()
110+
{
111+
if (!is_resource($this->master)) {
112+
return;
113+
}
114+
115+
$this->pause();
116+
fclose($this->master);
117+
$this->removeAllListeners();
118+
}
119+
120+
/** @internal */
121+
public function handleConnection($socket)
122+
{
123+
$connection = new Connection($socket, $this->loop);
124+
$connection->unix = true;
125+
126+
$this->emit('connection', array(
127+
$connection
128+
));
129+
}
130+
}

tests/ServerTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44

55
use React\EventLoop\Factory;
66
use React\Socket\Server;
7+
use React\Socket\TcpConnector;
8+
use React\Socket\UnixConnector;
79
use Clue\React\Block;
810
use React\Socket\ConnectionInterface;
911

1012
class ServerTest extends TestCase
1113
{
14+
const TIMEOUT = 0.1;
15+
1216
public function testCreateServer()
1317
{
1418
$loop = Factory::create();
@@ -26,6 +30,38 @@ public function testConstructorThrowsForInvalidUri()
2630
$server = new Server('invalid URI', $loop);
2731
}
2832

33+
public function testConstructorCreatesExpectedTcpServer()
34+
{
35+
$loop = Factory::create();
36+
37+
$server = new Server(0, $loop);
38+
39+
$connector = new TcpConnector($loop);
40+
$connector->connect($server->getAddress())
41+
->then($this->expectCallableOnce(), $this->expectCallableNever());
42+
43+
$connection = Block\await($connector->connect($server->getAddress()), $loop, self::TIMEOUT);
44+
45+
$connection->close();
46+
$server->close();
47+
}
48+
49+
public function testConstructorCreatesExpectedUnixServer()
50+
{
51+
$loop = Factory::create();
52+
53+
$server = new Server($this->getRandomSocketUri(), $loop);
54+
55+
$connector = new UnixConnector($loop);
56+
$connector->connect($server->getAddress())
57+
->then($this->expectCallableOnce(), $this->expectCallableNever());
58+
59+
$connection = Block\await($connector->connect($server->getAddress()), $loop, self::TIMEOUT);
60+
61+
$connection->close();
62+
$server->close();
63+
}
64+
2965
public function testEmitsConnectionForNewConnection()
3066
{
3167
$loop = Factory::create();
@@ -127,4 +163,9 @@ public function testDoesNotEmitSecureConnectionForNewPlainConnection()
127163

128164
Block\sleep(0.1, $loop);
129165
}
166+
167+
private function getRandomSocketUri()
168+
{
169+
return "unix://" . sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid(rand(), true) . '.sock';
170+
}
130171
}

0 commit comments

Comments
 (0)