Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Response extends EventEmitter implements WritableStreamInterface
private $conn;
private $headWritten = false;
private $chunkedEncoding = true;
public $closeConnection = false;

public function __construct(ConnectionInterface $conn)
{
Expand Down Expand Up @@ -64,6 +65,12 @@ public function writeHead($status = 200, array $headers = array())
$headers['Transfer-Encoding'] = 'chunked';
}

if ($this->closeConnection) {
$headers['Connection'] = 'close';
} else {
$headers['Connection'] = 'keep-alive';
}

$data = $this->formatHead($status, $headers);
$this->conn->write($data);

Expand Down Expand Up @@ -119,7 +126,9 @@ public function end($data = null)

$this->emit('end');
$this->removeAllListeners();
$this->conn->end();
if ($this->closeConnection) {
$this->conn->end();
}
}

public function close()
Expand All @@ -133,6 +142,5 @@ public function close()
$this->writable = false;
$this->emit('close');
$this->removeAllListeners();
$this->conn->close();
}
}
98 changes: 66 additions & 32 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,88 @@
use React\Socket\ServerInterface as SocketServerInterface;
use React\Socket\ConnectionInterface;


class ConnProxy {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you extract this into it's own file

public $id;
public $count;
public $conn;
public function __construct($id, ConnectionInterface $conn)
{
$this->id = $id;
$this->conn = $conn;
$this->count = 0;
}
}

/** @event request */
class Server extends EventEmitter implements ServerInterface
{
private $io;

const KEEPALIVE_MAX_REQUEST = 25;
private $connectionsCount = 0;
public function __construct(SocketServerInterface $io)
{
$this->io = $io;

$this->io->on('connection', function (ConnectionInterface $conn) {
// TODO: http 1.1 keep-alive
// TODO: chunked transfer encoding (also for outgoing data)
// TODO: multipart parsing

$parser = new RequestHeaderParser();
$parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $parser) {
// attach remote ip to the request as metadata
$request->remoteAddress = $conn->getRemoteAddress();

$this->handleRequest($conn, $request, $bodyBuffer);

$conn->removeListener('data', array($parser, 'feed'));
$conn->on('end', function () use ($request) {
$request->emit('end');
});
$conn->on('data', function ($data) use ($request) {
$request->emit('data', array($data));
});
$request->on('pause', function () use ($conn) {
$conn->emit('pause');
});
$request->on('resume', function () use ($conn) {
$conn->emit('resume');
});
$io->on('connection', function (ConnectionInterface $conn) {
$this->connectionsCount += 1;
$this->handleConnection(new ConnProxy($this->connectionsCount, $conn));
});
}

public function handleConnection(ConnProxy $connProxy) {
$conn = $connProxy->conn;
// TODO: chunked transfer encoding (also for outgoing data)
// TODO: multipart parsing
$parser = new RequestHeaderParser();
$parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $parser, $connProxy) {
// attach remote ip to the request as metadata
$request->remoteAddress = $conn->getRemoteAddress();

$conn->removeListener('data', array($parser, 'feed'));
$conn->on('end', function () use ($request) {
$request->emit('end');
});
$conn->on('data', function ($data) use ($request) {
$request->emit('data', array($data));
});
$request->on('pause', function () use ($conn) {
$conn->emit('pause');
});
$request->on('resume', function () use ($conn) {
$conn->emit('resume');
});

$conn->on('data', array($parser, 'feed'));
$this->handleRequest($connProxy, $request, $bodyBuffer);
});

$conn->on('data', array($parser, 'feed'));
}

public function handleRequest(ConnectionInterface $conn, Request $request, $bodyBuffer)
public function handleRequest(ConnProxy $connProxy, Request $request, $bodyBuffer)
{
$conn = $connProxy->conn;
$response = new Response($conn);

//Keepalive max requests
$connProxy->count += 1;
if ($connProxy->count >= self::KEEPALIVE_MAX_REQUEST) {
$response->closeConnection = true;
}

$headers = $request->getHeaders();
if (empty($headers['Connection']) || $headers['Connection'] == 'close') {
$response->closeConnection = true;
}

$response->on('close', array($request, 'close'));

//Handle keepalive
$response->on('end', function() use ($conn, $response, $connProxy) {
if (! $response->closeConnection) {
$this->handleConnection($connProxy);
}
});

if (!$this->listeners('request')) {
$response->end();

return;
}

Expand Down
6 changes: 6 additions & 0 deletions tests/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public function testResponseShouldBeChunkedByDefault()
$expected .= "HTTP/1.1 200 OK\r\n";
$expected .= "X-Powered-By: React/alpha\r\n";
$expected .= "Transfer-Encoding: chunked\r\n";
$expected .= "Connection: keep-alive\r\n";
$expected .= "\r\n";

$conn = $this->getMock('React\Socket\ConnectionInterface');
Expand All @@ -30,6 +31,7 @@ public function testResponseShouldNotBeChunkedWithContentLength()
$expected .= "HTTP/1.1 200 OK\r\n";
$expected .= "X-Powered-By: React/alpha\r\n";
$expected .= "Content-Length: 22\r\n";
$expected .= "Connection: keep-alive\r\n";
$expected .= "\r\n";

$conn = $this->getMock('React\Socket\ConnectionInterface');
Expand Down Expand Up @@ -132,6 +134,7 @@ public function shouldRemoveNewlinesFromHeaders()
$expected .= "X-Powered-By: React/alpha\r\n";
$expected .= "FooBar: BazQux\r\n";
$expected .= "Transfer-Encoding: chunked\r\n";
$expected .= "Connection: keep-alive\r\n";
$expected .= "\r\n";

$conn = $this->getMock('React\Socket\ConnectionInterface');
Expand All @@ -151,6 +154,7 @@ public function missingStatusCodeTextShouldResultInNumberOnlyStatus()
$expected .= "HTTP/1.1 700 \r\n";
$expected .= "X-Powered-By: React/alpha\r\n";
$expected .= "Transfer-Encoding: chunked\r\n";
$expected .= "Connection: keep-alive\r\n";
$expected .= "\r\n";

$conn = $this->getMock('React\Socket\ConnectionInterface');
Expand All @@ -172,6 +176,7 @@ public function shouldAllowArrayHeaderValues()
$expected .= "Set-Cookie: foo=bar\r\n";
$expected .= "Set-Cookie: bar=baz\r\n";
$expected .= "Transfer-Encoding: chunked\r\n";
$expected .= "Connection: keep-alive\r\n";
$expected .= "\r\n";

$conn = $this->getMock('React\Socket\ConnectionInterface');
Expand All @@ -191,6 +196,7 @@ public function shouldIgnoreHeadersWithNullValues()
$expected .= "HTTP/1.1 200 OK\r\n";
$expected .= "X-Powered-By: React/alpha\r\n";
$expected .= "Transfer-Encoding: chunked\r\n";
$expected .= "Connection: keep-alive\r\n";
$expected .= "\r\n";

$conn = $this->getMock('React\Socket\ConnectionInterface');
Expand Down
72 changes: 70 additions & 2 deletions tests/ServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,79 @@ public function testResponseContainsPoweredByHeader()
$this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $conn->getData());
}

private function createGetRequest()
public function testKeepaliveCloseAfterMax()
{
$io = new ServerStub();

$server = new Server($io);
$server->on('request', function ($request, $response) {
$response->writeHead();
$response->end();
});

$conn = new ConnectionStub();
$io->emit('connection', array($conn));

for ($i=0; $i<Server::KEEPALIVE_MAX_REQUEST; $i++) {
$data = $this->createGetRequest(false);
$conn->emit('data', array($data));
$this->assertContains("\r\nConnection: keep-alive\r\n", $conn->getData());

if ($i < Server::KEEPALIVE_MAX_REQUEST - 1) {
$this->assertNotContains("\r\nConnection: close\r\n", $conn->getData());
} else {
$this->assertContains("\r\nConnection: close\r\n", $conn->getData());
}
}
}

public function testKeepaliveClose()
{
$io = new ServerStub();

$server = new Server($io);
$server->on('request', function ($request, $response) {
$response->writeHead();
$response->end();
});

$conn = new ConnectionStub();
$io->emit('connection', array($conn));

$data = $this->createGetRequest(true);
$conn->emit('data', array($data));

$this->assertContains("\r\nConnection: close\r\n", $conn->getData());
}

public function testKeepaliveKeep()
{
$io = new ServerStub();

$server = new Server($io);
$server->on('request', function ($request, $response) {
$response->writeHead();
$response->end();
});

$conn = new ConnectionStub();
$io->emit('connection', array($conn));

$data = $this->createGetRequest(false);
$conn->emit('data', array($data));

$this->assertContains("\r\nConnection: keep-alive\r\n", $conn->getData());
}

private function createGetRequest($close = true)
{
$data = "GET / HTTP/1.1\r\n";
$data .= "Host: example.com:80\r\n";
$data .= "Connection: close\r\n";
if ($close) {
$data .= "Connection: close\r\n";
} else {
$data .= "Connection: keep-alive\r\n";
}
$data .= "\r\n";

return $data;
Expand Down