Skip to content

Commit 00e481e

Browse files
authored
Merge pull request #467 from clue-labs/reuse-address
Improve performance, reuse server params for same connection
2 parents b5a66a4 + d92e564 commit 00e481e

File tree

2 files changed

+108
-34
lines changed

2 files changed

+108
-34
lines changed

src/Io/RequestHeaderParser.php

+51-33
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ class RequestHeaderParser extends EventEmitter
2727
/** @var Clock */
2828
private $clock;
2929

30+
/** @var array<string|int,array<string,string>> */
31+
private $connectionParams = array();
32+
3033
public function __construct(Clock $clock)
3134
{
3235
$this->clock = $clock;
@@ -66,8 +69,7 @@ public function handle(ConnectionInterface $conn)
6669
try {
6770
$request = $that->parseRequest(
6871
(string)\substr($buffer, 0, $endOfHeader + 2),
69-
$conn->getRemoteAddress(),
70-
$conn->getLocalAddress()
72+
$conn
7173
);
7274
} catch (Exception $exception) {
7375
$buffer = '';
@@ -119,13 +121,12 @@ public function handle(ConnectionInterface $conn)
119121

120122
/**
121123
* @param string $headers buffer string containing request headers only
122-
* @param ?string $remoteSocketUri
123-
* @param ?string $localSocketUri
124+
* @param ConnectionInterface $connection
124125
* @return ServerRequestInterface
125126
* @throws \InvalidArgumentException
126127
* @internal
127128
*/
128-
public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
129+
public function parseRequest($headers, ConnectionInterface $connection)
129130
{
130131
// additional, stricter safe-guard for request line
131132
// because request parser doesn't properly cope with invalid ones
@@ -160,26 +161,59 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
160161
}
161162
}
162163

164+
// reuse same connection params for all server params for this connection
165+
$cid = \PHP_VERSION_ID < 70200 ? \spl_object_hash($connection) : \spl_object_id($connection);
166+
if (isset($this->connectionParams[$cid])) {
167+
$serverParams = $this->connectionParams[$cid];
168+
} else {
169+
// assign new server params for new connection
170+
$serverParams = array();
171+
172+
// scheme is `http` unless TLS is used
173+
$localSocketUri = $connection->getLocalAddress();
174+
$localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri);
175+
if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') {
176+
$serverParams['HTTPS'] = 'on';
177+
}
178+
179+
// apply SERVER_ADDR and SERVER_PORT if server address is known
180+
// address should always be known, even for Unix domain sockets (UDS)
181+
// but skip UDS as it doesn't have a concept of host/port.
182+
if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) {
183+
$serverParams['SERVER_ADDR'] = $localParts['host'];
184+
$serverParams['SERVER_PORT'] = $localParts['port'];
185+
}
186+
187+
// apply REMOTE_ADDR and REMOTE_PORT if source address is known
188+
// address should always be known, unless this is over Unix domain sockets (UDS)
189+
$remoteSocketUri = $connection->getRemoteAddress();
190+
if ($remoteSocketUri !== null) {
191+
$remoteAddress = \parse_url($remoteSocketUri);
192+
$serverParams['REMOTE_ADDR'] = $remoteAddress['host'];
193+
$serverParams['REMOTE_PORT'] = $remoteAddress['port'];
194+
}
195+
196+
// remember server params for all requests from this connection, reset on connection close
197+
$this->connectionParams[$cid] = $serverParams;
198+
$params =& $this->connectionParams;
199+
$connection->on('close', function () use (&$params, $cid) {
200+
assert(\is_array($params));
201+
unset($params[$cid]);
202+
});
203+
}
204+
163205
// create new obj implementing ServerRequestInterface by preserving all
164206
// previous properties and restoring original request-target
165-
$serverParams = array(
166-
'REQUEST_TIME' => (int) ($now = $this->clock->now()),
167-
'REQUEST_TIME_FLOAT' => $now
168-
);
207+
$serverParams['REQUEST_TIME'] = (int) ($now = $this->clock->now());
208+
$serverParams['REQUEST_TIME_FLOAT'] = $now;
169209

170210
// scheme is `http` unless TLS is used
171-
$localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri);
172-
if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') {
173-
$scheme = 'https://';
174-
$serverParams['HTTPS'] = 'on';
175-
} else {
176-
$scheme = 'http://';
177-
}
211+
$scheme = isset($serverParams['HTTPS']) ? 'https://' : 'http://';
178212

