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

File tree

8 files changed

+524
-7
lines changed

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy