Skip to content

Commit 0460210

Browse files
committed
Merge pull request #46 from clue-labs/resolving
Support Connector without DNS
2 parents 5e3c4c6 + 0f07289 commit 0460210

File tree

7 files changed

+245
-118
lines changed

7 files changed

+245
-118
lines changed

README.md

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,42 +22,77 @@ order to complete:
2222
## Usage
2323

2424
In order to use this project, you'll need the following react boilerplate code
25-
to initialize the main loop and select your DNS server if you have not already
26-
set it up anyway.
25+
to initialize the main loop.
2726

2827
```php
2928
$loop = React\EventLoop\Factory::create();
30-
31-
$dnsResolverFactory = new React\Dns\Resolver\Factory();
32-
$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop);
3329
```
3430

3531
### Async TCP/IP connections
3632

37-
The `React\SocketClient\Connector` provides a single promise-based
38-
`create($host, $ip)` method which resolves as soon as the connection
33+
The `React\SocketClient\TcpConnector` provides a single promise-based
34+
`create($ip, $port)` method which resolves as soon as the connection
3935
succeeds or fails.
4036

4137
```php
42-
$connector = new React\SocketClient\Connector($loop, $dns);
38+
$tcpConnector = new React\SocketClient\TcpConnector($loop);
4339

44-
$connector->create('www.google.com', 80)->then(function (React\Stream\Stream $stream) {
40+
$tcpConnector->create('127.0.0.1', 80)->then(function (React\Stream\Stream $stream) {
4541
$stream->write('...');
46-
$stream->close();
42+
$stream->end();
4743
});
4844

4945
$loop->run();
5046
```
5147

48+
Note that this class only allows you to connect to IP/port combinations.
49+
If you want to connect to hostname/port combinations, see also the following chapter.
50+
51+
### DNS resolution
52+
53+
The `DnsConnector` class decorates a given `TcpConnector` instance by first
54+
looking up the given domain name and then establishing the underlying TCP/IP
55+
connection to the resolved IP address.
56+
57+
It provides the same promise-based `create($host, $port)` method which resolves with
58+
a `Stream` instance that can be used just like above.
59+
60+
Make sure to set up your DNS resolver and underlying TCP connector like this:
61+
62+
```php
63+
$dnsResolverFactory = new React\Dns\Resolver\Factory();
64+
$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop);
65+
66+
$dnsConnector = new React\SocketClient\DnsConnector($tcpConnector, $dns);
67+
68+
$dnsConnector->create('www.google.com', 80)->then(function (React\Stream\Stream $stream) {
69+
$stream->write('...');
70+
$stream->end();
71+
});
72+
73+
$loop->run();
74+
```
75+
76+
The legacy `Connector` class can be used for backwards-compatiblity reasons.
77+
It works very much like the newer `DnsConnector` but instead has to be
78+
set up like this:
79+
80+
```php
81+
$connector = new React\SocketClient\Connector($loop, $dns);
82+
83+
$connector->create('www.google.com', 80)->then($callback);
84+
```
85+
5286
### Async SSL/TLS connections
5387

5488
The `SecureConnector` class decorates a given `Connector` instance by enabling
55-
SSL/TLS encryption as soon as the raw TCP/IP connection succeeds. It provides
56-
the same promise- based `create($host, $ip)` method which resolves with
57-
a `Stream` instance that can be used just like any non-encrypted stream.
89+
SSL/TLS encryption as soon as the raw TCP/IP connection succeeds.
90+
91+
It provides the same promise- based `create($host, $port)` method which resolves with
92+
a `Stream` instance that can be used just like any non-encrypted stream:
5893

5994
```php
60-
$secureConnector = new React\SocketClient\SecureConnector($connector, $loop);
95+
$secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop);
6196

6297
$secureConnector->create('www.google.com', 443)->then(function (React\Stream\Stream $stream) {
6398
$stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n");

src/Connector.php

Lines changed: 6 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,100 +4,21 @@
44

55
use React\EventLoop\LoopInterface;
66
use React\Dns\Resolver\Resolver;
7-
use React\Stream\Stream;
8-
use React\Promise;
9-
use React\Promise\Deferred;
107

8+
/**
9+
* @deprecated Exists for BC only, consider using the newer DnsConnector instead
10+
*/
1111
class Connector implements ConnectorInterface
1212
{
13-
private $loop;
14-
private $resolver;
13+
private $connector;
1514

1615
public function __construct(LoopInterface $loop, Resolver $resolver)
1716
{
18-
$this->loop = $loop;
19-
$this->resolver = $resolver;
17+
$this->connector = new DnsConnector(new TcpConnector($loop), $resolver);
2018
}
2119

2220
public function create($host, $port)
2321
{
24-
return $this
25-
->resolveHostname($host)
26-
->then(function ($address) use ($port) {
27-
return $this->createSocketForAddress($address, $port);
28-
});
29-
}
30-
31-
public function createSocketForAddress($address, $port)
32-
{
33-
$url = $this->getSocketUrl($address, $port);
34-
35-
$flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
36-
$socket = stream_socket_client($url, $errno, $errstr, 0, $flags);
37-
38-
if (!$socket) {
39-
return Promise\reject(new \RuntimeException(
40-
sprintf("connection to %s:%d failed: %s", $address, $port, $errstr),
41-
$errno
42-
));
43-
}
44-
45-
stream_set_blocking($socket, 0);
46-
47-
// wait for connection
48-
49-
return $this
50-
->waitForStreamOnce($socket)
51-
->then(array($this, 'checkConnectedSocket'))
52-
->then(array($this, 'handleConnectedSocket'));
53-
}
54-
55-
protected function waitForStreamOnce($stream)
56-
{
57-
$deferred = new Deferred();
58-
59-
$loop = $this->loop;
60-
61-
$this->loop->addWriteStream($stream, function ($stream) use ($loop, $deferred) {
62-
$loop->removeWriteStream($stream);
63-
64-
$deferred->resolve($stream);
65-
});
66-
67-
return $deferred->promise();
68-
}
69-
70-
public function checkConnectedSocket($socket)
71-
{
72-
// The following hack looks like the only way to
73-
// detect connection refused errors with PHP's stream sockets.
74-
if (false === stream_socket_get_name($socket, true)) {
75-
return Promise\reject(new ConnectionException('Connection refused'));
76-
}
77-
78-
return Promise\resolve($socket);
79-
}
80-
81-
public function handleConnectedSocket($socket)
82-
{
83-
return new Stream($socket, $this->loop);
84-
}
85-
86-
protected function getSocketUrl($host, $port)
87-
{
88-
if (strpos($host, ':') !== false) {
89-
// enclose IPv6 addresses in square brackets before appending port
90-
$host = '[' . $host . ']';
91-
}
92-
return sprintf('tcp://%s:%s', $host, $port);
93-
}
94-
95-
protected function resolveHostname($host)
96-
{
97-
if (false !== filter_var($host, FILTER_VALIDATE_IP)) {
98-
return Promise\resolve($host);
99-
}
100-
101-
return $this->resolver->resolve($host);
22+
return $this->connector->create($host, $port);
10223
}
10324
}

src/DnsConnector.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace React\SocketClient;
4+
5+
use React\EventLoop\LoopInterface;
6+
use React\Dns\Resolver\Resolver;
7+
use React\Stream\Stream;
8+
use React\Promise;
9+
use React\Promise\Deferred;
10+
11+
class DnsConnector implements ConnectorInterface
12+
{
13+
private $connector;
14+
private $resolver;
15+
16+
public function __construct(ConnectorInterface $connector, Resolver $resolver)
17+
{
18+
$this->connector = $connector;
19+
$this->resolver = $resolver;
20+
}
21+
22+
public function create($host, $port)
23+
{
24+
$connector = $this->connector;
25+
26+
return $this
27+
->resolveHostname($host)
28+
->then(function ($address) use ($connector, $port) {
29+
return $connector->create($address, $port);
30+
});
31+
}
32+
33+
private function resolveHostname($host)
34+
{
35+
if (false !== filter_var($host, FILTER_VALIDATE_IP)) {
36+
return Promise\resolve($host);
37+
}
38+
39+
return $this->resolver->resolve($host);
40+
}
41+
}

src/TcpConnector.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
namespace React\SocketClient;
4+
5+
use React\EventLoop\LoopInterface;
6+
use React\Dns\Resolver\Resolver;
7+
use React\Stream\Stream;
8+
use React\Promise;
9+
use React\Promise\Deferred;
10+
11+
class TcpConnector implements ConnectorInterface
12+
{
13+
private $loop;
14+
15+
public function __construct(LoopInterface $loop)
16+
{
17+
$this->loop = $loop;
18+
}
19+
20+
public function create($ip, $port)
21+
{
22+
if (false === filter_var($ip, FILTER_VALIDATE_IP)) {
23+
return Promise\reject(new \InvalidArgumentException('Given parameter "' . $ip . '" is not a valid IP'));
24+
}
25+
26+
$url = $this->getSocketUrl($ip, $port);
27+
28+
$socket = stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT);
29+
30+
if (!$socket) {
31+
return Promise\reject(new \RuntimeException(
32+
sprintf("Connection to %s:%d failed: %s", $ip, $port, $errstr),
33+
$errno
34+
));
35+
}
36+
37+
stream_set_blocking($socket, 0);
38+
39+
// wait for connection
40+
41+
return $this
42+
->waitForStreamOnce($socket)
43+
->then(array($this, 'checkConnectedSocket'))
44+
->then(array($this, 'handleConnectedSocket'));
45+
}
46+
47+
private function waitForStreamOnce($stream)
48+
{
49+
$deferred = new Deferred();
50+
51+
$loop = $this->loop;
52+
53+
$this->loop->addWriteStream($stream, function ($stream) use ($loop, $deferred) {
54+
$loop->removeWriteStream($stream);
55+
56+
$deferred->resolve($stream);
57+
});
58+
59+
return $deferred->promise();
60+
}
61+
62+
/** @internal */
63+
public function checkConnectedSocket($socket)
64+
{
65+
// The following hack looks like the only way to
66+
// detect connection refused errors with PHP's stream sockets.
67+
if (false === stream_socket_get_name($socket, true)) {
68+
return Promise\reject(new ConnectionException('Connection refused'));
69+
}
70+
71+
return Promise\resolve($socket);
72+
}
73+
74+
/** @internal */
75+
public function handleConnectedSocket($socket)
76+
{
77+
return new Stream($socket, $this->loop);
78+
}
79+
80+
private function getSocketUrl($ip, $port)
81+
{
82+
if (strpos($ip, ':') !== false) {
83+
// enclose IPv6 addresses in square brackets before appending port
84+
$ip = '[' . $ip . ']';
85+
}
86+
return sprintf('tcp://%s:%s', $ip, $port);
87+
}
88+
}

tests/DnsConnectorTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace React\Tests\SocketClient;
4+
5+
use React\SocketClient\DnsConnector;
6+
use React\Promise;
7+
8+
class DnsConnectorTest extends TestCase
9+
{
10+
private $tcp;
11+
private $resolver;
12+
private $connector;
13+
14+
public function setUp()
15+
{
16+
$this->tcp = $this->getMock('React\SocketClient\ConnectorInterface');
17+
$this->resolver = $this->getMockBuilder('React\Dns\Resolver\Resolver')->disableOriginalConstructor()->getMock();
18+
19+
$this->connector = new DnsConnector($this->tcp, $this->resolver);
20+
}
21+
22+
public function testPassByResolverIfGivenIp()
23+
{
24+
$this->resolver->expects($this->never())->method('resolve');
25+
$this->tcp->expects($this->once())->method('create')->with($this->equalTo('127.0.0.1'), $this->equalTo(80));
26+
27+
$this->connector->create('127.0.0.1', 80);
28+
}
29+
30+
public function testPassThroughResolverIfGivenHost()
31+
{
32+
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4')));
33+
$this->tcp->expects($this->once())->method('create')->with($this->equalTo('1.2.3.4'), $this->equalTo(80));
34+
35+
$this->connector->create('google.com', 80);
36+
}
37+
38+
public function testSkipConnectionIfDnsFails()
39+
{
40+
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.invalid'))->will($this->returnValue(Promise\reject()));
41+
$this->tcp->expects($this->never())->method('create');
42+
43+
$this->connector->create('example.invalid', 80);
44+
}
45+
}

tests/IntegrationTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ public function gettingStuffFromGoogleShouldWork()
1818

1919
$factory = new Factory();
2020
$dns = $factory->create('8.8.8.8', $loop);
21+
$connector = new Connector($loop, $dns);
2122

2223
$connected = false;
2324
$response = null;
2425

25-
$connector = new Connector($loop, $dns);
2626
$connector->create('google.com', 80)
2727
->then(function ($conn) use (&$connected) {
2828
$connected = true;

0 commit comments

Comments
 (0)