179213
// default host if unset comes from local socket address or defaults to localhost
180214
$hasHost = $host !== null;
181215
if ($host === null) {
182-
$host = isset($localParts['host'], $localParts['port']) ? $localParts['host'] . ':' . $localParts['port'] : '127.0.0.1';
216+
$host = isset($serverParams['SERVER_ADDR'], $serverParams['SERVER_PORT']) ? $serverParams['SERVER_ADDR'] . ':' . $serverParams['SERVER_PORT'] : '127.0.0.1';
183217
}
184218

185219
if ($start['method'] === 'OPTIONS' && $start['target'] === '*') {
@@ -210,22 +244,6 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
210244
}
211245
}
212246

213-
// apply REMOTE_ADDR and REMOTE_PORT if source address is known
214-
// address should always be known, unless this is over Unix domain sockets (UDS)
215-
if ($remoteSocketUri !== null) {
216-
$remoteAddress = \parse_url($remoteSocketUri);
217-
$serverParams['REMOTE_ADDR'] = $remoteAddress['host'];
218-
$serverParams['REMOTE_PORT'] = $remoteAddress['port'];
219-
}
220-
221-
// apply SERVER_ADDR and SERVER_PORT if server address is known
222-
// address should always be known, even for Unix domain sockets (UDS)
223-
// but skip UDS as it doesn't have a concept of host/port.
224-
if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) {
225-
$serverParams['SERVER_ADDR'] = $localParts['host'];
226-
$serverParams['SERVER_PORT'] = $localParts['port'];
227-
}
228-
229247
$request = new ServerRequest(
230248
$start['method'],
231249
$uri,

tests/Io/RequestHeaderParserTest.php

+57-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
namespace React\Tests\Http\Io;
44

5+
use Psr\Http\Message\ServerRequestInterface;
56
use React\Http\Io\RequestHeaderParser;
67
use React\Tests\Http\TestCase;
7-
use Psr\Http\Message\ServerRequestInterface;
88

99
class RequestHeaderParserTest extends TestCase
1010
{
@@ -808,6 +808,62 @@ public function testServerParamsWontBeSetOnMissingUrls()
808808
$this->assertArrayNotHasKey('REMOTE_PORT', $serverParams);
809809
}
810810

811+
public function testServerParamsWillBeReusedForMultipleRequestsFromSameConnection()
812+
{
813+
$clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
814+
$clock->expects($this->exactly(2))->method('now')->willReturn(1652972091.3958);
815+
816+
$parser = new RequestHeaderParser($clock);
817+
818+
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock();
819+
$connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000');
820+
$connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://192.168.1.1:8001');
821+
822+
$parser->handle($connection);
823+
$connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"));
824+
825+
$request = null;
826+
$parser->on('headers', function ($parsedRequest) use (&$request) {
827+
$request = $parsedRequest;
828+
});
829+
830+
$parser->handle($connection);
831+
$connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"));
832+
833+
assert($request instanceof ServerRequestInterface);
834+
$serverParams = $request->getServerParams();
835+
836+
$this->assertArrayNotHasKey('HTTPS', $serverParams);
837+
$this->assertEquals(1652972091, $serverParams['REQUEST_TIME']);
838+
$this->assertEquals(1652972091.3958, $serverParams['REQUEST_TIME_FLOAT']);
839+
840+
$this->assertEquals('127.1.1.1', $serverParams['SERVER_ADDR']);
841+
$this->assertEquals('8000', $serverParams['SERVER_PORT']);
842+
843+
$this->assertEquals('192.168.1.1', $serverParams['REMOTE_ADDR']);
844+
$this->assertEquals('8001', $serverParams['REMOTE_PORT']);
845+
}
846+
847+
public function testServerParamsWillBeRememberedUntilConnectionIsClosed()
848+
{
849+
$clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
850+
851+
$parser = new RequestHeaderParser($clock);
852+
853+
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock();
854+
855+
$parser->handle($connection);
856+
$connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"));
857+
858+
$ref = new \ReflectionProperty($parser, 'connectionParams');
859+
$ref->setAccessible(true);
860+
861+
$this->assertCount(1, $ref->getValue($parser));
862+
863+
$connection->emit('close');
864+
$this->assertEquals(array(), $ref->getValue($parser));
865+
}
866+
811867
public function testQueryParmetersWillBeSet()
812868
{
813869
$request = null;

0 commit comments

Comments
 (0)